Current section
Files
Jump to
Current section
Files
lib/ash/actions/update.ex
defmodule Ash.Actions.Update do
@moduledoc false
alias Ash.Engine
alias Ash.Engine.Request
alias Ash.Actions.{Relationships, SideLoad}
require Logger
@spec run(Ash.api(), Ash.record(), Ash.action(), Keyword.t()) ::
{:ok, Ash.record()} | {:error, Ecto.Changeset.t()} | {:error, Ash.error()}
def run(api, changeset, action, opts) do
side_load = opts[:side_load] || []
engine_opts =
opts
|> Keyword.take([:verbose?, :actor, :authorize?])
|> Keyword.put(:transaction?, true)
resource = changeset.resource
with %{valid?: true} = changeset <- changeset(changeset, api),
{:ok, side_load_query} <-
side_loads_as_query(changeset.api, changeset.resource, side_load),
side_load_requests <-
SideLoad.requests(side_load_query),
%{data: %{commit: %^resource{} = updated}, errors: []} = state <-
do_run_requests(
changeset,
engine_opts,
action,
resource,
api,
side_load_requests
) do
{:ok, SideLoad.attach_side_loads(updated, state)}
else
%Ash.Changeset{errors: errors} ->
{:error, Ash.Error.to_ash_error(errors)}
%{errors: errors} ->
{:error, Ash.Error.to_ash_error(errors)}
{:error, error} ->
{:error, Ash.Error.to_ash_error(error)}
end
end
defp changeset(changeset, api) do
%{changeset | api: api}
|> Relationships.handle_relationship_changes()
|> set_defaults()
|> add_validations()
end
defp add_validations(changeset) do
changeset.resource()
|> Ash.Resource.validations(:update)
|> Enum.reduce(changeset, fn validation, changeset ->
Ash.Changeset.before_action(changeset, &do_validation(&1, validation))
end)
end
defp do_validation(changeset, validation) do
case validation.module.validate(changeset, validation.opts) do
:ok -> changeset
{:error, error} -> Ash.Changeset.add_error(changeset, error)
end
end
defp set_defaults(changeset) do
changeset.resource
|> Ash.Resource.attributes()
|> Enum.filter(& &1.update_default)
|> Enum.reduce(changeset, fn attribute, changeset ->
Ash.Changeset.change_new_attribute_lazy(changeset, attribute.name, fn ->
default(attribute.update_default)
end)
end)
end
defp do_run_requests(
changeset,
engine_opts,
action,
resource,
api,
side_load_requests
) do
authorization_request =
Request.new(
api: api,
changeset: Relationships.changeset(changeset),
action: action,
resource: resource,
data: changeset.data,
path: :data,
name: "#{action.type} - `#{action.name}`: prepare"
)
commit_request =
Request.new(
api: api,
changeset:
Request.resolve([[:data, :changeset]], fn %{data: %{changeset: changeset}} ->
{:ok, changeset}
end),
action: action,
resource: resource,
data:
Request.resolve(
[[:data, :changeset]],
fn %{data: %{changeset: changeset}} ->
Ash.Changeset.with_hooks(changeset, fn changeset ->
Ash.DataLayer.update(resource, changeset)
end)
end
),
path: [:commit],
name: "#{action.type} - `#{action.name}` commit"
)
relationship_requests = changeset.requests
Engine.run(
[authorization_request | [commit_request | relationship_requests]] ++ side_load_requests,
api,
engine_opts
)
end
defp side_loads_as_query(_api, _resource, nil), do: {:ok, nil}
defp side_loads_as_query(_api, _resource, %Ash.Query{errors: []} = query), do: {:ok, query}
defp side_loads_as_query(_api, _resource, %Ash.Query{errors: errors}), do: {:error, errors}
defp side_loads_as_query(api, resource, side_loads) when is_list(side_loads) do
resource
|> Ash.Query.new(api)
|> Ash.Query.side_load(side_loads)
|> case do
%{errors: []} = query -> {:ok, query}
%{errors: errors} -> {:error, errors}
end
end
defp default({:constant, value}), do: value
defp default({mod, func, args}), do: apply(mod, func, args)
defp default(function) when is_function(function, 0), do: function.()
end