@@ -16,124 +16,6 @@ defmodule Phoenix.Sync.Writer do
1616 This allows you to build instant, offline-capable applications that work with
1717 [local optimistic state](https://electric-sql.com/docs/guides/writes).
1818
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-
13719 ## Controller example
13820
13921 For example, take a project management app that's using
@@ -199,6 +81,124 @@ defmodule Phoenix.Sync.Writer do
19981 > That's what `#{ inspect ( __MODULE__ ) } ` is for: specifying which resources can be
20082 > updated and registering functions to authorize and validate the mutation payload.
20183
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+
202202 ## Transactions
203203
204204 The `txid` in the return value from `apply/4` and `txid/1` / `txid!/1` allows the
0 commit comments