@@ -1116,8 +1116,7 @@ defmodule Phoenix.Sync.Writer do
11161116 # operations before doing anything. So i can just add an error to a blank
11171117 # multi and return that and the transaction step will fail before touching
11181118 # the repo.
1119- with { :parse , { :ok , % Transaction { } = txn } } <- { :parse , parse_transaction ( writer , changes ) } ,
1120- { :check , :ok } <- { :check , check_transaction ( writer , txn ) } do
1119+ with { :ok , % Transaction { } = txn } <- parse_check ( writer , changes ) do
11211120 txn . operations
11221121 |> Enum . reduce (
11231122 start_multi ( txn ) ,
@@ -1129,7 +1128,34 @@ defmodule Phoenix.Sync.Writer do
11291128 end
11301129 end
11311130
1132- defp parse_transaction ( % __MODULE__ { } = writer , changes ) do
1131+ defp parse_check ( % __MODULE__ { } = writer , changes ) do
1132+ with { :parse , { :ok , % Transaction { } = txn } } <- { :parse , parse_transaction ( writer , changes ) } ,
1133+ { :check , :ok } <- { :check , check_transaction ( writer , txn ) } do
1134+ { :ok , txn }
1135+ end
1136+ end
1137+
1138+ @ doc """
1139+ Use the parser configured in the given [`Writer`](`#{ inspect ( __MODULE__ ) } `)
1140+ instance to decode the given transaction data.
1141+
1142+ This can be used to handle mutation operations explicitly:
1143+
1144+ {:ok, txn} = #{ inspect ( __MODULE__ ) } .parse_transaction(writer, my_json_tx_data)
1145+
1146+ {:ok, txid} =
1147+ Repo.transaction(fn ->
1148+ Enum.each(txn.operations, fn operation ->
1149+ # do something wih the given operation
1150+ # raise if something is wrong...
1151+ end)
1152+ # return the transaction id
1153+ #{ inspect ( __MODULE__ ) } .txid!(Repo)
1154+ end)
1155+ """
1156+ @ spec parse_transaction ( t ( ) , Format . transaction_data ( ) ) ::
1157+ { :ok , Transaction . t ( ) } | { :error , term ( ) }
1158+ def parse_transaction ( % __MODULE__ { } = writer , changes ) do
11331159 case writer . parser do
11341160 fun when is_function ( fun , 1 ) ->
11351161 fun . ( changes )
@@ -1529,7 +1555,80 @@ defmodule Phoenix.Sync.Writer do
15291555 end
15301556
15311557 @ doc """
1532- Extract the transaction id from changes returned from `Repo.transaction`.
1558+ Apply operations from a mutation transaction directly via a transaction.
1559+
1560+ `operation_fun` is a 1-arity function that receives each of the
1561+ `%#{ inspect ( __MODULE__ . Operation ) } {}` structs within the mutation data and
1562+ should apply them appropriately.
1563+
1564+ The `operation_fun` callback is done within a `c:Ecto.Repo.transaction/2` so
1565+ any exceptions will cause the entire transaction to be aborted.
1566+
1567+ This function will also raise if the transaction data fails to parse.
1568+
1569+ {:ok, txid} =
1570+ #{ inspect ( __MODULE__ ) } .new(format: #{ inspect ( __MODULE__ . Format.TanstackOptimistic ) } )
1571+ |> #{ inspect ( __MODULE__ ) } .transaction(
1572+ my_encoded_txn,
1573+ MyApp.Repo,
1574+ fn
1575+ %{operation: :insert, relation: [_, "todos"], change: change} ->
1576+ # insert a Todo
1577+ %{operation: :update, relation: [_, "todos"], data: data, change: change} ->
1578+ # update a Todo
1579+ %{operation: :delete, relation: [_, "todos"], data: data} ->
1580+ # delete a Todo
1581+ end
1582+ )
1583+
1584+ The `opts` are passed onto the `c:Ecto.Repo.transaction/2` call.
1585+
1586+ This is equivalent to the below:
1587+
1588+ {:ok, txn} =
1589+ #{ inspect ( __MODULE__ . Format.TanstackOptimistic ) } .parse_transaction(my_encoded_txn)
1590+
1591+ {:ok, txid} =
1592+ MyApp.Repo.transaction(fn ->
1593+ Enum.each(txn.operations, fn
1594+ %{operation: :insert, relation: [_, "todos"], change: change} ->
1595+ # insert a Todo
1596+ %{operation: :update, relation: [_, "todos"], data: data, change: change} ->
1597+ # update a Todo
1598+ %{operation: :delete, relation: [_, "todos"], data: data} ->
1599+ # delete a Todo
1600+ end)
1601+ #{ inspect ( __MODULE__ ) } .txid!(MyApp.Repo)
1602+ end)
1603+ """
1604+ @ spec transaction (
1605+ t ( ) ,
1606+ Format . transaction_data ( ) ,
1607+ Ecto.Repo . t ( ) ,
1608+ operation_fun :: ( Operation . t ( ) -> any ( ) ) ,
1609+ keyword ( )
1610+ ) ::
1611+ { :ok , txid ( ) } | { :error , any ( ) }
1612+ def transaction ( % __MODULE__ { } = writer , changes , repo , operation_fun , opts \\ [ ] )
1613+ when is_function ( operation_fun , 1 ) and is_atom ( repo ) do
1614+ case parse_transaction ( writer , changes ) do
1615+ { :ok , % Transaction { } = txn } ->
1616+ repo . transaction (
1617+ fn ->
1618+ Enum . each ( txn . operations , operation_fun )
1619+ txid! ( repo )
1620+ end ,
1621+ opts
1622+ )
1623+
1624+ { :error , reason } ->
1625+ raise Error , message: reason
1626+ end
1627+ end
1628+
1629+ @ doc """
1630+ Extract the transaction id from changes or from a `Ecto.Repo` within a
1631+ transaction.
15331632
15341633 This allows you to use a standard `c:Ecto.Repo.transaction/2` call to apply
15351634 mutations defined using `apply/2` and extract the transaction id afterwards.
@@ -1544,20 +1643,45 @@ defmodule Phoenix.Sync.Writer do
15441643 |> MyApp.Repo.transaction()
15451644
15461645 {:ok, txid} = Phoenix.Sync.Writer.txid(changes)
1646+
1647+ It also allows you to get a transaction id from any active transaction:
1648+
1649+ MyApp.Repo.transaction(fn ->
1650+ {:ok, txid} = #{ inspect ( __MODULE__ ) } .txid(MyApp.Repo)
1651+ end)
1652+
1653+ Attempting to run `txid/1` on a repo outside a transaction will return an
1654+ error.
15471655 """
15481656 @ spec txid ( Ecto.Multi . changes ( ) ) :: { :ok , txid ( ) } | :error
15491657 def txid ( % { @ txid_name => txid } = _changes ) , do: { :ok , txid }
1550- def txid ( _ ) , do: :error
1658+ def txid ( changes ) when is_map ( changes ) , do: :error
1659+
1660+ def txid ( repo ) when is_atom ( repo ) do
1661+ if repo . in_transaction? ( ) do
1662+ with { :ok , % { rows: [ [ txid ] ] } } = repo . query ( @ txid_query ) do
1663+ { :ok , txid }
1664+ end
1665+ else
1666+ { :error , % Error { message: "not in a transaction" } }
1667+ end
1668+ end
15511669
15521670 @ doc """
1553- Returns the transaction id from a `Ecto.Multi.changes()` result or raises if
1554- not found.
1671+ Returns the a transaction id or raises on an error.
15551672
15561673 See `txid/1`.
15571674 """
15581675 @ spec txid! ( Ecto.Multi . changes ( ) ) :: txid ( )
15591676 def txid! ( % { @ txid_name => txid } = _changes ) , do: txid
1560- def txid! ( _ ) , do: raise ( ArgumentError , message: "No txid in change data" )
1677+ def txid! ( % { } ) , do: raise ( ArgumentError , message: "No txid in change data" )
1678+
1679+ def txid! ( repo ) when is_atom ( repo ) do
1680+ case txid ( repo ) do
1681+ { :ok , txid } -> txid
1682+ { :error , reason } -> raise reason
1683+ end
1684+ end
15611685
15621686 @ doc """
15631687 Return a unique operation name for use in `pre_apply` or `post_apply` callbacks.
0 commit comments