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, sensitive?: 1,
short_name: 1, short_name: 1,
simple_notifiers: 1, simple_notifiers: 1,
skip_global_validations?: 1,
soft?: 1, soft?: 1,
sort: 1, sort: 1,
source: 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 - Require any accepted attributes that are `allow_nil?` false
- Set any default values for attributes - Set any default values for attributes
- Run action changes & validations - 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 #### Running the Create/Update/Destroy Action

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@ defmodule Ash.Resource.Builder do
end end
@doc """ @doc """
Builds an action Builds a relationship
""" """
@spec build_relationship( @spec build_relationship(
type :: Ash.Resource.Relationships.type(), type :: Ash.Resource.Relationships.type(),
@ -118,6 +118,60 @@ defmodule Ash.Resource.Builder do
end end
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 """ @doc """
Builds and adds a change Builds and adds a change
""" """

View file

@ -304,6 +304,40 @@ defmodule Ash.Test.Actions.CreateTest do
end end
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 defmodule Registry do
@moduledoc false @moduledoc false
use Ash.Registry use Ash.Registry
@ -317,6 +351,7 @@ defmodule Ash.Test.Actions.CreateTest do
entry(PostLink) entry(PostLink)
entry(Authorized) entry(Authorized)
entry(GeneratedPkey) entry(GeneratedPkey)
entry(GlobalValidation)
end end
end end
@ -903,4 +938,16 @@ defmodule Ash.Test.Actions.CreateTest do
assert [] = Api.read!(Authorized) assert [] = Api.read!(Authorized)
end end
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 end