improvement: add manual? option for create/update/destroy

This commit is contained in:
Zach Daniel 2021-05-09 16:25:39 -04:00
parent 3bd2686de3
commit 7d37f8ae3d
9 changed files with 148 additions and 22 deletions

View file

@ -63,6 +63,7 @@ locals_without_parens = [
kind: 1, kind: 1,
list: 3, list: 3,
list: 4, list: 4,
manual?: 1,
many_to_many: 2, many_to_many: 2,
many_to_many: 3, many_to_many: 3,
message: 1, message: 1,

View file

@ -64,6 +64,8 @@ defmodule Ash.Actions.Create do
end end
end end
defp add_tenant({:ok, nil}, _), do: {:ok, nil}
defp add_tenant({:ok, data}, changeset) do defp add_tenant({:ok, data}, changeset) do
if Ash.Resource.Info.multitenancy_strategy(changeset.resource) do if Ash.Resource.Info.multitenancy_strategy(changeset.resource) do
{:ok, %{data | __metadata__: Map.put(data.__metadata__, :tenant, changeset.tenant)}} {:ok, %{data | __metadata__: Map.put(data.__metadata__, :tenant, changeset.tenant)}}
@ -158,6 +160,9 @@ defmodule Ash.Actions.Create do
) )
if changeset.valid? do if changeset.valid? do
if action.manual? do
{:ok, nil}
else
if upsert? do if upsert? do
resource resource
|> Ash.DataLayer.upsert(changeset) |> Ash.DataLayer.upsert(changeset)
@ -169,14 +174,63 @@ defmodule Ash.Actions.Create do
|> add_tenant(changeset) |> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts) |> manage_relationships(api, changeset, engine_opts)
end end
end
else else
{:error, changeset.errors} {:error, changeset.errors}
end end
end) end)
case result do case result do
{:ok, nil, _changeset, _instructions} ->
if action.manual? do
{:error,
"""
No record created in create action!
For manual actions, you must implement an `after_action` inside of a `change` that returns a newly created record.
For example:
# in the resource
action :special_create do
manual? true
change MyApp.DoCreate
end
# The change
defmodule MyApp.DoCreate do
use Ash.Resource.Change
def change(changeset, _, _) do
Ash.Changeset.after_action(changeset, fn changeset, _result ->
# result will be `nil`, because this is a manual action
result = do_something_that_creates_the_record(changeset)
{:ok, result}
end)
end
end
"""}
else
{:error, "No record created in create action!"}
end
{:ok, created, _changeset, instructions} -> {:ok, created, _changeset, instructions} ->
if action.manual? do
{:ok, created}
|> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts)
|> case do
{:ok, result} ->
{:ok, result, instructions}
{:error, error} ->
{:error, error}
end
else
{:ok, created, instructions} {:ok, created, instructions}
end
other -> other ->
other other
@ -194,6 +248,10 @@ defmodule Ash.Actions.Create do
) )
end end
defp manage_relationships({:ok, nil}, _, _, _) do
{:ok, nil}
end
defp manage_relationships({:ok, created}, api, changeset, engine_opts) do defp manage_relationships({:ok, created}, api, changeset, engine_opts) do
with {:ok, loaded} <- with {:ok, loaded} <-
Ash.Actions.ManagedRelationships.load(api, created, changeset, engine_opts), Ash.Actions.ManagedRelationships.load(api, created, changeset, engine_opts),

View file

@ -88,6 +88,9 @@ defmodule Ash.Actions.Destroy do
changeset changeset
|> Ash.Changeset.put_context(:private, %{actor: engine_opts[:actor]}) |> Ash.Changeset.put_context(:private, %{actor: engine_opts[:actor]})
|> Ash.Changeset.with_hooks(fn changeset -> |> Ash.Changeset.with_hooks(fn changeset ->
if action.manual? do
{:ok, record}
else
case Ash.DataLayer.destroy(resource, changeset) do case Ash.DataLayer.destroy(resource, changeset) do
:ok -> :ok ->
{:ok, record} {:ok, record}
@ -95,6 +98,7 @@ defmodule Ash.Actions.Destroy do
{:error, error} -> {:error, error} ->
{:error, error} {:error, error}
end end
end
end) end)
|> case do |> case do
{:ok, result, changeset, instructions} -> {:ok, result, changeset, instructions} ->

View file

@ -156,18 +156,37 @@ defmodule Ash.Actions.Update do
changeset = set_tenant(changeset) changeset = set_tenant(changeset)
if changeset.valid? do if changeset.valid? do
if action.manual? do
{:ok, nil}
else
resource resource
|> Ash.DataLayer.update(changeset) |> Ash.DataLayer.update(changeset)
|> add_tenant(changeset) |> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts) |> manage_relationships(api, changeset, engine_opts)
end
else else
{:error, changeset.errors} {:error, changeset.errors}
end end
end) end)
case result do case result do
{:ok, updated, _changeset, instructions} -> {:ok, updated, changeset, instructions} ->
if action.manual? do
updated = updated || changeset.data
{:ok, updated}
|> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts)
|> case do
{:ok, data} ->
{:ok, data, instructions}
{:error, error} ->
{:error, error}
end
else
{:ok, updated, instructions} {:ok, updated, instructions}
end
other -> other ->
other other

View file

@ -858,7 +858,7 @@ defmodule Ash.Changeset do
@spec with_hooks( @spec with_hooks(
t(), t(),
(t() -> (t() ->
{:ok, Ash.Resource.record(), %{notifications: list(Ash.Notifier.Notification.t())}} {:ok, term, %{notifications: list(Ash.Notifier.Notification.t())}}
| {:error, term}) | {:error, term})
) :: ) ::
{:ok, term, t(), %{notifications: list(Ash.Notifier.Notification.t())}} | {:error, term} {:ok, term, t(), %{notifications: list(Ash.Notifier.Notification.t())}} | {:error, term}

View file

@ -5,6 +5,7 @@ defmodule Ash.Resource.Actions.Create do
:primary?, :primary?,
:description, :description,
accept: nil, accept: nil,
manual?: false,
require_attributes: [], require_attributes: [],
arguments: [], arguments: [],
changes: [], changes: [],

View file

@ -6,6 +6,7 @@ defmodule Ash.Resource.Actions.Destroy do
:primary?, :primary?,
:soft?, :soft?,
:description, :description,
manual?: false,
arguments: [], arguments: [],
accept: nil, accept: nil,
changes: [], changes: [],

View file

@ -39,6 +39,47 @@ defmodule Ash.Resource.Actions.SharedOptions do
No need to include attributes that are `allow_nil?: false`. No need to include attributes that are `allow_nil?: false`.
""" """
],
manual?: [
type: :boolean,
doc: """
Instructs Ash to *skip* the actual update/create/destroy step.
All validation still takes place, but the `result` in any `after_action` callbacks
attached to that action will simply be the record that was read from the database initially.
For creates, the `result` will be `nil`, and you will be expected to handle the changeset in
an after_action callback and return an instance of the record. This is a good way to prevent
Ash from issuing an unnecessary update to the record, e.g updating the `updated_at` of the record
when an action actually only involves modifying relating records.
You could then handle the changeset automatically.
For example:
# in the action
```elixir
action :special_create do
manual? true
change MyApp.DoCreate
end
# The change
defmodule MyApp.DoCreate do
use Ash.Resource.Change
def change(changeset, _, _) do
Ash.Changeset.after_action(changeset, fn changeset, _result ->
# result will be `nil`, because this is a manual action
result = do_something_that_creates_the_record(changeset)
{:ok, result}
end)
end
end
```
"""
] ]
] ]

View file

@ -6,6 +6,7 @@ defmodule Ash.Resource.Actions.Update do
:primary?, :primary?,
:description, :description,
accept: nil, accept: nil,
manual?: false,
require_attributes: [], require_attributes: [],
arguments: [], arguments: [],
changes: [], changes: [],