mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
improvement: support require_reference?: false
on code interfaces
improvement: support `:filter` option on bulk create/destroy
This commit is contained in:
parent
9c74e52bd8
commit
bc69f904e2
12 changed files with 278 additions and 99 deletions
|
@ -170,6 +170,7 @@ spark_locals_without_parens = [
|
|||
require_atomic?: 1,
|
||||
require_attributes: 1,
|
||||
require_primary_key?: 1,
|
||||
require_reference?: 1,
|
||||
required?: 1,
|
||||
resource: 1,
|
||||
resource: 2,
|
||||
|
|
|
@ -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. |
|
||||
| [`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. |
|
||||
| [`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. |
|
||||
| [`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_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. |
|
||||
| [`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?`](#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`](#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. |
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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. |
|
||||
| [`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. |
|
||||
| [`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. |
|
||||
| [`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_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. |
|
||||
| [`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?`](#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`](#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. |
|
||||
|
||||
|
||||
|
||||
|
|
10
lib/ash.ex
10
lib/ash.ex
|
@ -462,6 +462,11 @@ defmodule Ash do
|
|||
doc:
|
||||
"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: [
|
||||
type: {:wrap_list, {:one_of, [:atomic, :atomic_batches, :stream]}},
|
||||
default: [:atomic],
|
||||
|
@ -524,6 +529,11 @@ defmodule Ash do
|
|||
doc:
|
||||
"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: [
|
||||
type: {:list, {:or, [:atom, :string]}},
|
||||
doc:
|
||||
|
|
|
@ -120,6 +120,9 @@ defmodule Ash.Actions.Destroy.Bulk do
|
|||
|> Keyword.put(:domain, domain)
|
||||
|> Keyword.take(Ash.stream_opt_keys())
|
||||
|
||||
query =
|
||||
Ash.Query.do_filter(query, opts[:filter])
|
||||
|
||||
run(
|
||||
domain,
|
||||
Ash.stream!(
|
||||
|
@ -661,6 +664,7 @@ defmodule Ash.Actions.Destroy.Bulk do
|
|||
tracer: opts[:tracer],
|
||||
atomic_changeset: atomic_changeset,
|
||||
return_errors?: opts[:return_errors?],
|
||||
filter: opts[:filter],
|
||||
return_notifications?: opts[:return_notifications?],
|
||||
notify?: opts[:notify?],
|
||||
return_records?: opts[:return_records?],
|
||||
|
@ -833,6 +837,7 @@ defmodule Ash.Actions.Destroy.Bulk do
|
|||
|
||||
resource
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.filter(opts[:filter])
|
||||
|> Map.put(:domain, domain)
|
||||
|> Ash.Actions.Helpers.add_context(opts)
|
||||
|> Ash.Changeset.set_context(opts[:context] || %{})
|
||||
|
|
|
@ -85,6 +85,9 @@ defmodule Ash.Actions.Update.Bulk do
|
|||
}
|
||||
|
||||
_ ->
|
||||
query =
|
||||
Ash.Query.do_filter(query, opts[:filter])
|
||||
|
||||
run(
|
||||
domain,
|
||||
Ash.stream!(
|
||||
|
@ -109,6 +112,9 @@ defmodule Ash.Actions.Update.Bulk do
|
|||
}
|
||||
|
||||
atomic_changeset ->
|
||||
atomic_changeset =
|
||||
Ash.Changeset.filter(atomic_changeset, opts[:filter])
|
||||
|
||||
{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],
|
||||
atomic_changeset: atomic_changeset,
|
||||
return_errors?: opts[:return_errors?],
|
||||
filter: opts[:filter],
|
||||
return_notifications?: opts[:return_notifications?],
|
||||
notify?: opts[:notify?],
|
||||
return_records?: opts[:return_records?],
|
||||
|
@ -899,6 +906,7 @@ defmodule Ash.Actions.Update.Bulk do
|
|||
resource
|
||||
|> Ash.Changeset.new()
|
||||
|> Map.put(:domain, domain)
|
||||
|> Ash.Changeset.filter(opts[:filter])
|
||||
|> Ash.Actions.Helpers.add_context(opts)
|
||||
|> Ash.Changeset.set_context(opts[:context] || %{})
|
||||
|> Ash.Changeset.prepare_changeset_for_action(action, opts)
|
||||
|
|
|
@ -86,6 +86,7 @@ defmodule Ash.Actions.Update do
|
|||
|> Ash.Changeset.set_context(%{data_layer: %{use_atomic_update_data?: true}})
|
||||
|> Map.put(:load, changeset.load)
|
||||
|> Map.put(:select, changeset.select)
|
||||
|> Map.put(:filter, changeset.filter)
|
||||
|> Ash.Changeset.set_context(changeset.context)
|
||||
|
||||
{atomic_changeset, opts} =
|
||||
|
|
|
@ -4899,6 +4899,10 @@ defmodule Ash.Changeset do
|
|||
Used by optimistic locking. See `Ash.Resource.Change.Builtins.optimistic_lock/1` for more.
|
||||
"""
|
||||
@spec filter(t(), Ash.Expr.t()) :: t()
|
||||
def filter(changeset, expr) when expr in [nil, %{}, []] do
|
||||
changeset
|
||||
end
|
||||
|
||||
def filter(changeset, expr) do
|
||||
if Ash.DataLayer.data_layer_can?(changeset.resource, :changeset_filter) do
|
||||
%{
|
||||
|
|
|
@ -411,7 +411,7 @@ defmodule Ash.CodeInterface do
|
|||
|
||||
filter_keys =
|
||||
cond do
|
||||
action.type != :read ->
|
||||
action.type not in [:read, :update, :destroy] ->
|
||||
[]
|
||||
|
||||
interface.get_by_identity ->
|
||||
|
@ -736,64 +736,93 @@ defmodule Ash.CodeInterface do
|
|||
|
||||
:update ->
|
||||
subject = quote do: changeset
|
||||
subject_args = quote do: [record]
|
||||
|
||||
subject_args =
|
||||
if interface.require_reference? do
|
||||
quote do: [record]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
resolve_subject =
|
||||
quote do
|
||||
{changeset_opts, opts} =
|
||||
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
|
||||
if Enum.empty?(filter_keys) do
|
||||
quote do
|
||||
{changeset_opts, opts} =
|
||||
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
|
||||
|
||||
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
|
||||
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
|
||||
|
||||
changeset =
|
||||
record
|
||||
|> case do
|
||||
%Ash.Changeset{resource: unquote(resource)} ->
|
||||
Ash.Changeset.for_update(
|
||||
record,
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
changeset =
|
||||
record
|
||||
|> case do
|
||||
%Ash.Changeset{resource: unquote(resource)} ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
%Ash.Changeset{resource: other_resource} ->
|
||||
raise ArgumentError,
|
||||
"Changeset #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
record
|
||||
|> Ash.Changeset.filter(filters)
|
||||
|> Ash.Changeset.for_update(
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
|
||||
%other_resource{} when other_resource != unquote(resource) ->
|
||||
raise ArgumentError,
|
||||
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
%Ash.Changeset{resource: other_resource} ->
|
||||
raise ArgumentError,
|
||||
"Changeset #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
|
||||
%struct{} = record when struct == unquote(resource) ->
|
||||
Ash.Changeset.for_update(
|
||||
record,
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
%other_resource{} when other_resource != unquote(resource) ->
|
||||
raise ArgumentError,
|
||||
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
|
||||
%Ash.Query{} = query ->
|
||||
{:atomic, :query, query}
|
||||
%struct{} = record when struct == unquote(resource) ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
value when is_function(value) ->
|
||||
{:atomic, :stream, value}
|
||||
record
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.filter(filters)
|
||||
|> Ash.Changeset.for_update(
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
|
||||
%Stream{} = value ->
|
||||
{:atomic, :stream, value}
|
||||
%Ash.Query{} = query ->
|
||||
{:atomic, :query, query}
|
||||
|
||||
[{_key, _val} | _] = id ->
|
||||
{:atomic, :id, id}
|
||||
value when is_function(value) ->
|
||||
{:atomic, :stream, value}
|
||||
|
||||
list when is_list(list) ->
|
||||
{:atomic, :stream, list}
|
||||
%Stream{} = value ->
|
||||
{:atomic, :stream, value}
|
||||
|
||||
other ->
|
||||
{:atomic, :id, other}
|
||||
end
|
||||
[{_key, _val} | _] = id ->
|
||||
{:atomic, :id, id}
|
||||
|
||||
list when is_list(list) ->
|
||||
{:atomic, :stream, list}
|
||||
|
||||
other ->
|
||||
{:atomic, :id, other}
|
||||
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 =
|
||||
quote do
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
case changeset do
|
||||
{:atomic, method, id} ->
|
||||
bulk_opts =
|
||||
|
@ -811,12 +840,20 @@ defmodule Ash.CodeInterface do
|
|||
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
|
||||
{:ok, query} ->
|
||||
query
|
||||
|> Ash.bulk_update(unquote(action.name), params, bulk_opts)
|
||||
|> case do
|
||||
%Ash.BulkResult{} = result when method == :stream ->
|
||||
%Ash.BulkResult{} = result
|
||||
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
|
||||
result
|
||||
|
||||
%Ash.BulkResult{status: :success, records: [record]} = result ->
|
||||
|
@ -848,6 +885,8 @@ defmodule Ash.CodeInterface do
|
|||
quote do
|
||||
case changeset do
|
||||
{:atomic, method, id} ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
bulk_opts =
|
||||
opts
|
||||
|> Keyword.delete(:bulk_options)
|
||||
|
@ -863,12 +902,20 @@ defmodule Ash.CodeInterface do
|
|||
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
|
||||
{:ok, query} ->
|
||||
query
|
||||
|> Ash.bulk_update!(unquote(action.name), params, bulk_opts)
|
||||
|> case do
|
||||
%Ash.BulkResult{} = result when method == :stream ->
|
||||
%Ash.BulkResult{} = result
|
||||
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
|
||||
result
|
||||
|
||||
%Ash.BulkResult{status: :success, records: [record]} = result ->
|
||||
|
@ -896,66 +943,95 @@ defmodule Ash.CodeInterface do
|
|||
|
||||
:destroy ->
|
||||
subject = quote do: changeset
|
||||
subject_args = quote do: [record]
|
||||
|
||||
subject_args =
|
||||
if interface.require_reference? do
|
||||
quote do: [record]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
resolve_subject =
|
||||
quote do
|
||||
{changeset_opts, opts} =
|
||||
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
|
||||
if interface.require_reference? do
|
||||
quote do
|
||||
{changeset_opts, opts} =
|
||||
Keyword.split(opts, [:actor, :tenant, :authorize?, :tracer, :context])
|
||||
|
||||
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
|
||||
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
|
||||
|
||||
changeset =
|
||||
record
|
||||
|> case do
|
||||
%Ash.Changeset{resource: unquote(resource)} ->
|
||||
Ash.Changeset.for_destroy(
|
||||
record,
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
changeset =
|
||||
record
|
||||
|> case do
|
||||
%Ash.Changeset{resource: unquote(resource)} ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
%Ash.Changeset{resource: other_resource} ->
|
||||
raise ArgumentError,
|
||||
"Changeset #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
record
|
||||
|> Ash.Changeset.filter(filters)
|
||||
|> Ash.Changeset.for_destroy(
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
|
||||
%other_resource{} when other_resource != unquote(resource) ->
|
||||
raise ArgumentError,
|
||||
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
%Ash.Changeset{resource: other_resource} ->
|
||||
raise ArgumentError,
|
||||
"Changeset #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
|
||||
%struct{} = record when struct == unquote(resource) ->
|
||||
Ash.Changeset.for_destroy(
|
||||
record,
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
%other_resource{} when other_resource != unquote(resource) ->
|
||||
raise ArgumentError,
|
||||
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
|
||||
|
||||
%Ash.Query{} = query ->
|
||||
{:atomic, :query, query}
|
||||
%struct{} = record when struct == unquote(resource) ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
value when is_function(value) ->
|
||||
{:atomic, :stream, value}
|
||||
record
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.filter(filters)
|
||||
|> Ash.Changeset.for_destroy(
|
||||
unquote(action.name),
|
||||
params,
|
||||
changeset_opts
|
||||
)
|
||||
|
||||
%Stream{} = value ->
|
||||
{:atomic, :stream, value}
|
||||
%Ash.Query{} = query ->
|
||||
{:atomic, :query, query}
|
||||
|
||||
[{_key, _val} | _] = id ->
|
||||
{:atomic, :id, id}
|
||||
value when is_function(value) ->
|
||||
{:atomic, :stream, value}
|
||||
|
||||
list when is_list(list) ->
|
||||
{:atomic, :stream, list}
|
||||
%Stream{} = value ->
|
||||
{:atomic, :stream, value}
|
||||
|
||||
other ->
|
||||
{:atomic, :id, other}
|
||||
end
|
||||
[{_key, _val} | _] = id ->
|
||||
{:atomic, :id, id}
|
||||
|
||||
list when is_list(list) ->
|
||||
{:atomic, :stream, list}
|
||||
|
||||
other ->
|
||||
{:atomic, :id, other}
|
||||
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 =
|
||||
quote do
|
||||
case changeset do
|
||||
{:atomic, method, id} ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
bulk_opts =
|
||||
opts
|
||||
|> Keyword.drop([:bulk_options, :return_destroyed?])
|
||||
|
@ -971,12 +1047,20 @@ defmodule Ash.CodeInterface do
|
|||
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
|
||||
{:ok, query} ->
|
||||
query
|
||||
|> Ash.bulk_destroy(unquote(action.name), params, bulk_opts)
|
||||
|> case do
|
||||
%Ash.BulkResult{} = result when method == :stream ->
|
||||
%Ash.BulkResult{} = result
|
||||
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
|
||||
result
|
||||
|
||||
%Ash.BulkResult{status: :success, records: [record]} = result ->
|
||||
|
@ -1013,6 +1097,8 @@ defmodule Ash.CodeInterface do
|
|||
quote do
|
||||
case changeset do
|
||||
{:atomic, method, id} ->
|
||||
{filters, params} = Map.split(params, unquote(filter_keys))
|
||||
|
||||
bulk_opts =
|
||||
opts
|
||||
|> Keyword.drop([:bulk_options, :return_destroyed?])
|
||||
|
@ -1028,12 +1114,20 @@ defmodule Ash.CodeInterface do
|
|||
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
|
||||
{:ok, query} ->
|
||||
query
|
||||
|> Ash.bulk_destroy!(unquote(action.name), params, bulk_opts)
|
||||
|> case do
|
||||
%Ash.BulkResult{} = result when method == :stream ->
|
||||
%Ash.BulkResult{} = result
|
||||
when method == :stream and unquote(Enum.empty?(filter_keys)) ->
|
||||
result
|
||||
|
||||
%Ash.BulkResult{status: :success, records: [record]} = result ->
|
||||
|
|
|
@ -62,6 +62,16 @@ defmodule Ash.Resource.Change.Builtins do
|
|||
@relate_actor_opts
|
||||
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()
|
||||
def relate_actor(relationship, opts \\ []) do
|
||||
opts =
|
||||
|
|
14
lib/ash/resource/change/filter.ex
Normal file
14
lib/ash/resource/change/filter.ex
Normal 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
|
|
@ -2,18 +2,42 @@ defmodule Ash.Resource.Interface do
|
|||
@moduledoc """
|
||||
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__{}
|
||||
|
||||
def transform(definition) do
|
||||
{:ok,
|
||||
definition
|
||||
|> set_get?()
|
||||
|> set_require_reference?()}
|
||||
end
|
||||
|
||||
defp set_get?(definition) do
|
||||
if definition.get_by || definition.get_by_identity do
|
||||
{:ok, %{definition | get?: true}}
|
||||
%{definition | get?: true}
|
||||
else
|
||||
{:ok, definition}
|
||||
definition
|
||||
end
|
||||
end
|
||||
|
||||
defp set_require_reference?(%{get?: true} = definition) do
|
||||
%{definition | require_reference?: false}
|
||||
end
|
||||
|
||||
defp set_require_reference?(definition) do
|
||||
definition
|
||||
end
|
||||
|
||||
def interface_options(:calculate) do
|
||||
[
|
||||
actor: [
|
||||
|
@ -125,22 +149,28 @@ defmodule Ash.Resource.Interface do
|
|||
doc:
|
||||
"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?: [
|
||||
type: :boolean,
|
||||
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: [
|
||||
type: {:wrap_list, :atom},
|
||||
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: [
|
||||
type: :atom,
|
||||
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.
|
||||
"""
|
||||
]
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue