improvement: add :* as a valid value in skip_unknown_inputs

improvement: add `skip_unknown_inputs` to individual actions
improvement: add `skip_unknown_inputs` constraint to embedded resources
This commit is contained in:
Zach Daniel 2024-07-23 12:24:41 -04:00
parent 4763181ff9
commit 32c8da1f8d
17 changed files with 75 additions and 33 deletions

View file

@ -187,6 +187,7 @@ spark_locals_without_parens = [
sensitive?: 1,
short_name: 1,
skip_global_validations?: 1,
skip_unknown_inputs: 1,
soft?: 1,
sort: 1,
sortable?: 1,

View file

@ -471,7 +471,7 @@ end
| [`return_stream?`](#reactor-bulk_create-return_stream?){: #reactor-bulk_create-return_stream? } | `boolean` | `false` | If set to `true`, instead of an `Ash.BulkResult`, a mixed stream is returned. |
| [`rollback_on_error?`](#reactor-bulk_create-rollback_on_error?){: #reactor-bulk_create-rollback_on_error? } | `boolean` | `true` | Whether or not to rollback the transaction on error, if the resource is in a transaction. |
| [`select`](#reactor-bulk_create-select){: #reactor-bulk_create-select } | `atom \| list(atom)` | | A select statement to apply to records. Ignored if `return_records?` is not `true`. |
| [`skip_unknown_inputs`](#reactor-bulk_create-skip_unknown_inputs){: #reactor-bulk_create-skip_unknown_inputs } | `atom \| list(atom)` | | A list of inputs that, if provided, will be ignored if they are not recognized by the action. |
| [`skip_unknown_inputs`](#reactor-bulk_create-skip_unknown_inputs){: #reactor-bulk_create-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | | A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys. |
| [`sorted?`](#reactor-bulk_create-sorted?){: #reactor-bulk_create-sorted? } | `boolean` | `false` | Whether or not to sort results by their input position, in cases where `return_records?` is set to `true`. |
| [`stop_on_error?`](#reactor-bulk_create-stop_on_error?){: #reactor-bulk_create-stop_on_error? } | `boolean` | `false` | If `true`, the first encountered error will stop the action and be returned. Otherwise, errors will be skipped. |
| [`success_state`](#reactor-bulk_create-success_state){: #reactor-bulk_create-success_state } | `:success \| :partial_success` | `:success` | Bulk results can be entirely or partially successful. Chooses the `Ash.BulkResult` state to consider the step a success. |
@ -673,7 +673,7 @@ end
| [`reuse_values?`](#reactor-bulk_update-reuse_values?){: #reactor-bulk_update-reuse_values? } | `boolean` | `false` | Whether calculations are allowed to reuse values that have already been loaded, or must refetch them from the data layer. |
| [`rollback_on_error?`](#reactor-bulk_update-rollback_on_error?){: #reactor-bulk_update-rollback_on_error? } | `boolean` | `true` | Whether or not to rollback the transaction on error, if the resource is in a transaction. |
| [`select`](#reactor-bulk_update-select){: #reactor-bulk_update-select } | `atom \| list(atom)` | | A select statement to apply to records. Ignored if `return_records?` is not `true`. |
| [`skip_unknown_inputs`](#reactor-bulk_update-skip_unknown_inputs){: #reactor-bulk_update-skip_unknown_inputs } | `atom \| list(atom)` | | A list of inputs that, if provided, will be ignored if they are not recognized by the action. |
| [`skip_unknown_inputs`](#reactor-bulk_update-skip_unknown_inputs){: #reactor-bulk_update-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | | A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys. |
| [`sorted?`](#reactor-bulk_update-sorted?){: #reactor-bulk_update-sorted? } | `boolean` | `false` | Whether or not to sort results by their input position, in cases where `return_records?` is set to `true`. |
| [`stop_on_error?`](#reactor-bulk_update-stop_on_error?){: #reactor-bulk_update-stop_on_error? } | `boolean` | `false` | If `true`, the first encountered error will stop the action and be returned. Otherwise, errors will be skipped. |
| [`strategy`](#reactor-bulk_update-strategy){: #reactor-bulk_update-strategy } | `list(:atomic \| :atomic_batches \| :stream)` | `[:atomic]` | The strategy or strategies to enable. `:stream` is used in all cases if the data layer does not support atomics. |

View file

@ -1028,6 +1028,7 @@ end
| [`skip_global_validations?`](#actions-create-skip_global_validations?){: #actions-create-skip_global_validations? } | `boolean` | `false` | If true, global validations will be skipped. Useful for manual actions. |
| [`error_handler`](#actions-create-error_handler){: #actions-create-error_handler } | `mfa` | | Sets the error handler on the changeset. See `Ash.Changeset.handle_errors/2` for more |
| [`notifiers`](#actions-create-notifiers){: #actions-create-notifiers } | `list(module)` | | Notifiers that will be called specifically for this action. |
| [`skip_unknown_inputs`](#actions-create-skip_unknown_inputs){: #actions-create-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. |
| [`manual?`](#actions-create-manual?){: #actions-create-manual? } | `boolean` | | Instructs Ash to *skip* the actual update/create/destroy step at the data layer. See the [manual actions guide](/documentation/topics/manual-actions.md) for more. |
@ -1510,6 +1511,7 @@ update :flag_for_review, primary?: true
| [`skip_global_validations?`](#actions-update-skip_global_validations?){: #actions-update-skip_global_validations? } | `boolean` | `false` | If true, global validations will be skipped. Useful for manual actions. |
| [`error_handler`](#actions-update-error_handler){: #actions-update-error_handler } | `mfa` | | Sets the error handler on the changeset. See `Ash.Changeset.handle_errors/2` for more |
| [`notifiers`](#actions-update-notifiers){: #actions-update-notifiers } | `list(module)` | | Notifiers that will be called specifically for this action. |
| [`skip_unknown_inputs`](#actions-update-skip_unknown_inputs){: #actions-update-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. |
| [`manual?`](#actions-update-manual?){: #actions-update-manual? } | `boolean` | | Instructs Ash to *skip* the actual update/create/destroy step at the data layer. See the [manual actions guide](/documentation/topics/manual-actions.md) for more. |
@ -1752,6 +1754,7 @@ end
| [`skip_global_validations?`](#actions-destroy-skip_global_validations?){: #actions-destroy-skip_global_validations? } | `boolean` | `false` | If true, global validations will be skipped. Useful for manual actions. |
| [`error_handler`](#actions-destroy-error_handler){: #actions-destroy-error_handler } | `mfa` | | Sets the error handler on the changeset. See `Ash.Changeset.handle_errors/2` for more |
| [`notifiers`](#actions-destroy-notifiers){: #actions-destroy-notifiers } | `list(module)` | | Notifiers that will be called specifically for this action. |
| [`skip_unknown_inputs`](#actions-destroy-skip_unknown_inputs){: #actions-destroy-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. |
| [`manual?`](#actions-destroy-manual?){: #actions-destroy-manual? } | `boolean` | | Instructs Ash to *skip* the actual update/create/destroy step at the data layer. See the [manual actions guide](/documentation/topics/manual-actions.md) for more. |

View file

@ -98,6 +98,11 @@ defmodule Ash do
""",
default: false
],
skip_unknown_inputs: [
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys."
],
reuse_values?: [
type: :boolean,
default: false,
@ -248,6 +253,11 @@ defmodule Ash do
Metadata to be merged into the metadata field for all notifications sent from this operation.
"""
],
skip_unknown_inputs: [
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys."
],
load: [
type: :any,
doc: "A load statement to add onto the changeset"
@ -402,9 +412,9 @@ defmodule Ash do
"If set to a value greater than 0, up to that many tasks will be started to run batches asynchronously"
],
skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}},
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action."
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys."
]
]
@ -452,11 +462,6 @@ defmodule Ash do
doc:
"The strategy or strategies to enable. :stream is used in all cases if the data layer does not support atomics."
],
skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action."
],
load: [
type: :any,
doc: "A load statement to apply on the resulting records."
@ -516,11 +521,6 @@ defmodule Ash do
type: :any,
doc:
"A filter to apply to records. This is also applied to a stream of inputs."
],
skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action."
]
]
|> Spark.Options.merge(
@ -572,11 +572,6 @@ defmodule Ash do
]},
doc:
"The fields to upsert. If not set, the action's `upsert_fields` is used. Unlike singular `create`, `bulk_create` with `upsert?` requires that `upsert_fields` be specified explicitly in one of these two locations."
],
skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action."
]
]
|> Spark.Options.merge(

View file

@ -61,8 +61,22 @@ defmodule Ash.Actions.Helpers do
defp set_context(%Ash.ActionInput{} = action_input, context),
do: Ash.ActionInput.set_context(action_input, context)
defp set_skip_unknown_opts(opts, %{action: %{skip_unknown_inputs: skip_unknown_inputs}}) do
Keyword.update(
opts,
:skip_unknown_inputs,
skip_unknown_inputs,
&Enum.concat(List.wrap(&1), skip_unknown_inputs)
)
end
defp set_skip_unknown_opts(opts, _query_or_changeset) do
opts
end
def set_context_and_get_opts(domain, query_or_changeset, opts) do
opts = transform_tenant(opts)
opts = set_skip_unknown_opts(opts, query_or_changeset)
query_or_changeset = set_context(query_or_changeset, opts[:context] || %{})
domain =

View file

@ -962,6 +962,9 @@ defmodule Ash.Changeset do
match?("_" <> _, key) ->
{:cont, changeset}
:* in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
key in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
@ -998,6 +1001,9 @@ defmodule Ash.Changeset do
{:halt, {:not_atomic, reason}}
end
:* in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
key in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
@ -1020,6 +1026,9 @@ defmodule Ash.Changeset do
match?("_" <> _, key) ->
{:cont, changeset}
:* in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
key in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
@ -1112,9 +1121,9 @@ defmodule Ash.Changeset do
doc: "set the tenant on the changeset"
],
skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}},
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action."
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys."
],
context: [
type: :map,
@ -2037,6 +2046,9 @@ defmodule Ash.Changeset do
cond do
!Ash.Resource.Info.action_input?(changeset.resource, action.name, name) ->
cond do
:* in List.wrap(opts[:skip_unknown_inputs]) ->
changeset
name in skip_unknown_inputs ->
changeset

View file

@ -364,6 +364,7 @@ defmodule Ash.EmbeddableType do
:destroy_action,
:update_action,
:domain,
:skip_unknown_inputs,
:__source__
])
|> Keyword.put(:on_update,
@ -448,6 +449,8 @@ defmodule Ash.EmbeddableType do
|> Enum.concat(
Enum.flat_map(Ash.Resource.Info.primary_key(__MODULE__), &[&1, to_string(&1)])
)
|> Enum.concat(List.wrap(constraints[:skip_unknown_inputs]))
|> Enum.concat(List.wrap(constraints[:items][:skip_unknown_inputs]))
end
def prepare_change(old_value, "", constraints) do

View file

@ -474,10 +474,6 @@ defmodule Ash.Helpers do
get_domain(resource, opts)
end
def get_domain(%resource{}, opts) do
get_domain(resource, opts)
end
def get_domain(nil, opts) do
cond do
domain = opts[:domain] ->

View file

@ -472,9 +472,9 @@ defmodule Ash.Query do
doc: "set the tenant on the query"
],
skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}},
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action."
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys."
]
]
@ -648,6 +648,9 @@ defmodule Ash.Query do
has_argument?(action, name) ->
set_argument(query, name, value)
:* in List.wrap(opts[:skip_unknown_inputs]) ->
query
name in skip_unknown_inputs ->
query

View file

@ -70,7 +70,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
return_stream?: boolean,
rollback_on_error?: boolean,
select: [atom],
skip_unknown_inputs: [atom],
skip_unknown_inputs: list(atom | String.t()),
sorted?: boolean,
stop_on_error?: boolean,
success_state: :success | :partial_success,
@ -226,9 +226,9 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
required: false
],
skip_unknown_inputs: [
type: {:wrap_list, :atom},
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action.",
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys.",
required: false
],
sorted?: [

View file

@ -88,7 +88,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
reuse_values?: boolean,
rollback_on_error?: boolean,
select: [atom],
skip_unknown_inputs: [atom],
skip_unknown_inputs: list(atom | String.t()),
sorted?: boolean,
stop_on_error?: boolean,
strategy: :atomic | :atomic_batches | :stream,
@ -290,9 +290,9 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
required: false
],
skip_unknown_inputs: [
type: {:wrap_list, :atom},
type: {:wrap_list, {:or, [:atom, :string]}},
doc:
"A list of inputs that, if provided, will be ignored if they are not recognized by the action.",
"A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use `:*` to indicate all unknown keys.",
required: false
],
sorted?: [

View file

@ -8,6 +8,7 @@ defmodule Ash.Resource.Actions.Action do
:run,
constraints: [],
touches_resources: [],
skip_unknown_inputs: [],
arguments: [],
allow_nil?: false,
transaction?: false,
@ -20,6 +21,7 @@ defmodule Ash.Resource.Actions.Action do
name: atom,
description: String.t() | nil,
arguments: [Ash.Resource.Actions.Argument.t()],
skip_unknown_inputs: list(atom() | String.t()),
allow_nil?: boolean,
touches_resources: [Ash.Resource.t()],
constraints: Keyword.t(),

View file

@ -13,6 +13,7 @@ defmodule Ash.Resource.Actions.Create do
touches_resources: [],
delay_global_validations?: false,
skip_global_validations?: false,
skip_unknown_inputs: [],
upsert?: false,
upsert_identity: nil,
upsert_fields: nil,
@ -32,6 +33,7 @@ defmodule Ash.Resource.Actions.Create do
allow_nil_input: list(atom),
manual: module | nil,
upsert?: boolean,
skip_unknown_inputs: list(atom | String.t()),
notifiers: [module()],
delay_global_validations?: boolean,
skip_global_validations?: boolean,

View file

@ -11,6 +11,7 @@ defmodule Ash.Resource.Actions.Destroy do
:error_handler,
manual: nil,
require_atomic?: Application.compile_env(:ash, :require_atomic_by_default?, true),
skip_unknown_inputs: [],
atomic_upgrade?: true,
atomic_upgrade_with: nil,
arguments: [],
@ -35,6 +36,7 @@ defmodule Ash.Resource.Actions.Destroy do
notifiers: list(module),
arguments: list(Ash.Resource.Actions.Argument.t()),
atomic_upgrade?: boolean(),
skip_unknown_inputs: list(atom() | String.t()),
atomic_upgrade_with: nil | atom(),
require_atomic?: boolean,
accept: nil | list(atom),

View file

@ -9,6 +9,7 @@ defmodule Ash.Resource.Actions.Read do
get?: nil,
manual: nil,
metadata: [],
skip_unknown_inputs: [],
modify_query: nil,
multitenancy: nil,
name: nil,
@ -29,6 +30,7 @@ defmodule Ash.Resource.Actions.Read do
filters: [any],
manual: atom | {atom, Keyword.t()} | nil,
metadata: [Ash.Resource.Actions.Metadata.t()],
skip_unknown_inputs: list(atom | String.t()),
modify_query: nil | mfa,
multitenancy: atom,
name: atom,

View file

@ -69,6 +69,11 @@ defmodule Ash.Resource.Actions.SharedOptions do
type: {:list, {:behaviour, Ash.Notifier}},
doc: "Notifiers that will be called specifically for this action."
],
skip_unknown_inputs: [
type: {:wrap_list, {:or, [:atom, :string]}},
default: [],
doc: "A list of unknown fields to skip, or `:*` to skip all unknown fields."
],
manual?: [
type: :boolean,
doc: """

View file

@ -11,6 +11,7 @@ defmodule Ash.Resource.Actions.Update do
accept: nil,
require_attributes: [],
allow_nil_input: [],
skip_unknown_inputs: [],
manual: nil,
manual?: false,
require_atomic?: Application.compile_env(:ash, :require_atomic_by_default?, true),
@ -33,6 +34,7 @@ defmodule Ash.Resource.Actions.Update do
type: :update,
name: atom,
manual: module | nil,
skip_unknown_inputs: list(atom | String.t()),
atomic_upgrade?: boolean(),
atomic_upgrade_with: nil | atom(),
notifiers: list(module),