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,
list: 3,
list: 4,
manual?: 1,
many_to_many: 2,
many_to_many: 3,
message: 1,

View file

@ -64,6 +64,8 @@ defmodule Ash.Actions.Create do
end
end
defp add_tenant({:ok, nil}, _), do: {:ok, nil}
defp add_tenant({:ok, data}, changeset) do
if Ash.Resource.Info.multitenancy_strategy(changeset.resource) do
{:ok, %{data | __metadata__: Map.put(data.__metadata__, :tenant, changeset.tenant)}}
@ -158,16 +160,20 @@ defmodule Ash.Actions.Create do
)
if changeset.valid? do
if upsert? do
resource
|> Ash.DataLayer.upsert(changeset)
|> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts)
if action.manual? do
{:ok, nil}
else
resource
|> Ash.DataLayer.create(changeset)
|> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts)
if upsert? do
resource
|> Ash.DataLayer.upsert(changeset)
|> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts)
else
resource
|> Ash.DataLayer.create(changeset)
|> add_tenant(changeset)
|> manage_relationships(api, changeset, engine_opts)
end
end
else
{:error, changeset.errors}
@ -175,8 +181,56 @@ defmodule Ash.Actions.Create do
end)
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, 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}
end
other ->
other
@ -194,6 +248,10 @@ defmodule Ash.Actions.Create do
)
end
defp manage_relationships({:ok, nil}, _, _, _) do
{:ok, nil}
end
defp manage_relationships({:ok, created}, api, changeset, engine_opts) do
with {:ok, loaded} <-
Ash.Actions.ManagedRelationships.load(api, created, changeset, engine_opts),

View file

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

View file

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

View file

@ -858,7 +858,7 @@ defmodule Ash.Changeset do
@spec with_hooks(
t(),
(t() ->
{:ok, Ash.Resource.record(), %{notifications: list(Ash.Notifier.Notification.t())}}
{:ok, term, %{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?,
:description,
accept: nil,
manual?: false,
require_attributes: [],
arguments: [],
changes: [],

View file

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

View file

@ -39,6 +39,47 @@ defmodule Ash.Resource.Actions.SharedOptions do
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?,
:description,
accept: nil,
manual?: false,
require_attributes: [],
arguments: [],
changes: [],