Sorcery.Mutation (sorcery v0.4.4)
This is the interface for creating changes in Sorcery. Mutations have some similarities to Ecto Changesets in the sense that we are building a struct that defines all the changes without actually applying them. But there are many differences, as you will soon see.
You start by creating the initial mutation with a portal.
Then you apply all the changes you want to the data in that portal.
At first nothing happens! This is because you must send the mutation up to the parent PortalServer.
After it applies the changes, it will dispatch them back down to any child PortalServers that care about any of the entities involved. This is where the magic of Sorcery really comes in. Under the hood, we are doing some pretty efficient reverse queries, in parallel, to figure out where to send the data.
All the PortalServers will update their Portals accordingly.
iex> alias Sorcery.Mutation, as: M
iex> m = M.init(socket.assigns.sorcery, :my_portal)
iex> m = M.update(m, [:player, 1, :age], fn _original_age, latest_age -> latest_age + 1 end)
iex> m = M.put(m, [:player, 1, :health], 100)
iex> ### We can also use placeholder ids so that new entities (which haven't even been created yet) can be referenced by other entities.
iex> m = M.create_entity(m, :team, "?my_new_team", %{name: "My New Team"})
iex> m = M.put(m, [:player, 1, :team_id], "?my_new_team.id")
iex> # This funny string is of course, the placeholder. But after we actually run the mutation, the team is created with a normal integer id.
iex> # The parent PortalServer creates the new team entity, it will automatically replace all calls to "?my_new_team" with that entity.
iex> Sorcery.Mutation.send_mutation(m)
iex> # ... a few milliseconds later, after receiving the update
iex> player = portal_view(@sorcery, :my_portal, "?all_players")[1]
iex> new_team_id = player.team_id
iex> is_integer(new_team_id)
true
iex> team = portal_view(@sorcery, :my_portal, "?all_teams")[new_team_id]
iex> team.name
"My New Team"
Summary
Functions
One of the benefits of a Mutation is that you can create entities using placeholder ids.
Sometimes we want to delete an entity entirely. Be careful, after the mutation is run, this cannot be undone
The first step of every mutation. You must have an existing portal in order to do any of this.
Just like put_in, but for mutations.
Sends the mutation to the corresponding Portal Server The Portal Server will then update its own data store. All portals that care about the changes will automatically update accordingly (including the one calling this function)
Mark a mutation to NOT be sent to the PortalServer. Instead such a mutation will return an error tuple. When using the LiveView helper with optimistic_mutation/2, skipped mutations will trigger put_flash/3
Kind of like a fusion between Kernel.update_in/3 and Map.update/4
The function must return either: :ok or {kind, reason}
Functions
create_entity(mutation, tk, lvar, body)
One of the benefits of a Mutation is that you can create entities using placeholder ids.
Examples
iex> m = M.create_entity(m, :team, "?my_new_team", %{name: "My New Team"})
iex> m = M.put(m, [:player, 1, :team_id], "?my_new_team")
iex> # It always defaults to using the :id, but you can specify another field
iex> m = M.put(m, [:player, 1, :team_id], "?my_new_team.my_field")
delete_entity(mutation, tk, id)
Sometimes we want to delete an entity entirely. Be careful, after the mutation is run, this cannot be undone
get_in(mutation, path)
init(state, portal_name)
The first step of every mutation. You must have an existing portal in order to do any of this.
Examples
iex> m = Sorcery.Mutation.init(socket.assigns.sorcery, :my_portal)
iex> is_struct(m)
true
put(mutation, path, value)
Just like put_in, but for mutations.
Examples
iex> m = Sorcery.Mutation.put(m, [:player, 1, :health], 100)
send_mutation(mutation, state)
Sends the mutation to the corresponding Portal Server The Portal Server will then update its own data store. All portals that care about the changes will automatically update accordingly (including the one calling this function)
Returns the mutation passed in.
The return state will include a temp_portal, which is handy for both testing, and for optimistic updates. When the PortalServer sends the new, fully updated data, then it will overwrite the portal and remove the temp_portal data. There are limitations to the temp_portal, and it should not be trusted too much.
Takes an optional keyword list of options. Available options: :optimistic, :handle_fail, :handle_success
:optimistic (true)
When true, this will attempt an optimistic update. Does not work great when creating a new entity because we do not yet have the id.
:handle_fail (nil)
Expects nil, or a callback function of the shape fn error, state -> state end
For example if you use Mutation.validate, which fails, then the error argument will be a map including keys :kind, :reason
:handle_success (nil)
Expects nil, or a callback function of the shape fn data, state -> state end
DO NOT try to manually persist the data into a portal. That happens automatically after SorceryDb does some work.
Instead this can be useful as a sanity test, or to verify that the transaction has, indeed, completed successfully.
send_mutation(mutation, state, opts)
skip(mutation, reason)
Mark a mutation to NOT be sent to the PortalServer. Instead such a mutation will return an error tuple. When using the LiveView helper with optimistic_mutation/2, skipped mutations will trigger put_flash/3
Examples
iex> mutation = skip(mutation, :info, "Testing")
iex> send_mutation(mutation, sorcery)
{:error, {:skip, :info, "Testing"}}
skip(mutation, kind, reason)
update(mutation, path, cb)
Kind of like a fusion between Kernel.update_in/3 and Map.update/4
You need to pass in both a path list, and a default value in case the original value is nil.
Examples
iex> Sorcery.Mutation.update(m, [:player, 1, :age], 100, fn _original_age, latest_age -> latest_age + 1 end)
validate(mutation, path, cb)
The function must return either: :ok or {kind, reason}
for example
{:error, "You can't do that."}
This is used by LiveHelpers.optimistic_mutation to skip mutations and show flash messages instead.
The function passed in takes two arguments, old_data and new_data Be careful, the new_data is not real. It is simply the best guess based on the current diff. By time the mutation reaches the portal server, it could be different!
Examples
iex> Mutation.validate(mutation, [:tk, :id, :some_attr], fn v ->
if v > 0, do: :ok, else: {:error, "Something went wrong"}
end)