improvement: support require_reference?: false on code interfaces

improvement: support `:filter` option on bulk create/destroy
This commit is contained in:
Zach Daniel 2024-04-13 17:20:59 -04:00
parent 9c74e52bd8
commit bc69f904e2
12 changed files with 278 additions and 99 deletions

View file

@ -170,6 +170,7 @@ spark_locals_without_parens = [
require_atomic?: 1, require_atomic?: 1,
require_attributes: 1, require_attributes: 1,
require_primary_key?: 1, require_primary_key?: 1,
require_reference?: 1,
required?: 1, required?: 1,
resource: 1, resource: 1,
resource: 2, resource: 2,

View file

@ -122,9 +122,10 @@ define :get_user_by_id, User, action: :get_by_id, args: [:id], get?: true
| [`action`](#resources-resource-define-action){: #resources-resource-define-action } | `atom` | | The name of the action that will be called. Defaults to the same name as the function. | | [`action`](#resources-resource-define-action){: #resources-resource-define-action } | `atom` | | The name of the action that will be called. Defaults to the same name as the function. |
| [`args`](#resources-resource-define-args){: #resources-resource-define-args } | `list(atom \| {:optional, atom})` | | Map specific arguments to named inputs. Can provide any argument/attributes that the action allows. | | [`args`](#resources-resource-define-args){: #resources-resource-define-args } | `list(atom \| {:optional, atom})` | | Map specific arguments to named inputs. Can provide any argument/attributes that the action allows. |
| [`not_found_error?`](#resources-resource-define-not_found_error?){: #resources-resource-define-not_found_error? } | `boolean` | `true` | If the action or interface is configured with `get?: true`, this determines whether or not an error is raised or `nil` is returned. | | [`not_found_error?`](#resources-resource-define-not_found_error?){: #resources-resource-define-not_found_error? } | `boolean` | `true` | If the action or interface is configured with `get?: true`, this determines whether or not an error is raised or `nil` is returned. |
| [`get?`](#resources-resource-define-get?){: #resources-resource-define-get? } | `boolean` | | Expects to only receive a single result from a read action, and returns a single result instead of a list. Ignored for other action types. | | [`require_reference?`](#resources-resource-define-require_reference?){: #resources-resource-define-require_reference? } | `boolean` | `true` | For update and destroy actions, require a resource or identifier to be passed in as the first argument. Not relevant for other action types. |
| [`get_by`](#resources-resource-define-get_by){: #resources-resource-define-get_by } | `atom \| list(atom)` | | Takes a list of fields and adds those fields as arguments, which will then be used to filter. Sets `get?` to true automatically. Ignored for non-read actions. | | [`get?`](#resources-resource-define-get?){: #resources-resource-define-get? } | `boolean` | | Expects to only receive a single result from a read action or a bulk update/destroy, and returns a single result instead of a list. Sets `require_reference?` to false automatically. |
| [`get_by_identity`](#resources-resource-define-get_by_identity){: #resources-resource-define-get_by_identity } | `atom` | | Only relevant for read actions. Takes an identity, and gets its field list, performing the same logic as `get_by` once it has the list of fields. | | [`get_by`](#resources-resource-define-get_by){: #resources-resource-define-get_by } | `atom \| list(atom)` | | Takes a list of fields and adds those fields as arguments, which will then be used to filter. Sets `get?` to true and `require_reference?` to false automatically. Adds filters for read, update and destroy actions, replacing the `record` first argument. |
| [`get_by_identity`](#resources-resource-define-get_by_identity){: #resources-resource-define-get_by_identity } | `atom` | | Takes an identity, gets its field list, and performs the same logic as `get_by` with those fields. Adds filters for read, update and destroy actions, replacing the `record` first argument. |

View file

@ -2032,9 +2032,10 @@ define :get_user_by_id, action: :get_by_id, args: [:id], get?: true
| [`action`](#code_interface-define-action){: #code_interface-define-action } | `atom` | | The name of the action that will be called. Defaults to the same name as the function. | | [`action`](#code_interface-define-action){: #code_interface-define-action } | `atom` | | The name of the action that will be called. Defaults to the same name as the function. |
| [`args`](#code_interface-define-args){: #code_interface-define-args } | `list(atom \| {:optional, atom})` | | Map specific arguments to named inputs. Can provide any argument/attributes that the action allows. | | [`args`](#code_interface-define-args){: #code_interface-define-args } | `list(atom \| {:optional, atom})` | | Map specific arguments to named inputs. Can provide any argument/attributes that the action allows. |
| [`not_found_error?`](#code_interface-define-not_found_error?){: #code_interface-define-not_found_error? } | `boolean` | `true` | If the action or interface is configured with `get?: true`, this determines whether or not an error is raised or `nil` is returned. | | [`not_found_error?`](#code_interface-define-not_found_error?){: #code_interface-define-not_found_error? } | `boolean` | `true` | If the action or interface is configured with `get?: true`, this determines whether or not an error is raised or `nil` is returned. |
| [`get?`](#code_interface-define-get?){: #code_interface-define-get? } | `boolean` | | Expects to only receive a single result from a read action, and returns a single result instead of a list. Ignored for other action types. | | [`require_reference?`](#code_interface-define-require_reference?){: #code_interface-define-require_reference? } | `boolean` | `true` | For update and destroy actions, require a resource or identifier to be passed in as the first argument. Not relevant for other action types. |
| [`get_by`](#code_interface-define-get_by){: #code_interface-define-get_by } | `atom \| list(atom)` | | Takes a list of fields and adds those fields as arguments, which will then be used to filter. Sets `get?` to true automatically. Ignored for non-read actions. | | [`get?`](#code_interface-define-get?){: #code_interface-define-get? } | `boolean` | | Expects to only receive a single result from a read action or a bulk update/destroy, and returns a single result instead of a list. Sets `require_reference?` to false automatically. |
| [`get_by_identity`](#code_interface-define-get_by_identity){: #code_interface-define-get_by_identity } | `atom` | | Only relevant for read actions. Takes an identity, and gets its field list, performing the same logic as `get_by` once it has the list of fields. | | [`get_by`](#code_interface-define-get_by){: #code_interface-define-get_by } | `atom \| list(atom)` | | Takes a list of fields and adds those fields as arguments, which will then be used to filter. Sets `get?` to true and `require_reference?` to false automatically. Adds filters for read, update and destroy actions, replacing the `record` first argument. |
| [`get_by_identity`](#code_interface-define-get_by_identity){: #code_interface-define-get_by_identity } | `atom` | | Takes an identity, gets its field list, and performs the same logic as `get_by` with those fields. Adds filters for read, update and destroy actions, replacing the `record` first argument. |

View file

@ -462,6 +462,11 @@ defmodule Ash do
doc: doc:
"A select statement to apply to records. Ignored if `return_records?` is not true." "A select statement to apply to records. Ignored if `return_records?` is not true."
], ],
filter: [
type: :any,
doc:
"A filter to apply to records. This is also applied to a stream of inputs."
],
strategy: [ strategy: [
type: {:wrap_list, {:one_of, [:atomic, :atomic_batches, :stream]}}, type: {:wrap_list, {:one_of, [:atomic, :atomic_batches, :stream]}},
default: [:atomic], default: [:atomic],
@ -524,6 +529,11 @@ defmodule Ash do
doc: doc:
"The strategy or strategies to enable. :stream is used in all cases if the data layer does not support atomics." "The strategy or strategies to enable. :stream is used in all cases if the data layer does not support atomics."
], ],
filter: [
type: :any,
doc:
"A filter to apply to records. This is also applied to a stream of inputs."
],
skip_unknown_inputs: [ skip_unknown_inputs: [
type: {:list, {:or, [:atom, :string]}}, type: {:list, {:or, [:atom, :string]}},
doc: doc:

View file

@ -120,6 +120,9 @@ defmodule Ash.Actions.Destroy.Bulk do
|> Keyword.put(:domain, domain) |> Keyword.put(:domain, domain)
|> Keyword.take(Ash.stream_opt_keys()) |> Keyword.take(Ash.stream_opt_keys())
query =
Ash.Query.do_filter(query, opts[:filter])
run( run(
domain, domain,
Ash.stream!( Ash.stream!(
@ -661,6 +664,7 @@ defmodule Ash.Actions.Destroy.Bulk do
tracer: opts[:tracer], tracer: opts[:tracer],
atomic_changeset: atomic_changeset, atomic_changeset: atomic_changeset,
return_errors?: opts[:return_errors?], return_errors?: opts[:return_errors?],
filter: opts[:filter],
return_notifications?: opts[:return_notifications?], return_notifications?: opts[:return_notifications?],
notify?: opts[:notify?], notify?: opts[:notify?],
return_records?: opts[:return_records?], return_records?: opts[:return_records?],
@ -833,6 +837,7 @@ defmodule Ash.Actions.Destroy.Bulk do
resource resource
|> Ash.Changeset.new() |> Ash.Changeset.new()
|> Ash.Changeset.filter(opts[:filter])
|> Map.put(:domain, domain) |> Map.put(:domain, domain)
|> Ash.Actions.Helpers.add_context(opts) |> Ash.Actions.Helpers.add_context(opts)
|> Ash.Changeset.set_context(opts[:context] || %{}) |> Ash.Changeset.set_context(opts[:context] || %{})

View file

@ -85,6 +85,9 @@ defmodule Ash.Actions.Update.Bulk do
} }
_ -> _ ->
query =
Ash.Query.do_filter(query, opts[:filter])
run( run(
domain, domain,
Ash.stream!( Ash.stream!(
@ -109,6 +112,9 @@ defmodule Ash.Actions.Update.Bulk do
} }
atomic_changeset -> atomic_changeset ->
atomic_changeset =
Ash.Changeset.filter(atomic_changeset, opts[:filter])
{atomic_changeset, opts} = {atomic_changeset, opts} =
Ash.Actions.Helpers.set_context_and_get_opts(domain, atomic_changeset, opts) Ash.Actions.Helpers.set_context_and_get_opts(domain, atomic_changeset, opts)
@ -655,6 +661,7 @@ defmodule Ash.Actions.Update.Bulk do
tracer: opts[:tracer], tracer: opts[:tracer],
atomic_changeset: atomic_changeset, atomic_changeset: atomic_changeset,
return_errors?: opts[:return_errors?], return_errors?: opts[:return_errors?],
filter: opts[:filter],
return_notifications?: opts[:return_notifications?], return_notifications?: opts[:return_notifications?],
notify?: opts[:notify?], notify?: opts[:notify?],
return_records?: opts[:return_records?], return_records?: opts[:return_records?],
@ -899,6 +906,7 @@ defmodule Ash.Actions.Update.Bulk do
resource resource
|> Ash.Changeset.new() |> Ash.Changeset.new()
|> Map.put(:domain, domain) |> Map.put(:domain, domain)
|> Ash.Changeset.filter(opts[:filter])
|> Ash.Actions.Helpers.add_context(opts) |> Ash.Actions.Helpers.add_context(opts)
|> Ash.Changeset.set_context(opts[:context] || %{}) |> Ash.Changeset.set_context(opts[:context] || %{})
|> Ash.Changeset.prepare_changeset_for_action(action, opts) |> Ash.Changeset.prepare_changeset_for_action(action, opts)

View file

@ -86,6 +86,7 @@ defmodule Ash.Actions.Update do
|> Ash.Changeset.set_context(%{data_layer: %{use_atomic_update_data?: true}}) |> Ash.Changeset.set_context(%{data_layer: %{use_atomic_update_data?: true}})
|> Map.put(:load, changeset.load) |> Map.put(:load, changeset.load)
|> Map.put(:select, changeset.select) |> Map.put(:select, changeset.select)
|> Map.put(:filter, changeset.filter)
|> Ash.Changeset.set_context(changeset.context) |> Ash.Changeset.set_context(changeset.context)
{atomic_changeset, opts} = {atomic_changeset, opts} =

View file

@ -4899,6 +4899,10 @@ defmodule Ash.Changeset do
Used by optimistic locking. See `Ash.Resource.Change.Builtins.optimistic_lock/1` for more. Used by optimistic locking. See `Ash.Resource.Change.Builtins.optimistic_lock/1` for more.
""" """
@spec filter(t(), Ash.Expr.t()) :: t() @spec filter(t(), Ash.Expr.t()) :: t()
def filter(changeset, expr) when expr in [nil, %{}, []] do
changeset
end
def filter(changeset, expr) do def filter(changeset, expr) do
if Ash.DataLayer.data_layer_can?(changeset.resource, :changeset_filter) do if Ash.DataLayer.data_layer_can?(changeset.resource, :changeset_filter) do
%{ %{

View file

@ -411,7 +411,7 @@ defmodule Ash.CodeInterface do
filter_keys = filter_keys =
cond do cond do
action.type != :read -> action.type not in [:read, :update, :destroy] ->
[] []
interface.get_by_identity -> interface.get_by_identity ->
@ -736,9 +736,16 @@ defmodule Ash.CodeInterface do
:update -> :update ->
subject = quote do: changeset subject = quote do: changeset
subject_args = quote do: [record]
subject_args =
if interface.require_reference? do
quote do: [record]
else
[]
end
resolve_subject = resolve_subject =
if Enum.empty?(filter_keys) do
quote do quote do
{changeset_opts, opts} = {changeset_opts, opts} =
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context]) Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
@ -749,8 +756,11 @@ defmodule Ash.CodeInterface do
record record
|> case do |> case do
%Ash.Changeset{resource: unquote(resource)} -> %Ash.Changeset{resource: unquote(resource)} ->
Ash.Changeset.for_update( {filters, params} = Map.split(params, unquote(filter_keys))
record,
record
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_update(
unquote(action.name), unquote(action.name),
params, params,
changeset_opts changeset_opts
@ -765,8 +775,12 @@ defmodule Ash.CodeInterface do
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}." "Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
%struct{} = record when struct == unquote(resource) -> %struct{} = record when struct == unquote(resource) ->
Ash.Changeset.for_update( {filters, params} = Map.split(params, unquote(filter_keys))
record,
record
|> Ash.Changeset.new()
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_update(
unquote(action.name), unquote(action.name),
params, params,
changeset_opts changeset_opts
@ -791,9 +805,24 @@ defmodule Ash.CodeInterface do
{:atomic, :id, other} {:atomic, :id, other}
end end
end end
else
quote do
filters = Map.take(params, unquote(filter_keys))
{changeset_opts, opts} =
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
{:atomic, :query, Ash.Query.do_filter(unquote(resource), filters)}
end
end
act = act =
quote do quote do
{filters, params} = Map.split(params, unquote(filter_keys))
case changeset do case changeset do
{:atomic, method, id} -> {:atomic, method, id} ->
bulk_opts = bulk_opts =
@ -811,12 +840,20 @@ defmodule Ash.CodeInterface do
end end
end) end)
bulk_opts =
if method == :stream do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} -> {:ok, query} ->
query query
|> Ash.bulk_update(unquote(action.name), params, bulk_opts) |> Ash.bulk_update(unquote(action.name), params, bulk_opts)
|> case do |> case do
%Ash.BulkResult{} = result when method == :stream -> %Ash.BulkResult{} = result
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
result result
%Ash.BulkResult{status: :success, records: [record]} = result -> %Ash.BulkResult{status: :success, records: [record]} = result ->
@ -848,6 +885,8 @@ defmodule Ash.CodeInterface do
quote do quote do
case changeset do case changeset do
{:atomic, method, id} -> {:atomic, method, id} ->
{filters, params} = Map.split(params, unquote(filter_keys))
bulk_opts = bulk_opts =
opts opts
|> Keyword.delete(:bulk_options) |> Keyword.delete(:bulk_options)
@ -863,12 +902,20 @@ defmodule Ash.CodeInterface do
end end
end) end)
bulk_opts =
if method == :stream do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} -> {:ok, query} ->
query query
|> Ash.bulk_update!(unquote(action.name), params, bulk_opts) |> Ash.bulk_update!(unquote(action.name), params, bulk_opts)
|> case do |> case do
%Ash.BulkResult{} = result when method == :stream -> %Ash.BulkResult{} = result
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
result result
%Ash.BulkResult{status: :success, records: [record]} = result -> %Ash.BulkResult{status: :success, records: [record]} = result ->
@ -896,9 +943,16 @@ defmodule Ash.CodeInterface do
:destroy -> :destroy ->
subject = quote do: changeset subject = quote do: changeset
subject_args = quote do: [record]
subject_args =
if interface.require_reference? do
quote do: [record]
else
[]
end
resolve_subject = resolve_subject =
if interface.require_reference? do
quote do quote do
{changeset_opts, opts} = {changeset_opts, opts} =
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context]) Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
@ -909,8 +963,11 @@ defmodule Ash.CodeInterface do
record record
|> case do |> case do
%Ash.Changeset{resource: unquote(resource)} -> %Ash.Changeset{resource: unquote(resource)} ->
Ash.Changeset.for_destroy( {filters, params} = Map.split(params, unquote(filter_keys))
record,
record
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_destroy(
unquote(action.name), unquote(action.name),
params, params,
changeset_opts changeset_opts
@ -925,8 +982,12 @@ defmodule Ash.CodeInterface do
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}." "Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
%struct{} = record when struct == unquote(resource) -> %struct{} = record when struct == unquote(resource) ->
Ash.Changeset.for_destroy( {filters, params} = Map.split(params, unquote(filter_keys))
record,
record
|> Ash.Changeset.new()
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_destroy(
unquote(action.name), unquote(action.name),
params, params,
changeset_opts changeset_opts
@ -951,11 +1012,26 @@ defmodule Ash.CodeInterface do
{:atomic, :id, other} {:atomic, :id, other}
end end
end end
else
quote do
filters = Map.take(params, unquote(filter_keys))
{changeset_opts, opts} =
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
{:atomic, :query, Ash.Query.do_filter(unquote(resource), filters)}
end
end
act = act =
quote do quote do
case changeset do case changeset do
{:atomic, method, id} -> {:atomic, method, id} ->
{filters, params} = Map.split(params, unquote(filter_keys))
bulk_opts = bulk_opts =
opts opts
|> Keyword.drop([:bulk_options, :return_destroyed?]) |> Keyword.drop([:bulk_options, :return_destroyed?])
@ -971,12 +1047,20 @@ defmodule Ash.CodeInterface do
end end
end) end)
bulk_opts =
if method == :stream do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} -> {:ok, query} ->
query query
|> Ash.bulk_destroy(unquote(action.name), params, bulk_opts) |> Ash.bulk_destroy(unquote(action.name), params, bulk_opts)
|> case do |> case do
%Ash.BulkResult{} = result when method == :stream -> %Ash.BulkResult{} = result
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
result result
%Ash.BulkResult{status: :success, records: [record]} = result -> %Ash.BulkResult{status: :success, records: [record]} = result ->
@ -1013,6 +1097,8 @@ defmodule Ash.CodeInterface do
quote do quote do
case changeset do case changeset do
{:atomic, method, id} -> {:atomic, method, id} ->
{filters, params} = Map.split(params, unquote(filter_keys))
bulk_opts = bulk_opts =
opts opts
|> Keyword.drop([:bulk_options, :return_destroyed?]) |> Keyword.drop([:bulk_options, :return_destroyed?])
@ -1028,12 +1114,20 @@ defmodule Ash.CodeInterface do
end end
end) end)
bulk_opts =
if method == :stream do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} -> {:ok, query} ->
query query
|> Ash.bulk_destroy!(unquote(action.name), params, bulk_opts) |> Ash.bulk_destroy!(unquote(action.name), params, bulk_opts)
|> case do |> case do
%Ash.BulkResult{} = result when method == :stream -> %Ash.BulkResult{} = result
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
result result
%Ash.BulkResult{status: :success, records: [record]} = result -> %Ash.BulkResult{status: :success, records: [record]} = result ->

View file

@ -62,6 +62,16 @@ defmodule Ash.Resource.Change.Builtins do
@relate_actor_opts @relate_actor_opts
end end
@doc """
Applies a filter to the changeset. Has no effect for create actions.
This ensures that only things matching the provided filter are updated or destroyed.
"""
@spec filter(expr :: Ash.Expr.t()) :: Ash.Resource.Change.ref()
def filter(filter) do
{Ash.Resource.Change.Filter, filter: filter}
end
@spec relate_actor(relationship :: atom, opts :: Keyword.t()) :: Ash.Resource.Change.ref() @spec relate_actor(relationship :: atom, opts :: Keyword.t()) :: Ash.Resource.Change.ref()
def relate_actor(relationship, opts \\ []) do def relate_actor(relationship, opts \\ []) do
opts = opts =

View file

@ -0,0 +1,14 @@
defmodule Ash.Resource.Change.Filter do
@moduledoc false
use Ash.Resource.Change
@impl true
def change(changeset, opts, _) do
Ash.Changeset.filter(changeset, opts[:filter])
end
@impl true
def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end
end

View file

@ -2,16 +2,40 @@ defmodule Ash.Resource.Interface do
@moduledoc """ @moduledoc """
Represents a function in a resource's code interface Represents a function in a resource's code interface
""" """
defstruct [:name, :action, :args, :get?, :get_by, :get_by_identity, :not_found_error?] defstruct [
:name,
:action,
:args,
:get?,
:get_by,
:get_by_identity,
:not_found_error?,
require_reference?: true
]
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
def transform(definition) do def transform(definition) do
if definition.get_by || definition.get_by_identity do {:ok,
{:ok, %{definition | get?: true}} definition
else |> set_get?()
{:ok, definition} |> set_require_reference?()}
end end
defp set_get?(definition) do
if definition.get_by || definition.get_by_identity do
%{definition | get?: true}
else
definition
end
end
defp set_require_reference?(%{get?: true} = definition) do
%{definition | require_reference?: false}
end
defp set_require_reference?(definition) do
definition
end end
def interface_options(:calculate) do def interface_options(:calculate) do
@ -125,22 +149,28 @@ defmodule Ash.Resource.Interface do
doc: doc:
"If the action or interface is configured with `get?: true`, this determines whether or not an error is raised or `nil` is returned." "If the action or interface is configured with `get?: true`, this determines whether or not an error is raised or `nil` is returned."
], ],
require_reference?: [
type: :boolean,
default: true,
doc:
"For update and destroy actions, require a resource or identifier to be passed in as the first argument. Not relevant for other action types."
],
get?: [ get?: [
type: :boolean, type: :boolean,
doc: """ doc: """
Expects to only receive a single result from a read action, and returns a single result instead of a list. Ignored for other action types. Expects to only receive a single result from a read action or a bulk update/destroy, and returns a single result instead of a list. Sets `require_reference?` to false automatically.
""" """
], ],
get_by: [ get_by: [
type: {:wrap_list, :atom}, type: {:wrap_list, :atom},
doc: """ doc: """
Takes a list of fields and adds those fields as arguments, which will then be used to filter. Sets `get?` to true automatically. Ignored for non-read actions. Takes a list of fields and adds those fields as arguments, which will then be used to filter. Sets `get?` to true and `require_reference?` to false automatically. Adds filters for read, update and destroy actions, replacing the `record` first argument.
""" """
], ],
get_by_identity: [ get_by_identity: [
type: :atom, type: :atom,
doc: """ doc: """
Only relevant for read actions. Takes an identity, and gets its field list, performing the same logic as `get_by` once it has the list of fields. Takes an identity, gets its field list, and performs the same logic as `get_by` with those fields. Adds filters for read, update and destroy actions, replacing the `record` first argument.
""" """
] ]
] ]