diff --git a/.formatter.exs b/.formatter.exs index 6cfbca16..0b3a4c4e 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -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, diff --git a/documentation/dsls/DSL:-Ash.Reactor.md b/documentation/dsls/DSL:-Ash.Reactor.md index 7f733124..aa62bd04 100644 --- a/documentation/dsls/DSL:-Ash.Reactor.md +++ b/documentation/dsls/DSL:-Ash.Reactor.md @@ -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. | diff --git a/documentation/dsls/DSL:-Ash.Resource.md b/documentation/dsls/DSL:-Ash.Resource.md index ea0eadbd..77bb0706 100644 --- a/documentation/dsls/DSL:-Ash.Resource.md +++ b/documentation/dsls/DSL:-Ash.Resource.md @@ -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. | diff --git a/lib/ash.ex b/lib/ash.ex index 23634c03..e8713c48 100644 --- a/lib/ash.ex +++ b/lib/ash.ex @@ -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( diff --git a/lib/ash/actions/helpers.ex b/lib/ash/actions/helpers.ex index 9834925b..0a53db59 100644 --- a/lib/ash/actions/helpers.ex +++ b/lib/ash/actions/helpers.ex @@ -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 = diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 82e26682..9cc1be53 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -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 diff --git a/lib/ash/embeddable_type.ex b/lib/ash/embeddable_type.ex index d9324696..2b5c5b2c 100644 --- a/lib/ash/embeddable_type.ex +++ b/lib/ash/embeddable_type.ex @@ -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 diff --git a/lib/ash/helpers.ex b/lib/ash/helpers.ex index 33c9ee6b..f9d9d5dc 100644 --- a/lib/ash/helpers.ex +++ b/lib/ash/helpers.ex @@ -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] -> diff --git a/lib/ash/query/query.ex b/lib/ash/query/query.ex index dff93ab2..a791e126 100644 --- a/lib/ash/query/query.ex +++ b/lib/ash/query/query.ex @@ -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 diff --git a/lib/ash/reactor/dsl/bulk_create.ex b/lib/ash/reactor/dsl/bulk_create.ex index ae37f368..e68cb930 100644 --- a/lib/ash/reactor/dsl/bulk_create.ex +++ b/lib/ash/reactor/dsl/bulk_create.ex @@ -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?: [ diff --git a/lib/ash/reactor/dsl/bulk_update.ex b/lib/ash/reactor/dsl/bulk_update.ex index 844fb5bd..cd62ba6b 100644 --- a/lib/ash/reactor/dsl/bulk_update.ex +++ b/lib/ash/reactor/dsl/bulk_update.ex @@ -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?: [ diff --git a/lib/ash/resource/actions/action/action.ex b/lib/ash/resource/actions/action/action.ex index db6639fc..14fe7571 100644 --- a/lib/ash/resource/actions/action/action.ex +++ b/lib/ash/resource/actions/action/action.ex @@ -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(), diff --git a/lib/ash/resource/actions/create.ex b/lib/ash/resource/actions/create.ex index 66e232be..e0d7a2c8 100644 --- a/lib/ash/resource/actions/create.ex +++ b/lib/ash/resource/actions/create.ex @@ -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, diff --git a/lib/ash/resource/actions/destroy.ex b/lib/ash/resource/actions/destroy.ex index 4055ce2f..27e0694d 100644 --- a/lib/ash/resource/actions/destroy.ex +++ b/lib/ash/resource/actions/destroy.ex @@ -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), diff --git a/lib/ash/resource/actions/read.ex b/lib/ash/resource/actions/read.ex index 91162242..873e3fa9 100644 --- a/lib/ash/resource/actions/read.ex +++ b/lib/ash/resource/actions/read.ex @@ -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, diff --git a/lib/ash/resource/actions/shared_options.ex b/lib/ash/resource/actions/shared_options.ex index 3eca0657..b543b3c1 100644 --- a/lib/ash/resource/actions/shared_options.ex +++ b/lib/ash/resource/actions/shared_options.ex @@ -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: """ diff --git a/lib/ash/resource/actions/update.ex b/lib/ash/resource/actions/update.ex index 91964937..c0d04b73 100644 --- a/lib/ash/resource/actions/update.ex +++ b/lib/ash/resource/actions/update.ex @@ -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),