improvement: add skip_global_validations? option for actions

This commit is contained in:
Zach Daniel 2023-03-21 13:33:48 -04:00
parent ddf78ca1f2
commit ed45a72ea6
9 changed files with 133 additions and 14 deletions

View file

@ -175,6 +175,7 @@ spark_locals_without_parens = [
sensitive?: 1,
short_name: 1,
simple_notifiers: 1,
skip_global_validations?: 1,
soft?: 1,
sort: 1,
source: 1,

View file

@ -192,7 +192,7 @@ The following steps are run when calling `Ash.Changeset.for_create/4`, `Ash.Chan
- Require any accepted attributes that are `allow_nil?` false
- Set any default values for attributes
- Run action changes & validations
- Run validations, or add them in `before_action` hooks if using `d:Ash.Resource.Dsl.actions.create.validate|before_action?`
- Run validations, or add them in `before_action` hooks if using `d:Ash.Resource.Dsl.actions.create.validate|before_action?`. Any global validations are skipped if the action has `skip_global_validations?` set to `true`.
#### Running the Create/Update/Destroy Action

View file

@ -1257,18 +1257,22 @@ defmodule Ash.Changeset do
defp default(:update, %{update_default: value}), do: value
defp add_validations(changeset, tracer, metadata, actor) do
changeset.resource
# We use the `changeset.action_type` to support soft deletes
# Because a delete is an `update` with an action type of `update`
|> Ash.Resource.Info.validations(changeset.action_type)
|> then(fn validations ->
if changeset.action.delay_global_validations? do
Enum.map(validations, &%{&1 | before_action?: true})
else
validations
end
end)
|> Enum.reduce(changeset, &validate(&2, &1, tracer, metadata, actor))
if changeset.action.skip_global_validations? do
changeset
else
changeset.resource
# We use the `changeset.action_type` to support soft deletes
# Because a delete is an `update` with an action type of `update`
|> Ash.Resource.Info.validations(changeset.action_type)
|> then(fn validations ->
if changeset.action.delay_global_validations? do
Enum.map(validations, &%{&1 | before_action?: true})
else
validations
end
end)
|> Enum.reduce(changeset, &validate(&2, &1, tracer, metadata, actor))
end
end
defp validate(changeset, validation, tracer, metadata, actor) do

View file

@ -11,6 +11,7 @@ defmodule Ash.Resource.Actions.Create do
touches_resources: [],
require_attributes: [],
delay_global_validations?: false,
skip_global_validations?: false,
upsert?: false,
upsert_identity: nil,
arguments: [],
@ -29,6 +30,7 @@ defmodule Ash.Resource.Actions.Create do
manual: module | nil,
upsert?: boolean,
delay_global_validations?: boolean,
skip_global_validations?: boolean,
upsert_identity: atom | nil,
allow_nil_input: list(atom),
touches_resources: list(atom),

View file

@ -12,6 +12,7 @@ defmodule Ash.Resource.Actions.Destroy do
arguments: [],
touches_resources: [],
delay_global_validations?: false,
skip_global_validations?: false,
accept: nil,
changes: [],
reject: [],
@ -27,6 +28,7 @@ defmodule Ash.Resource.Actions.Destroy do
manual: module | nil,
arguments: list(Ash.Resource.Actions.Argument.t()),
delay_global_validations?: boolean,
skip_global_validations?: boolean,
touches_resources: list(atom),
primary?: boolean,
description: String.t()

View file

@ -45,6 +45,13 @@ defmodule Ash.Resource.Actions.SharedOptions do
on the resource.
"""
],
skip_global_validations?: [
type: :boolean,
default: false,
doc: """
If true, global validations will be skipped. Useful for manual actions.
"""
],
reject: [
type: {:or, [in: [:all], list: :atom]},
doc: """

View file

@ -11,6 +11,7 @@ defmodule Ash.Resource.Actions.Update do
manual?: false,
require_attributes: [],
delay_global_validations?: false,
skip_global_validations?: false,
arguments: [],
changes: [],
reject: [],
@ -27,6 +28,7 @@ defmodule Ash.Resource.Actions.Update do
accept: list(atom),
arguments: list(Ash.Resource.Actions.Argument.t()),
delay_global_validations?: boolean,
skip_global_validations?: boolean,
primary?: boolean,
touches_resources: list(atom),
description: String.t()

View file

@ -98,7 +98,7 @@ defmodule Ash.Resource.Builder do
end
@doc """
Builds an action
Builds a relationship
"""
@spec build_relationship(
type :: Ash.Resource.Relationships.type(),
@ -118,6 +118,60 @@ defmodule Ash.Resource.Builder do
end
end
@doc """
Builds and adds a new identity unless an identity with that name already exists
"""
@spec add_new_identity(
Spark.Dsl.Builder.input(),
name :: atom,
fields :: atom | list(atom),
opts :: Keyword.t()
) ::
Spark.Dsl.Builder.result()
defbuilder add_new_identity(dsl_state, name, fields, opts \\ []) do
if Ash.Resource.Info.identity(dsl_state, name) do
dsl_state
else
add_identity(dsl_state, name, fields, opts)
end
end
@doc """
Builds and adds an identity
"""
@spec add_identity(
Spark.Dsl.Builder.input(),
name :: atom,
fields :: atom | list(atom),
opts :: Keyword.t()
) ::
Spark.Dsl.Builder.result()
defbuilder add_identity(dsl_state, name, fields, opts \\ []) do
with {:ok, identity} <- build_identity(name, fields, opts) do
Transformer.add_entity(dsl_state, [:identities], identity)
end
end
@doc """
Builds an action
"""
@spec build_relationship(
name :: atom,
fields :: atom | list(atom),
opts :: Keyword.t()
) ::
{:ok, Ash.Resource.Relationships.relationship()} | {:error, term}
def build_identity(name, fields, opts \\ []) do
with {:ok, opts} <- handle_nested_builders(opts, [:changes, :arguments, :metadata]) do
Transformer.build_entity(
Ash.Resource.Dsl,
[:identities],
:identity,
Keyword.merge(opts, name: name, keys: fields)
)
end
end
@doc """
Builds and adds a change
"""

View file

@ -304,6 +304,40 @@ defmodule Ash.Test.Actions.CreateTest do
end
end
defmodule GlobalValidation do
@moduledoc false
use Ash.Resource,
data_layer: Ash.DataLayer.Ets
ets do
private?(true)
end
actions do
defaults [:create, :read, :update, :destroy]
create :manual do
skip_global_validations?(true)
manual fn changeset, _ ->
Ash.Changeset.apply_attributes(changeset)
end
end
end
validations do
validate compare(:foo, greater_than: 10)
end
attributes do
uuid_primary_key :id
attribute :foo, :integer do
allow_nil? false
end
end
end
defmodule Registry do
@moduledoc false
use Ash.Registry
@ -317,6 +351,7 @@ defmodule Ash.Test.Actions.CreateTest do
entry(PostLink)
entry(Authorized)
entry(GeneratedPkey)
entry(GlobalValidation)
end
end
@ -903,4 +938,16 @@ defmodule Ash.Test.Actions.CreateTest do
assert [] = Api.read!(Authorized)
end
end
describe "global validations" do
test "they can be skipped" do
assert %{errors: [%Ash.Error.Changes.InvalidAttribute{field: :foo}]} =
GlobalValidation
|> Ash.Changeset.for_create(:create, %{foo: 5})
assert %{errors: []} =
GlobalValidation
|> Ash.Changeset.for_create(:manual, %{foo: 5})
end
end
end