diff --git a/documentation/dsls/DSL-Ash.Resource.md b/documentation/dsls/DSL-Ash.Resource.md index 0912fbea9..3dcc379b3 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/DSL-Ash.Resource.md @@ -880,12 +880,14 @@ multiple actions of each type in a large application. * pipe_through * metadata * argument + * filter * [destroy](#actions-destroy) * change * validate * pipe_through * metadata * argument + * filter ### Examples @@ -1823,6 +1825,7 @@ Declares a `update` action. For calling this action, see the `Ash.Domain` docume * [pipe_through](#actions-update-pipe_through) * [metadata](#actions-update-metadata) * [argument](#actions-update-argument) + * [filter](#actions-update-filter) ### Examples @@ -2088,6 +2091,40 @@ argument :password_confirmation, :string Target: `Ash.Resource.Actions.Argument` +### actions.update.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#actions-update-filter-filter){: #actions-update-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + @@ -2112,6 +2149,7 @@ See `Ash.Resource.Change.Builtins.cascade_destroy/2` for cascading destroy opera * [pipe_through](#actions-destroy-pipe_through) * [metadata](#actions-destroy-metadata) * [argument](#actions-destroy-argument) + * [filter](#actions-destroy-filter) ### Examples @@ -2381,6 +2419,40 @@ argument :password_confirmation, :string Target: `Ash.Resource.Actions.Argument` +### actions.destroy.filter +```elixir +filter filter +``` + + +Applies a filter. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. + + + +### Examples +``` +filter expr(first_name == "fred") +filter expr(last_name == "weasley" and magician == true) + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`filter`](#actions-destroy-filter-filter){: #actions-destroy-filter-filter .spark-required} | `any` | | The filter to apply. Can use `^arg/1`, `^context/1` and `^actor/1` templates. Multiple filters are combined with *and*. | + + + + + + +### Introspection + +Target: `Ash.Resource.Dsl.Filter` + diff --git a/lib/ash/actions/destroy/bulk.ex b/lib/ash/actions/destroy/bulk.ex index 9e73ab41f..2a98399f5 100644 --- a/lib/ash/actions/destroy/bulk.ex +++ b/lib/ash/actions/destroy/bulk.ex @@ -126,6 +126,13 @@ defmodule Ash.Actions.Destroy.Bulk do query = %{query | domain: domain} + query = + if Map.get(action, :filter) do + Ash.Query.do_filter(query, action.filter) + else + query + end + fully_atomic_changeset = cond do not_atomic_reason -> diff --git a/lib/ash/actions/update/bulk.ex b/lib/ash/actions/update/bulk.ex index 52980f50c..bafc9d40e 100644 --- a/lib/ash/actions/update/bulk.ex +++ b/lib/ash/actions/update/bulk.ex @@ -86,6 +86,13 @@ defmodule Ash.Actions.Update.Bulk do query = %{query | domain: domain} + query = + if is_map(action) && Map.get(action, :filter) do + Ash.Query.do_filter(query, action.filter) + else + query + end + fully_atomic_changeset = cond do not_atomic_reason -> diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index ea511a963..4d5cb77f8 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -3136,6 +3136,16 @@ defmodule Ash.Changeset do |> timeout(changeset.timeout || opts[:timeout]) |> set_tenant(opts[:tenant] || changeset.tenant || changeset.data.__metadata__[:tenant]) |> Map.put(:action_type, action.type) + |> apply_action_filter(action) + end + + defp apply_action_filter(changeset, %{filter: nil}), do: changeset + + defp apply_action_filter(changeset, action) do + case Map.get(action, :filter) do + nil -> changeset + action_filter -> filter(changeset, action_filter) + end end defp reset_arguments(%{arguments: arguments} = changeset) do diff --git a/lib/ash/resource/actions/destroy.ex b/lib/ash/resource/actions/destroy.ex index b140e1c54..ebbba87f2 100644 --- a/lib/ash/resource/actions/destroy.ex +++ b/lib/ash/resource/actions/destroy.ex @@ -12,6 +12,7 @@ defmodule Ash.Resource.Actions.Destroy do :description, :error_handler, :multitenancy, + :filter, manual: nil, require_atomic?: Application.compile_env(:ash, :require_atomic_by_default?, true), skip_unknown_inputs: [], @@ -27,6 +28,7 @@ defmodule Ash.Resource.Actions.Destroy do require_attributes: [], allow_nil_input: [], changes: [], + filters: [], reject: [], transaction?: true, public?: true, @@ -40,6 +42,8 @@ defmodule Ash.Resource.Actions.Destroy do name: atom, manual: module | nil, multitenancy: atom, + filter: any, + filters: [any], action_select: list(atom) | nil, notifiers: list(module), arguments: list(Ash.Resource.Actions.Argument.t()), @@ -118,8 +122,23 @@ defmodule Ash.Resource.Actions.Destroy do def opt_schema, do: @opt_schema def transform(%{manual: manual} = action) when not is_nil(manual) do - {:ok, %{action | require_atomic?: false}} + {:ok, %{action | require_atomic?: false} |> concat_filters()} end - def transform(action), do: {:ok, action} + def transform(action), do: {:ok, concat_filters(action)} + + defp concat_filters(%{filters: [filter]} = action) do + %{action | filter: filter.filter} + end + + defp concat_filters(%{filters: [first | rest]} = action) do + filter = + Enum.reduce(rest, first.filter, fn filter, acc -> + Ash.Query.BooleanExpression.new(:and, filter.filter, acc) + end) + + %{action | filter: filter} + end + + defp concat_filters(action), do: action end diff --git a/lib/ash/resource/actions/update.ex b/lib/ash/resource/actions/update.ex index d94971590..e72d0d687 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 :description, :error_handler, :multitenancy, + :filter, accept: nil, require_attributes: [], allow_nil_input: [], @@ -27,6 +28,7 @@ defmodule Ash.Resource.Actions.Update do skip_global_validations?: false, arguments: [], changes: [], + filters: [], reject: [], metadata: [], transaction?: true, @@ -41,6 +43,8 @@ defmodule Ash.Resource.Actions.Update do name: atom, manual: module | nil, multitenancy: atom, + filter: any, + filters: [any], skip_unknown_inputs: list(atom | String.t()), atomic_upgrade?: boolean(), action_select: list(atom) | nil, @@ -115,8 +119,23 @@ defmodule Ash.Resource.Actions.Update do def opt_schema, do: @opt_schema def transform(%{manual: manual} = action) when not is_nil(manual) do - {:ok, %{action | require_atomic?: false}} + {:ok, %{action | require_atomic?: false} |> concat_filters()} end - def transform(action), do: {:ok, action} + def transform(action), do: {:ok, concat_filters(action)} + + defp concat_filters(%{filters: [filter]} = action) do + %{action | filter: filter.filter} + end + + defp concat_filters(%{filters: [first | rest]} = action) do + filter = + Enum.reduce(rest, first.filter, fn filter, acc -> + Ash.Query.BooleanExpression.new(:and, filter.filter, acc) + end) + + %{action | filter: filter} + end + + defp concat_filters(action), do: action end diff --git a/lib/ash/resource/change/builtins.ex b/lib/ash/resource/change/builtins.ex index 8069352c3..5dd281592 100644 --- a/lib/ash/resource/change/builtins.ex +++ b/lib/ash/resource/change/builtins.ex @@ -14,8 +14,8 @@ defmodule Ash.Resource.Change.Builtins do 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 + @spec filter_where(expr :: Ash.Expr.t()) :: Ash.Resource.Change.ref() + def filter_where(filter) do {Ash.Resource.Change.Filter, filter: filter} end diff --git a/lib/ash/resource/dsl.ex b/lib/ash/resource/dsl.ex index 2030bb0d7..c64ed712e 100644 --- a/lib/ash/resource/dsl.ex +++ b/lib/ash/resource/dsl.ex @@ -727,6 +727,9 @@ defmodule Ash.Resource.Dsl do ], arguments: [ @action_argument + ], + filters: [ + @filter ] ], deprecations: [ @@ -773,6 +776,9 @@ defmodule Ash.Resource.Dsl do ], arguments: [ @action_argument + ], + filters: [ + @filter ] ], target: Ash.Resource.Actions.Destroy, diff --git a/test/actions/bulk/bulk_destroy_test.exs b/test/actions/bulk/bulk_destroy_test.exs index d0e07e521..cf44c37fe 100644 --- a/test/actions/bulk/bulk_destroy_test.exs +++ b/test/actions/bulk/bulk_destroy_test.exs @@ -256,7 +256,7 @@ defmodule Ash.Test.Actions.BulkDestroyTest do end destroy :destroy_with_filter do - change filter(expr(title == "foo")) + change filter_where(expr(title == "foo")) end destroy :soft do diff --git a/test/actions/bulk/bulk_update_test.exs b/test/actions/bulk/bulk_update_test.exs index 3c5a0ef52..8f3fee2ae 100644 --- a/test/actions/bulk/bulk_update_test.exs +++ b/test/actions/bulk/bulk_update_test.exs @@ -422,7 +422,7 @@ defmodule Ash.Test.Actions.BulkUpdateTest do end update :update_with_filter do - change filter(expr(title == "foo")) + change filter_where(expr(title == "foo")) end update :update_fails_on_title2 do