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
|