changed hex_metadata.config
 
@@ -1,7 +1,7 @@
1
1
{<<"links">>,
2
2
[{<<"Source code">>,<<"https://github.com/electric-sql/phoenix_sync">>}]}.
3
3
{<<"name">>,<<"phoenix_sync">>}.
4
- {<<"version">>,<<"0.4.0">>}.
4
+ {<<"version">>,<<"0.4.1">>}.
5
5
{<<"description">>,
6
6
<<"Real-time sync for Postgres-backed Phoenix applications.">>}.
7
7
{<<"elixir">>,<<"~> 1.17">>}.
 
@@ -13,16 +13,16 @@
13
13
<<"lib/phoenix/sync/plug">>,<<"lib/phoenix/sync/plug/utils.ex">>,
14
14
<<"lib/phoenix/sync/gateway.ex">>,<<"lib/phoenix/sync/electric">>,
15
15
<<"lib/phoenix/sync/electric/client_adapter.ex">>,
16
- <<"lib/phoenix/sync/adapter.ex">>,<<"lib/phoenix/sync/controller.ex">>,
17
- <<"lib/phoenix/sync/predefined_shape.ex">>,<<"lib/phoenix/sync/router.ex">>,
18
- <<"lib/phoenix/sync/application.ex">>,<<"lib/phoenix/sync/client.ex">>,
19
- <<"lib/phoenix/sync/electric.ex">>,<<"lib/phoenix/sync/plug.ex">>,
20
- <<"lib/phoenix/sync/writer.ex">>,<<"lib/phoenix/sync/writer">>,
21
- <<"lib/phoenix/sync/writer/format.ex">>,
16
+ <<"lib/phoenix/sync/adapter.ex">>,
17
+ <<"lib/phoenix/sync/predefined_shape.ex">>,<<"lib/phoenix/sync/plug.ex">>,
18
+ <<"lib/phoenix/sync/writer">>,<<"lib/phoenix/sync/writer/format.ex">>,
22
19
<<"lib/phoenix/sync/writer/format">>,
23
20
<<"lib/phoenix/sync/writer/format/tanstack_db.ex">>,
24
21
<<"lib/phoenix/sync/writer/operation.ex">>,
25
22
<<"lib/phoenix/sync/writer/transaction.ex">>,
23
+ <<"lib/phoenix/sync/application.ex">>,<<"lib/phoenix/sync/controller.ex">>,
24
+ <<"lib/phoenix/sync/router.ex">>,<<"lib/phoenix/sync/client.ex">>,
25
+ <<"lib/phoenix/sync/writer.ex">>,<<"lib/phoenix/sync/electric.ex">>,
26
26
<<"lib/phoenix/sync/live_view.ex">>,<<"lib/phoenix/sync.ex">>,
27
27
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}.
28
28
{<<"requirements">>,
 
@@ -59,6 +59,6 @@
59
59
[{<<"name">>,<<"electric_client">>},
60
60
{<<"app">>,<<"electric_client">>},
61
61
{<<"optional">>,false},
62
- {<<"requirement">>,<<"== 0.3.0">>},
62
+ {<<"requirement">>,<<">= 0.5.0-beta-1">>},
63
63
{<<"repository">>,<<"hexpm">>}]]}.
64
64
{<<"build_tools">>,[<<"mix">>]}.
changed lib/phoenix/sync/electric.ex
 
@@ -519,16 +519,9 @@ defmodule Phoenix.Sync.Electric do
519
519
end
520
520
521
521
defp http_mode_plug_opts(electric_config) do
522
- with {:ok, url} <- fetch_with_error(electric_config, :url),
523
- credential_params = electric_config |> Keyword.get(:credentials, []) |> Map.new(),
524
- extra_params = electric_config |> Keyword.get(:params, []) |> Map.new(),
525
- params = Map.merge(extra_params, credential_params),
526
- {:ok, client} <-
527
- Electric.Client.new(
528
- base_url: url,
529
- params: params,
530
- fetch: {Electric.Client.Fetch.HTTP, [request: [raw: true]]}
531
- ) do
522
+ with {:ok, client} <- configure_client(electric_config, :http) do
523
+ # don't decode the body - just pass it directly
524
+ client = %{client | fetch: {Electric.Client.Fetch.HTTP, [request: [raw: true]]}}
532
525
{:ok, %Phoenix.Sync.Electric.ClientAdapter{client: client}}
533
526
end
534
527
end
 
@@ -543,13 +536,15 @@ defmodule Phoenix.Sync.Electric do
543
536
end
544
537
end
545
538
546
- defp configure_client(opts, :http) do
547
- case Keyword.fetch(opts, :url) do
548
- {:ok, url} ->
549
- Electric.Client.new(base_url: url)
550
-
551
- :error ->
552
- {:error, "`phoenix_sync[:electric][:url]` not set for phoenix_sync in HTTP mode"}
539
+ defp configure_client(electric_config, :http) do
540
+ with {:ok, url} <- fetch_with_error(electric_config, :url),
541
+ credential_params = electric_config |> Keyword.get(:credentials, []) |> Map.new(),
542
+ extra_params = electric_config |> Keyword.get(:params, []) |> Map.new(),
543
+ params = Map.merge(extra_params, credential_params) do
544
+ Electric.Client.new(
545
+ base_url: url,
546
+ params: params
547
+ )
553
548
end
554
549
end
555
550
end
 
@@ -568,11 +563,13 @@ if Code.ensure_loaded?(Electric.Shapes.Api) do
568
563
case Shapes.Api.validate(api, params) do
569
564
{:ok, request} ->
570
565
conn
566
+ |> content_type()
571
567
|> Plug.Conn.assign(:request, request)
572
568
|> Shapes.Api.serve_shape_log(request)
573
569
574
570
{:error, response} ->
575
571
conn
572
+ |> content_type()
576
573
|> Shapes.Api.Response.send(response)
577
574
|> Plug.Conn.halt()
578
575
end
 
@@ -582,11 +579,13 @@ if Code.ensure_loaded?(Electric.Shapes.Api) do
582
579
case Shapes.Api.validate_for_delete(api, params) do
583
580
{:ok, request} ->
584
581
conn
582
+ |> content_type()
585
583
|> Plug.Conn.assign(:request, request)
586
584
|> Shapes.Api.delete_shape(request)
587
585
588
586
{:error, response} ->
589
587
conn
588
+ |> content_type()
590
589
|> Shapes.Api.Response.send(response)
591
590
|> Plug.Conn.halt()
592
591
end
 
@@ -595,5 +594,9 @@ if Code.ensure_loaded?(Electric.Shapes.Api) do
595
594
def call(_api, %{method: "OPTIONS"} = conn, _params) do
596
595
Shapes.Api.options(conn)
597
596
end
597
+
598
+ defp content_type(conn) do
599
+ Plug.Conn.put_resp_content_type(conn, "application/json")
600
+ end
598
601
end
599
602
end
changed lib/phoenix/sync/live_view.ex
 
@@ -272,57 +272,44 @@ if Code.ensure_loaded?(Phoenix.Component) do
272
272
pid = self()
273
273
274
274
client
275
- |> Electric.Client.stream(query, live: false, replica: :full)
275
+ |> Electric.Client.stream(query, live: false, replica: :full, errors: :stream)
276
276
|> Stream.transform(
277
277
fn -> {[], nil} end,
278
- &live_stream_message(&1, &2, client, name, query, pid, component),
279
- &update_mode(&1, client, name, query, pid, component)
278
+ &live_stream_message/2,
279
+ &update_mode(&1, {client, name, query, pid, component})
280
280
)
281
281
end
282
282
283
283
defp live_stream_message(
284
284
%Message.ChangeMessage{headers: %{operation: :insert}, value: value},
285
- acc,
286
- _client,
287
- _name,
288
- _query,
289
- _pid,
290
- _component
285
+ acc
291
286
) do
292
287
{[value], acc}
293
288
end
294
289
295
- defp live_stream_message(
296
- %Message.ChangeMessage{headers: %{operation: operation}} = msg,
297
- {updates, resume},
298
- _client,
299
- _name,
300
- _query,
301
- _pid,
302
- _component
303
- )
304
- when operation in [:update, :delete] do
290
+ defp live_stream_message(%Message.ChangeMessage{} = msg, {updates, resume}) do
305
291
{[], {[msg | updates], resume}}
306
292
end
307
293
308
- defp live_stream_message(
309
- %Message.ResumeMessage{} = resume,
310
- {updates, nil},
311
- _client,
312
- _name,
313
- _query,
314
- _pid,
315
- _component
316
- ) do
317
- {[], {updates, resume}}
318
- end
319
-
320
- defp live_stream_message(_message, acc, _client, _name, _query, _pid, _component) do
294
+ defp live_stream_message(%Message.ControlMessage{}, acc) do
321
295
{[], acc}
322
296
end
323
297
324
- defp update_mode({updates, resume}, client, name, query, pid, component) do
298
+ defp live_stream_message(%Message.ResumeMessage{} = resume, {updates, nil}) do
299
+ {[], {updates, resume}}
300
+ end
301
+
302
+ defp live_stream_message(%Electric.Client.Error{} = error, _acc) do
303
+ {[], {error, nil}}
304
+ end
305
+
306
+ defp update_mode({%Electric.Client.Error{} = error, _resume}, _state) do
307
+ raise error
308
+ end
309
+
310
+ defp update_mode({updates, resume}, {client, name, query, pid, component}) do
325
311
# need to send every update as a separate message.
312
+
326
313
for event <- updates |> Enum.reverse() |> Enum.map(&wrap_msg(&1, name, component)),
327
314
do: send(pid, {:sync, event})
changed lib/phoenix/sync/router.ex
 
@@ -94,7 +94,7 @@ defmodule Phoenix.Sync.Router do
94
94
more details on keyword-based shapes.
95
95
"""
96
96
defmacro sync(path, opts) when is_list(opts) do
97
- route(env!(__CALLER__), path, build_definition(path, __CALLER__, opts))
97
+ route(env!(__CALLER__), path, build_definition(__CALLER__, opts))
98
98
end
99
99
100
100
# e.g. shape "/path", Ecto.Query.from(t in MyTable)
 
@@ -140,13 +140,13 @@ defmodule Phoenix.Sync.Router do
140
140
end
141
141
end
142
142
143
- defp build_definition(path, caller, opts) when is_list(opts) do
143
+ defp build_definition(caller, opts) when is_list(opts) do
144
144
case Keyword.fetch(opts, :query) do
145
145
{:ok, queryable} ->
146
146
build_shape_from_query(queryable, caller, opts)
147
147
148
148
:error ->
149
- define_shape(path, caller, opts)
149
+ define_shape(caller, opts)
150
150
end
151
151
end
152
152
 
@@ -167,23 +167,21 @@ defmodule Phoenix.Sync.Router do
167
167
end
168
168
end
169
169
170
- defp define_shape(path, caller, opts) do
171
- relation = build_relation(path, opts)
170
+ defp define_shape(caller, opts) do
171
+ relation = build_relation(opts)
172
172
173
173
{storage, _binding} = Code.eval_quoted(opts[:storage], [], caller)
174
174
175
175
Phoenix.Sync.PredefinedShape.new!(Keyword.merge(opts, relation), storage: storage)
176
176
end
177
177
178
- defp build_relation(path, opts) do
178
+ defp build_relation(opts) do
179
179
case Keyword.fetch(opts, :table) do
180
180
{:ok, table} ->
181
181
[table: table]
182
182
183
183
:error ->
184
- raise ArgumentError,
185
- message:
186
- "No valid table specified. The path #{inspect(path)} is not a valid table name and no `:table` option passed."
184
+ raise ArgumentError, message: "Cannot build shape: no :table specified."
187
185
end
188
186
|> add_namespace(opts)
189
187
end
changed lib/phoenix/sync/writer.ex
 
@@ -16,124 +16,6 @@ defmodule Phoenix.Sync.Writer do
16
16
This allows you to build instant, offline-capable applications that work with
17
17
[local optimistic state](https://electric-sql.com/docs/guides/writes).
18
18
19
- ## Usage levels ([low](#module-low-level-usage-diy), [mid](#module-mid-level-usage), [high](#module-high-level-usage))
20
-
21
- You don't need to use `#{inspect(__MODULE__)}` to ingest write operations using Phoenix.
22
- Phoenix already ships with primitives like `Ecto.Multi` and `c:Ecto.Repo.transaction/2`.
23
- However, `#{inspect(__MODULE__)}` provides:
24
-
25
- - a number of convienience functions that simplify ingesting mutation operations
26
- - a high-level pipeline that dries up a lot of common boilerplate and allows you to re-use
27
- your existing `Plug` and `Ecto.Changeset` logic
28
-
29
- ### Low-level usage (DIY)
30
-
31
- If you're comfortable parsing, validating and persisting changes yourself then the
32
- simplest way to use `#{inspect(__MODULE__)}` is to use `txid!/1` within
33
- `c:Ecto.Repo.transaction/2`:
34
-
35
- {:ok, txid} =
36
- MyApp.Repo.transaction(fn ->
37
- # ... save your changes to the database ...
38
-
39
- # Return the transaction id.
40
- #{inspect(__MODULE__)}.txid!(MyApp.Repo)
41
- end)
42
-
43
- This returns the database transaction ID that the changes were applied within. This allows
44
- you to return it to the client, which can then monitor the read-path sync stream to detect
45
- when the transaction syncs through. At which point the client can discard its local
46
- optimistic state.
47
-
48
- A convienient way of doing this is to parse the request data into a list of
49
- `#{inspect(__MODULE__)}.Operation`s using a `#{inspect(__MODULE__)}.Format`.
50
- You can then apply the changes yourself by matching on the operation data:
51
-
52
- {:ok, %Transaction{operations: operations}} =
53
- #{inspect(__MODULE__)}.parse_transaction(
54
- my_encoded_txn,
55
- format: #{inspect(__MODULE__.Format.TanstackDB)}
56
- )
57
-
58
- {:ok, txid} =
59
- MyApp.Repo.transaction(fn ->
60
- Enum.each(txn.operations, fn
61
- %{operation: :insert, relation: [_, "todos"], change: change} ->
62
- # insert a Todo
63
- %{operation: :update, relation: [_, "todos"], data: data, change: change} ->
64
- # update a Todo
65
- %{operation: :delete, relation: [_, "todos"], data: data} ->
66
- # for example, if you don't want to allow deletes...
67
- raise "invalid delete"
68
- end)
69
-
70
- #{inspect(__MODULE__)}.txid!(MyApp.Repo)
71
- end, timeout: 60_000)
72
-
73
- ### Mid-level usage
74
-
75
- The pattern above is wrapped-up into the more convienient `transact/4` function.
76
- This abstracts the parsing and txid details whilst still allowing you to handle
77
- and apply mutation operations yourself:
78
-
79
- {:ok, txid} =
80
- #{inspect(__MODULE__)}.transact(
81
- my_encoded_txn,
82
- MyApp.Repo,
83
- fn
84
- %{operation: :insert, relation: [_, "todos"], change: change} ->
85
- MyApp.Repo.insert(...)
86
- %{operation: :update, relation: [_, "todos"], data: data, change: change} ->
87
- MyApp.Repo.update(Ecto.Changeset.cast(...))
88
- %{operation: :delete, relation: [_, "todos"], data: data} ->
89
- # we don't allow deletes...
90
- {:error, "invalid delete"}
91
- end,
92
- format: #{inspect(__MODULE__.Format.TanstackDB)},
93
- timeout: 60_000
94
- )
95
-
96
- However, with larger applications, this flexibility can become tiresome as you end up
97
- repeating boilerplate and defining your own pipeline to authorize, validate and apply
98
- changes with the right error handling and return values.
99
-
100
- ### High-level usage
101
-
102
- To avoid this, `#{inspect(__MODULE__)}` provides a higer level pipeline that dries up
103
- the boilerplate, whilst still allowing flexibility and extensibility. You create an
104
- ingest pipeline by instantiating a `#{inspect(__MODULE__)}` instance and piping into
105
- `allow/3` and `apply/4` calls:
106
-
107
- {:ok, txid, _changes} =
108
- #{inspect(__MODULE__)}.new()
109
- |> #{inspect(__MODULE__)}.allow(MyApp.Todo)
110
- |> #{inspect(__MODULE__)}.allow(MyApp.OtherSchema)
111
- |> #{inspect(__MODULE__)}.apply(transaction, Repo, format: MyApp.MutationFormat)
112
-
113
- Or, instead of `apply/4` you can use seperate calls to `ingest/3` and then `transaction/2`.
114
- This allows you to ingest multiple formats, for example:
115
-
116
- {:ok, txid} =
117
- #{inspect(__MODULE__)}.new()
118
- |> #{inspect(__MODULE__)}.allow(MyApp.Todo)
119
- |> #{inspect(__MODULE__)}.ingest(changes, format: MyApp.MutationFormat)
120
- |> #{inspect(__MODULE__)}.ingest(other_changes, parser: &MyApp.MutationFormat.parse_other/1)
121
- |> #{inspect(__MODULE__)}.ingest(more_changes, parser: {MyApp.MutationFormat, :parse_more, []})
122
- |> #{inspect(__MODULE__)}.transaction(MyApp.Repo)
123
-
124
- And at any point you can drop down / eject out to the underlying `Ecto.Multi` using
125
- `to_multi/1` or `to_multi/3`:
126
-
127
- multi =
128
- #{inspect(__MODULE__)}.new()
129
- |> #{inspect(__MODULE__)}.allow(MyApp.Todo)
130
- |> #{inspect(__MODULE__)}.to_multi(changes, format: MyApp.MutationFormat)
131
-
132
- # ... do anything you like with the multi ...
133
-
134
- {:ok, changes} = Repo.transaction(multi)
135
- {:ok, txid} = #{inspect(__MODULE__)}.txid(changes)
136
-
137
19
## Controller example
138
20
139
21
For example, take a project management app that's using
 
@@ -199,6 +81,124 @@ defmodule Phoenix.Sync.Writer do
199
81
> That's what `#{inspect(__MODULE__)}` is for: specifying which resources can be
200
82
> updated and registering functions to authorize and validate the mutation payload.
201
83
84
+ ## Usage levels ([high](#module-high-level-usage), [mid](#module-mid-level-usage), [low](#module-low-level-usage-diy))
85
+
86
+ You don't need to use `#{inspect(__MODULE__)}` to ingest write operations using Phoenix.
87
+ Phoenix already ships with primitives like `Ecto.Multi` and `c:Ecto.Repo.transaction/2`.
88
+ However, `#{inspect(__MODULE__)}` provides:
89
+
90
+ - a number of convienience functions that simplify ingesting mutation operations
91
+ - a high-level pipeline that dries up a lot of common boilerplate and allows you to re-use
92
+ your existing `Plug` and `Ecto.Changeset` logic
93
+
94
+ ### High-level usage
95
+
96
+ The controller example above uses a higher level pipeline that dries up common
97
+ boilerplate, whilst still allowing flexibility and extensibility. You create an
98
+ ingest pipeline by instantiating a `#{inspect(__MODULE__)}` instance and piping into
99
+ `allow/3` and `apply/4` calls:
100
+
101
+ {:ok, txid, _changes} =
102
+ #{inspect(__MODULE__)}.new()
103
+ |> #{inspect(__MODULE__)}.allow(MyApp.Todo)
104
+ |> #{inspect(__MODULE__)}.allow(MyApp.OtherSchema)
105
+ |> #{inspect(__MODULE__)}.apply(transaction, Repo, format: MyApp.MutationFormat)
106
+
107
+ Or, instead of `apply/4` you can use seperate calls to `ingest/3` and then `transaction/2`.
108
+ This allows you to ingest multiple formats, for example:
109
+
110
+ {:ok, txid} =
111
+ #{inspect(__MODULE__)}.new()
112
+ |> #{inspect(__MODULE__)}.allow(MyApp.Todo)
113
+ |> #{inspect(__MODULE__)}.ingest(changes, format: MyApp.MutationFormat)
114
+ |> #{inspect(__MODULE__)}.ingest(other_changes, parser: &MyApp.MutationFormat.parse_other/1)
115
+ |> #{inspect(__MODULE__)}.ingest(more_changes, parser: {MyApp.MutationFormat, :parse_more, []})
116
+ |> #{inspect(__MODULE__)}.transaction(MyApp.Repo)
117
+
118
+ And at any point you can drop down / eject out to the underlying `Ecto.Multi` using
119
+ `to_multi/1` or `to_multi/3`:
120
+
121
+ multi =
122
+ #{inspect(__MODULE__)}.new()
123
+ |> #{inspect(__MODULE__)}.allow(MyApp.Todo)
124
+ |> #{inspect(__MODULE__)}.to_multi(changes, format: MyApp.MutationFormat)
125
+
126
+ # ... do anything you like with the multi ...
127
+
128
+ {:ok, changes} = Repo.transaction(multi)
129
+ {:ok, txid} = #{inspect(__MODULE__)}.txid(changes)
130
+
131
+ ### Mid-level usage
132
+
133
+ The pattern above uses a lower-level `transact/4` function.
134
+ This abstracts the mechanical details of transaction management whilst
135
+ still allowing you to handle and apply mutation operations yourself:
136
+
137
+ {:ok, txid} =
138
+ #{inspect(__MODULE__)}.transact(
139
+ my_encoded_txn,
140
+ MyApp.Repo,
141
+ fn
142
+ %{operation: :insert, relation: [_, "todos"], change: change} ->
143
+ MyApp.Repo.insert(...)
144
+ %{operation: :update, relation: [_, "todos"], data: data, change: change} ->
145
+ MyApp.Repo.update(Ecto.Changeset.cast(...))
146
+ %{operation: :delete, relation: [_, "todos"], data: data} ->
147
+ # we don't allow deletes...
148
+ {:error, "invalid delete"}
149
+ end,
150
+ format: #{inspect(__MODULE__.Format.TanstackDB)},
151
+ timeout: 60_000
152
+ )
153
+
154
+ However, with larger applications, this flexibility can become tiresome as you end up
155
+ repeating boilerplate and defining your own pipeline to authorize, validate and apply
156
+ changes with the right error handling and return values.
157
+
158
+ ### Low-level usage (DIY)
159
+
160
+ For the more advanced cases, if you're comfortable parsing, validating and persisting
161
+ changes yourself then the simplest way to use `#{inspect(__MODULE__)}` is to use `txid!/1`
162
+ within `c:Ecto.Repo.transaction/2`:
163
+
164
+ {:ok, txid} =
165
+ MyApp.Repo.transaction(fn ->
166
+ # ... save your changes to the database ...
167
+
168
+ # Return the transaction id.
169
+ #{inspect(__MODULE__)}.txid!(MyApp.Repo)
170
+ end)
171
+
172
+ This returns the database transaction ID that the changes were applied within. This allows
173
+ you to return it to the client, which can then monitor the read-path sync stream to detect
174
+ when the transaction syncs through. At which point the client can discard its local
175
+ optimistic state.
176
+
177
+ A convinient way of doing this is to parse the request data into a list of
178
+ `#{inspect(__MODULE__)}.Operation`s using a `#{inspect(__MODULE__)}.Format`.
179
+ You can then apply the changes yourself by matching on the operation data:
180
+
181
+ {:ok, %Transaction{operations: operations}} =
182
+ #{inspect(__MODULE__)}.parse_transaction(
183
+ my_encoded_txn,
184
+ format: #{inspect(__MODULE__.Format.TanstackDB)}
185
+ )
186
+
187
+ {:ok, txid} =
188
+ MyApp.Repo.transaction(fn ->
189
+ Enum.each(txn.operations, fn
190
+ %{operation: :insert, relation: [_, "todos"], change: change} ->
191
+ # insert a Todo
192
+ %{operation: :update, relation: [_, "todos"], data: data, change: change} ->
193
+ # update a Todo
194
+ %{operation: :delete, relation: [_, "todos"], data: data} ->
195
+ # for example, if you don't want to allow deletes...
196
+ raise "invalid delete"
197
+ end)
198
+
199
+ #{inspect(__MODULE__)}.txid!(MyApp.Repo)
200
+ end, timeout: 60_000)
201
+
202
202
## Transactions
203
203
204
204
The `txid` in the return value from `apply/4` and `txid/1` / `txid!/1` allows the
changed mix.exs
 
@@ -2,7 +2,7 @@ defmodule Phoenix.Sync.MixProject do
2
2
use Mix.Project
3
3
4
4
# Remember to update the README when you change the version
5
- @version "0.4.0"
5
+ @version "0.4.1"
6
6
7
7
def project do
8
8
[
 
@@ -39,7 +39,7 @@ defmodule Phoenix.Sync.MixProject do
39
39
# require an exact version because electric moves very quickly atm
40
40
# and a more generous specification would inevitably break.
41
41
{:electric, "== 1.0.1", optional: true},
42
- {:electric_client, "== 0.3.0"}
42
+ {:electric_client, ">= 0.5.0-beta-1"}
43
43
] ++ deps_for_env(Mix.env())
44
44
end