improvement: ignore unknown string-keyed inputs beginning with _

improvement: support requesting to ignore additional keys
This commit is contained in:
Zach Daniel 2024-03-28 18:20:22 -04:00
parent f8384e44b5
commit 48d8181967
5 changed files with 122 additions and 83 deletions

View file

@ -1926,6 +1926,8 @@ defmodule Ash do
{:ok, resource} <- Ash.Domain.Info.resource(domain, changeset.resource),
{:ok, action} <- Ash.Helpers.get_action(resource, opts, :create, changeset.action) do
Ash.Actions.Create.run(domain, changeset, action, opts)
else
{:error, error} -> {:error, Ash.Error.to_error_class(error)}
end
end

View file

@ -184,18 +184,23 @@ defmodule Ash.ActionInput do
}
Enum.reduce(params, input, fn {name, value}, input ->
if has_argument?(input.action, name) do
set_argument(input, name, value)
else
error =
Ash.Error.Invalid.NoSuchInput.exception(
resource: input.resource,
action: input.action.name,
input: name,
inputs: Enum.map(input.action.arguments, & &1.name)
)
cond do
has_argument?(input.action, name) ->
set_argument(input, name, value)
add_error(input, Ash.Error.set_path(error, name))
match?("_" <> _, name) ->
input
true ->
error =
Ash.Error.Invalid.NoSuchInput.exception(
resource: input.resource,
action: input.action.name,
input: name,
inputs: Enum.map(input.action.arguments, & &1.name)
)
add_error(input, Ash.Error.set_path(error, name))
end
end)
end

View file

@ -28,7 +28,7 @@ defmodule Ash.Actions.Read do
def run(query, action, opts) do
query = Ash.Query.new(query)
domain = query.domain || opts[:domain]
domain = query.domain || opts[:domain] || Ash.Resource.Info.domain(query.resource)
if !domain do
raise Ash.Error.Framework.AssumptionFailed, message: "got a query without a domain"

View file

@ -332,18 +332,15 @@ defmodule Ash.Changeset do
context = Ash.Resource.Info.default_context(resource) || %{}
domain = Ash.Resource.Info.domain(resource)
if Ash.Resource.Info.resource?(resource) do
%__MODULE__{resource: resource, data: record, action_type: action_type, domain: domain}
%__MODULE__{resource: resource, data: record, action_type: action_type}
|> set_context(context)
|> set_tenant(tenant)
else
%__MODULE__{
resource: resource,
action_type: action_type,
data: struct(resource),
domain: domain
data: struct(resource)
}
|> add_error(NoSuchResource.exception(resource: resource))
|> set_tenant(tenant)
@ -858,6 +855,9 @@ defmodule Ash.Changeset do
{:halt, {:not_atomic, reason}}
end
match?("_" <> _, key) ->
{:cont, changeset}
key in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
@ -881,21 +881,26 @@ defmodule Ash.Changeset do
{:cont, set_argument(changeset, key, value)}
attribute = Ash.Resource.Info.attribute(changeset.resource, key) ->
if attribute.name in action.accept do
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, atomic} ->
{:cont, atomic_update(changeset, attribute.name, {:atomic, atomic})}
cond do
attribute.name in action.accept ->
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, atomic} ->
{:cont, atomic_update(changeset, attribute.name, {:atomic, atomic})}
{:error, error} ->
{:cont, add_invalid_errors(value, :attribute, changeset, attribute, error)}
{:error, error} ->
{:cont, add_invalid_errors(value, :attribute, changeset, attribute, error)}
{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
end
else
if key in List.wrap(opts[:skip_unknown_inputs]) do
{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
end
key in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
else
match?("_" <> _, key) ->
{:cont, changeset}
true ->
{:cont,
add_error(
changeset,
@ -906,9 +911,11 @@ defmodule Ash.Changeset do
inputs: Ash.Resource.Info.action_inputs(changeset.resource, action.name)
)
)}
end
end
match?("_" <> _, key) ->
{:cont, changeset}
key in List.wrap(opts[:skip_unknown_inputs]) ->
{:cont, changeset}
@ -1793,30 +1800,14 @@ defmodule Ash.Changeset do
Enum.reduce(params, changeset, fn {name, value}, changeset ->
cond do
!Ash.Resource.Info.action_input?(changeset.resource, action.name, name) ->
if name in skip_unknown_inputs do
changeset
else
add_error(
changeset,
NoSuchInput.exception(
resource: changeset.resource,
action: action.name,
input: name,
inputs: Ash.Resource.Info.action_inputs(changeset.resource, action.name)
)
)
end
argument = get_action_argument(action, name) ->
do_set_argument(changeset, argument.name, value, true)
attr = Ash.Resource.Info.public_attribute(changeset.resource, name) ->
if attr.writable? do
do_change_attribute(changeset, attr.name, value, true)
else
if name in skip_unknown_inputs do
cond do
name in skip_unknown_inputs ->
changeset
else
match?("_" <> _, name) ->
changeset
true ->
add_error(
changeset,
NoSuchInput.exception(
@ -1826,6 +1817,32 @@ defmodule Ash.Changeset do
inputs: Ash.Resource.Info.action_inputs(changeset.resource, action.name)
)
)
end
argument = get_action_argument(action, name) ->
do_set_argument(changeset, argument.name, value, true)
attr = Ash.Resource.Info.public_attribute(changeset.resource, name) ->
if attr.writable? do
do_change_attribute(changeset, attr.name, value, true)
else
cond do
name in skip_unknown_inputs ->
changeset
match?("_" <> _, name) ->
changeset
true ->
add_error(
changeset,
NoSuchInput.exception(
resource: changeset.resource,
action: action.name,
input: name,
inputs: Ash.Resource.Info.action_inputs(changeset.resource, action.name)
)
)
end
end
@ -4321,21 +4338,21 @@ defmodule Ash.Changeset do
Ash.Type.include_source(attribute.type, changeset, attribute.constraints),
{{:ok, prepared}, _} <-
{prepare_change(changeset, attribute, value, constraints), value},
{:ok, casted} <-
Ash.Type.cast_input(
attribute.type,
prepared,
constraints
),
{:ok, casted} <-
handle_change(
changeset,
attribute,
casted,
constraints
),
{:ok, casted} <-
Ash.Type.apply_constraints(attribute.type, casted, constraints) do
{{:ok, casted}, _} <-
{Ash.Type.cast_input(
attribute.type,
prepared,
constraints
), prepared},
{{:ok, casted}, _} <-
{handle_change(
changeset,
attribute,
casted,
constraints
), casted},
{{:ok, casted}, _} <-
{Ash.Type.apply_constraints(attribute.type, casted, constraints), casted} do
data_value = Map.get(changeset.data, attribute.name)
changeset = remove_default(changeset, attribute.name)

View file

@ -381,7 +381,7 @@ defmodule Ash.Query do
def new(resource, opts) when is_atom(resource) do
query = %__MODULE__{
domain: opts[:domain] || Ash.Resource.Info.domain(resource),
domain: opts[:domain],
filter: nil,
resource: resource
}
@ -438,6 +438,11 @@ defmodule Ash.Query do
tenant: [
type: {:protocol, Ash.ToTenant},
doc: "set the tenant on the query"
],
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."
]
]
@ -508,7 +513,7 @@ defmodule Ash.Query do
|> set_authorize?(opts)
|> set_tracer(opts)
|> set_tenant(opts[:tenant] || query.tenant)
|> cast_params(action, args)
|> cast_params(action, args, opts)
|> set_argument_defaults(action)
|> require_arguments(action)
|> run_preparations(action, opts[:actor], opts[:authorize?], opts[:tracer], metadata)
@ -603,20 +608,30 @@ defmodule Ash.Query do
end)
end
defp cast_params(query, action, args) do
Enum.reduce(args, query, fn {name, value}, query ->
if has_argument?(action, name) do
set_argument(query, name, value)
else
error =
Ash.Error.Invalid.NoSuchInput.exception(
resource: query.resource,
action: query.action.name,
input: name,
inputs: Enum.map(query.action.arguments, & &1.name)
)
defp cast_params(query, action, args, opts) do
skip_unknown_inputs = opts[:skip_unknown_inputs] || []
add_error(query, Ash.Error.set_path(error, name))
Enum.reduce(args, query, fn {name, value}, query ->
cond do
has_argument?(action, name) ->
set_argument(query, name, value)
name in skip_unknown_inputs ->
query
match?("_" <> _, name) ->
query
true ->
error =
Ash.Error.Invalid.NoSuchInput.exception(
resource: query.resource,
action: query.action.name,
input: name,
inputs: Enum.map(query.action.arguments, & &1.name)
)
add_error(query, Ash.Error.set_path(error, name))
end
end)
end