Skip to content

Commit 8672c3a

Browse files
authored
Have higher-level examples first (#36)
1 parent cf22762 commit 8672c3a

File tree

1 file changed

+118
-118
lines changed

1 file changed

+118
-118
lines changed

lib/phoenix/sync/writer.ex

Lines changed: 118 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)