diff --git a/.formatter.exs b/.formatter.exs index 00aba8ee3..cea649e36 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -211,6 +211,7 @@ spark_locals_without_parens = [ not_found_error?: 1, not_found_message: 1, notification_metadata: 1, + notification_metadata: 2, notifiers: 1, notify?: 1, offset?: 1, diff --git a/documentation/dsls/DSL-Ash.Reactor.md b/documentation/dsls/DSL-Ash.Reactor.md index 5c1342457..478a7f788 100644 --- a/documentation/dsls/DSL-Ash.Reactor.md +++ b/documentation/dsls/DSL-Ash.Reactor.md @@ -670,6 +670,7 @@ Caveats/differences from `Ash.bulk_create/4`: * [guard](#reactor-bulk_create-guard) * [where](#reactor-bulk_create-where) * [load](#reactor-bulk_create-load) + * [notification_metadata](#reactor-bulk_create-notification_metadata) * [tenant](#reactor-bulk_create-tenant) * [wait_for](#reactor-bulk_create-wait_for) @@ -917,6 +918,37 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` +### reactor.bulk_create.notification_metadata +```elixir +notification_metadata source +``` + + +Specifies metadata to be merged into the metadata field for all notifications sent from this operation + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source`](#reactor-bulk_create-notification_metadata-source){: #reactor-bulk_create-notification_metadata-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value \| map \| nil` | | What to use as the source of the notification metadata. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`transform`](#reactor-bulk_create-notification_metadata-transform){: #reactor-bulk_create-notification_metadata-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the notification metadata before it is passed to the action. | + + + + + +### Introspection + +Target: `Ash.Reactor.Dsl.NotificationMetadata` + ### reactor.bulk_create.tenant ```elixir tenant source @@ -1030,6 +1062,7 @@ Caveats/differences from `Ash.bulk_update/4`: * [guard](#reactor-bulk_update-guard) * [where](#reactor-bulk_update-where) * [inputs](#reactor-bulk_update-inputs) + * [notification_metadata](#reactor-bulk_update-notification_metadata) * [tenant](#reactor-bulk_update-tenant) * [wait_for](#reactor-bulk_update-wait_for) @@ -1298,6 +1331,37 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` +### reactor.bulk_update.notification_metadata +```elixir +notification_metadata source +``` + + +Specifies metadata to be merged into the metadata field for all notifications sent from this operation + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source`](#reactor-bulk_update-notification_metadata-source){: #reactor-bulk_update-notification_metadata-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value \| map \| nil` | | What to use as the source of the notification metadata. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`transform`](#reactor-bulk_update-notification_metadata-transform){: #reactor-bulk_update-notification_metadata-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the notification metadata before it is passed to the action. | + + + + + +### Introspection + +Target: `Ash.Reactor.Dsl.NotificationMetadata` + ### reactor.bulk_update.tenant ```elixir tenant source @@ -1554,6 +1618,7 @@ Declares a step that will call a create action on a resource. * [where](#reactor-create-where) * [inputs](#reactor-create-inputs) * [load](#reactor-create-load) + * [notification_metadata](#reactor-create-notification_metadata) * [tenant](#reactor-create-tenant) * [wait_for](#reactor-create-wait_for) @@ -1830,6 +1895,37 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` +### reactor.create.notification_metadata +```elixir +notification_metadata source +``` + + +Specifies metadata to be merged into the metadata field for all notifications sent from this operation + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source`](#reactor-create-notification_metadata-source){: #reactor-create-notification_metadata-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value \| map \| nil` | | What to use as the source of the notification metadata. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`transform`](#reactor-create-notification_metadata-transform){: #reactor-create-notification_metadata-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the notification metadata before it is passed to the action. | + + + + + +### Introspection + +Target: `Ash.Reactor.Dsl.NotificationMetadata` + ### reactor.create.tenant ```elixir tenant source @@ -1934,6 +2030,7 @@ Declares a step that will call a destroy action on a resource. * [where](#reactor-destroy-where) * [inputs](#reactor-destroy-inputs) * [load](#reactor-destroy-load) + * [notification_metadata](#reactor-destroy-notification_metadata) * [tenant](#reactor-destroy-tenant) * [wait_for](#reactor-destroy-wait_for) @@ -2206,6 +2303,37 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` +### reactor.destroy.notification_metadata +```elixir +notification_metadata source +``` + + +Specifies metadata to be merged into the metadata field for all notifications sent from this operation + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source`](#reactor-destroy-notification_metadata-source){: #reactor-destroy-notification_metadata-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value \| map \| nil` | | What to use as the source of the notification metadata. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`transform`](#reactor-destroy-notification_metadata-transform){: #reactor-destroy-notification_metadata-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the notification metadata before it is passed to the action. | + + + + + +### Introspection + +Target: `Ash.Reactor.Dsl.NotificationMetadata` + ### reactor.destroy.tenant ```elixir tenant source @@ -3487,6 +3615,7 @@ Declares a step that will call an update action on a resource. * [where](#reactor-update-where) * [inputs](#reactor-update-inputs) * [load](#reactor-update-load) + * [notification_metadata](#reactor-update-notification_metadata) * [tenant](#reactor-update-tenant) * [wait_for](#reactor-update-wait_for) @@ -3761,6 +3890,37 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` +### reactor.update.notification_metadata +```elixir +notification_metadata source +``` + + +Specifies metadata to be merged into the metadata field for all notifications sent from this operation + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source`](#reactor-update-notification_metadata-source){: #reactor-update-notification_metadata-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value \| map \| nil` | | What to use as the source of the notification metadata. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`transform`](#reactor-update-notification_metadata-transform){: #reactor-update-notification_metadata-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the notification metadata before it is passed to the action. | + + + + + +### Introspection + +Target: `Ash.Reactor.Dsl.NotificationMetadata` + ### reactor.update.tenant ```elixir tenant source diff --git a/lib/ash/reactor/builders/bulk_create.ex b/lib/ash/reactor/builders/bulk_create.ex index d1d2b1761..73bf9cbc9 100644 --- a/lib/ash/reactor/builders/bulk_create.ex +++ b/lib/ash/reactor/builders/bulk_create.ex @@ -10,27 +10,18 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.BulkCreate do alias Reactor.{Argument, Builder} alias Spark.{Dsl.Transformer, Error.DslError} import Ash.Reactor.BuilderUtils - import Reactor.Template, only: :macros @doc false @impl true def build(bulk_create, reactor) do initial = %Argument{name: :initial, source: bulk_create.initial} - notification_metadata = - case bulk_create.notification_metadata do - template when is_template(template) -> - %Argument{name: :notification_metadata, source: template} - - map when is_map(map) -> - Argument.from_value(:notification_metadata, map) - end - arguments = - [initial, notification_metadata] + [initial] |> maybe_append(bulk_create.actor) |> maybe_append(bulk_create.tenant) |> maybe_append(bulk_create.load) + |> maybe_append(bulk_create.notification_metadata) |> maybe_append(bulk_create.context) |> Enum.concat(bulk_create.wait_for) diff --git a/lib/ash/reactor/builders/bulk_update.ex b/lib/ash/reactor/builders/bulk_update.ex index 9c0f6cd3a..301a39d50 100644 --- a/lib/ash/reactor/builders/bulk_update.ex +++ b/lib/ash/reactor/builders/bulk_update.ex @@ -10,7 +10,6 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.BulkUpdate do alias Reactor.{Argument, Builder} alias Spark.{Dsl.Transformer, Error.DslError} import Ash.Reactor.BuilderUtils - import Reactor.Template, only: :macros @doc false @impl true @@ -18,23 +17,15 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.BulkUpdate do with {:ok, reactor, arguments} <- build_input_arguments(reactor, bulk_update) do initial = %Argument{name: :initial, source: bulk_update.initial} - notification_metadata = - case bulk_update.notification_metadata do - template when is_template(template) -> - %Argument{name: :notification_metadata, source: template} - - map when is_map(map) -> - Argument.from_value(:notification_metadata, map) - end - arguments = arguments |> maybe_append(bulk_update.actor) |> maybe_append(bulk_update.tenant) |> maybe_append(bulk_update.load) + |> maybe_append(bulk_update.notification_metadata) |> maybe_append(bulk_update.context) |> Enum.concat(bulk_update.wait_for) - |> Enum.concat([initial, notification_metadata]) + |> Enum.concat([initial]) action_options = bulk_update diff --git a/lib/ash/reactor/builders/create.ex b/lib/ash/reactor/builders/create.ex index 0852e384e..cfe70889b 100644 --- a/lib/ash/reactor/builders/create.ex +++ b/lib/ash/reactor/builders/create.ex @@ -34,6 +34,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Create do |> maybe_append(create.tenant) |> maybe_append(create.load) |> maybe_append(create.context) + |> maybe_append(create.notification_metadata) |> Enum.concat(create.wait_for) |> Enum.concat([initial]) diff --git a/lib/ash/reactor/builders/destroy.ex b/lib/ash/reactor/builders/destroy.ex index 23fd801b0..e206b4ccb 100644 --- a/lib/ash/reactor/builders/destroy.ex +++ b/lib/ash/reactor/builders/destroy.ex @@ -21,6 +21,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Destroy do |> maybe_append(destroy.tenant) |> maybe_append(destroy.load) |> maybe_append(destroy.context) + |> maybe_append(destroy.notification_metadata) |> Enum.concat(destroy.wait_for) |> Enum.concat([%Argument{name: :initial, source: destroy.initial}]) diff --git a/lib/ash/reactor/builders/notification_metadata.ex b/lib/ash/reactor/builders/notification_metadata.ex new file mode 100644 index 000000000..eb1f00b3a --- /dev/null +++ b/lib/ash/reactor/builders/notification_metadata.ex @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2019 ash contributors +# +# SPDX-License-Identifier: MIT + +defimpl Reactor.Argument.Build, for: Ash.Reactor.Dsl.NotificationMetadata do + import Reactor.Template, only: [is_template: 1] + + @doc false + @impl true + def build(notification_metadata) when is_template(notification_metadata.source) do + %Reactor.Argument{ + name: :notification_metadata, + source: notification_metadata.source, + transform: notification_metadata.transform + } + |> then(&{:ok, [&1]}) + end + + def build(notification_metadata) when is_map(notification_metadata.source) do + Reactor.Argument.from_value( + :notification_metadata, + notification_metadata.source, + transform: notification_metadata.transform + ) + |> then(&{:ok, [&1]}) + end + + def build(notification_metadata) when is_nil(notification_metadata.source) do + build(%{notification_metadata | source: %{}}) + end +end diff --git a/lib/ash/reactor/builders/update.ex b/lib/ash/reactor/builders/update.ex index 32c98598d..9aa9110b7 100644 --- a/lib/ash/reactor/builders/update.ex +++ b/lib/ash/reactor/builders/update.ex @@ -21,6 +21,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Update do |> maybe_append(update.tenant) |> maybe_append(update.load) |> maybe_append(update.context) + |> maybe_append(update.notification_metadata) |> Enum.concat(update.wait_for) |> Enum.concat([%Argument{name: :initial, source: update.initial}]) diff --git a/lib/ash/reactor/dsl/bulk_create.ex b/lib/ash/reactor/dsl/bulk_create.ex index fd112e9f3..91d158772 100644 --- a/lib/ash/reactor/dsl/bulk_create.ex +++ b/lib/ash/reactor/dsl/bulk_create.ex @@ -31,7 +31,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do load: nil, max_concurrency: 0, name: nil, - notification_metadata: %{}, + notification_metadata: nil, notify?: false, read_action: nil, resource: nil, @@ -76,7 +76,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do load: nil | Ash.Reactor.Dsl.ActionLoad.t(), max_concurrency: non_neg_integer(), name: atom, - notification_metadata: map, + notification_metadata: nil | Ash.Reactor.Dsl.NotificationMetadata.t(), notify?: boolean, read_action: atom, resource: module, @@ -138,10 +138,11 @@ defmodule Ash.Reactor.Dsl.BulkCreate do context: [Ash.Reactor.Dsl.Context.__entity__()], guards: [Reactor.Dsl.Guard.__entity__(), Reactor.Dsl.Where.__entity__()], load: [Ash.Reactor.Dsl.ActionLoad.__entity__()], + notification_metadata: [Ash.Reactor.Dsl.NotificationMetadata.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], wait_for: [Reactor.Dsl.WaitFor.__entity__()] ], - singleton_entity_keys: [:actor, :context, :load, :tenant], + singleton_entity_keys: [:actor, :context, :load, :notification_metadata, :tenant], recursive_as: :steps, schema: [ diff --git a/lib/ash/reactor/dsl/bulk_update.ex b/lib/ash/reactor/dsl/bulk_update.ex index 86a5cab07..fecc63bd8 100644 --- a/lib/ash/reactor/dsl/bulk_update.ex +++ b/lib/ash/reactor/dsl/bulk_update.ex @@ -37,7 +37,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do lock: nil, max_concurrency: 0, name: nil, - notification_metadata: %{}, + notification_metadata: nil, notify?: false, page: [], read_action: nil, @@ -92,7 +92,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do lock: nil | Ash.DataLayer.lock_type(), max_concurrency: non_neg_integer(), name: atom, - notification_metadata: map | Reactor.Template.t(), + notification_metadata: nil | Ash.Reactor.Dsl.NotificationMetadata.t(), notify?: boolean, page: Keyword.t(), read_action: atom, @@ -156,10 +156,11 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do context: [Ash.Reactor.Dsl.Context.__entity__()], guards: [Reactor.Dsl.Guard.__entity__(), Reactor.Dsl.Where.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], + notification_metadata: [Ash.Reactor.Dsl.NotificationMetadata.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], wait_for: [Reactor.Dsl.WaitFor.__entity__()] ], - singleton_entity_keys: [:actor, :context, :tenant], + singleton_entity_keys: [:actor, :context, :notification_metadata, :tenant], recursive_as: :steps, schema: [ diff --git a/lib/ash/reactor/dsl/create.ex b/lib/ash/reactor/dsl/create.ex index 02121b7b9..35139ee65 100644 --- a/lib/ash/reactor/dsl/create.ex +++ b/lib/ash/reactor/dsl/create.ex @@ -21,6 +21,7 @@ defmodule Ash.Reactor.Dsl.Create do inputs: [], load: nil, name: nil, + notification_metadata: nil, resource: nil, tenant: [], transform: nil, @@ -47,6 +48,7 @@ defmodule Ash.Reactor.Dsl.Create do inputs: [Ash.Reactor.Dsl.Inputs.t()], load: nil | Ash.Reactor.Dsl.ActionLoad.t(), name: atom, + notification_metadata: nil | Ash.Reactor.Dsl.NotificationMetadata.t(), resource: module, tenant: nil | Ash.Reactor.Dsl.Tenant.t(), type: :create, @@ -90,10 +92,11 @@ defmodule Ash.Reactor.Dsl.Create do guards: [Reactor.Dsl.Guard.__entity__(), Reactor.Dsl.Where.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], load: [Ash.Reactor.Dsl.ActionLoad.__entity__()], + notification_metadata: [Ash.Reactor.Dsl.NotificationMetadata.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], wait_for: [Reactor.Dsl.WaitFor.__entity__()] ], - singleton_entity_keys: [:actor, :context, :load, :tenant], + singleton_entity_keys: [:actor, :context, :load, :notification_metadata, :tenant], recursive_as: :steps, schema: [ diff --git a/lib/ash/reactor/dsl/destroy.ex b/lib/ash/reactor/dsl/destroy.ex index 18267a086..f91993fd2 100644 --- a/lib/ash/reactor/dsl/destroy.ex +++ b/lib/ash/reactor/dsl/destroy.ex @@ -21,6 +21,7 @@ defmodule Ash.Reactor.Dsl.Destroy do inputs: [], load: nil, name: nil, + notification_metadata: nil, resource: nil, return_destroyed?: false, tenant: [], @@ -46,6 +47,7 @@ defmodule Ash.Reactor.Dsl.Destroy do inputs: [Ash.Reactor.Dsl.Inputs.t()], load: nil | Ash.Reactor.Dsl.ActionLoad.t(), name: atom, + notification_metadata: nil | Ash.Reactor.Dsl.NotificationMetadata.t(), resource: module, return_destroyed?: boolean, tenant: nil | Ash.Reactor.Dsl.Tenant.t(), @@ -85,10 +87,11 @@ defmodule Ash.Reactor.Dsl.Destroy do guards: [Reactor.Dsl.Guard.__entity__(), Reactor.Dsl.Where.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], load: [Ash.Reactor.Dsl.ActionLoad.__entity__()], + notification_metadata: [Ash.Reactor.Dsl.NotificationMetadata.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], wait_for: [Reactor.Dsl.WaitFor.__entity__()] ], - singleton_entity_keys: [:actor, :context, :load, :tenant], + singleton_entity_keys: [:actor, :context, :load, :notification_metadata, :tenant], recursive_as: :steps, schema: [ diff --git a/lib/ash/reactor/dsl/notification_metadata.ex b/lib/ash/reactor/dsl/notification_metadata.ex new file mode 100644 index 000000000..527ff7f9b --- /dev/null +++ b/lib/ash/reactor/dsl/notification_metadata.ex @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2019 ash contributors +# +# SPDX-License-Identifier: MIT + +defmodule Ash.Reactor.Dsl.NotificationMetadata do + @moduledoc """ + Specify the notification metadata for an action. + """ + + defstruct __identifier__: nil, source: nil, transform: nil, __spark_metadata__: nil + + alias Reactor.Template + + @type t :: %__MODULE__{ + __identifier__: any, + source: Template.t() | map() | nil, + transform: nil | (any -> any) | {module, keyword} | mfa, + __spark_metadata__: Spark.Dsl.Entity.spark_meta() + } + + @doc false + def __entity__ do + template_type = Template.type() + + %Spark.Dsl.Entity{ + name: :notification_metadata, + describe: + "Specifies metadata to be merged into the metadata field for all notifications sent from this operation", + args: [:source], + imports: [Reactor.Dsl.Argument], + identifier: {:auto, :unique_integer}, + target: __MODULE__, + schema: [ + source: [ + type: {:or, [template_type, :map, nil]}, + required: true, + doc: "What to use as the source of the notification metadata." + ], + transform: [ + type: + {:or, [{:spark_function_behaviour, Reactor.Step, {Reactor.Step.Transform, 1}}, nil]}, + required: false, + default: nil, + doc: + "An optional transformation function which can be used to modify the notification metadata before it is passed to the action." + ] + ] + } + end +end diff --git a/lib/ash/reactor/dsl/update.ex b/lib/ash/reactor/dsl/update.ex index be96dfb57..13f796238 100644 --- a/lib/ash/reactor/dsl/update.ex +++ b/lib/ash/reactor/dsl/update.ex @@ -21,6 +21,7 @@ defmodule Ash.Reactor.Dsl.Update do inputs: [], load: nil, name: nil, + notification_metadata: nil, resource: nil, tenant: [], transform: nil, @@ -45,6 +46,7 @@ defmodule Ash.Reactor.Dsl.Update do inputs: [Ash.Reactor.Dsl.Inputs.t()], load: nil | Ash.Reactor.Dsl.ActionLoad.t(), name: atom, + notification_metadata: nil | Ash.Reactor.Dsl.NotificationMetadata.t(), resource: module, tenant: nil | Ash.Reactor.Dsl.Tenant.t(), type: :update, @@ -86,10 +88,11 @@ defmodule Ash.Reactor.Dsl.Update do guards: [Reactor.Dsl.Guard.__entity__(), Reactor.Dsl.Where.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], load: [Ash.Reactor.Dsl.ActionLoad.__entity__()], + notification_metadata: [Ash.Reactor.Dsl.NotificationMetadata.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], wait_for: [Reactor.Dsl.WaitFor.__entity__()] ], - singleton_entity_keys: [:actor, :context, :load, :tenant], + singleton_entity_keys: [:actor, :context, :load, :notification_metadata, :tenant], recursive_as: :steps, schema: [ diff --git a/lib/ash/reactor/steps/create_step.ex b/lib/ash/reactor/steps/create_step.ex index 7d224da29..e866c321e 100644 --- a/lib/ash/reactor/steps/create_step.ex +++ b/lib/ash/reactor/steps/create_step.ex @@ -31,6 +31,7 @@ defmodule Ash.Reactor.CreateStep do |> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:load, arguments[:load]) |> maybe_set_kw(:context, arguments[:context]) + |> maybe_set_kw(:notification_metadata, arguments[:notification_metadata]) changeset = case arguments.initial do diff --git a/lib/ash/reactor/steps/destroy_step.ex b/lib/ash/reactor/steps/destroy_step.ex index 7e6323751..e3414389b 100644 --- a/lib/ash/reactor/steps/destroy_step.ex +++ b/lib/ash/reactor/steps/destroy_step.ex @@ -33,6 +33,7 @@ defmodule Ash.Reactor.DestroyStep do |> maybe_set_kw(:return_destroyed?, return_destroyed?) |> maybe_set_kw(:load, arguments[:load]) |> maybe_set_kw(:context, arguments[:context]) + |> maybe_set_kw(:notification_metadata, arguments[:notification_metadata]) arguments[:initial] |> Changeset.for_destroy(options[:action], arguments[:input], changeset_options) diff --git a/lib/ash/reactor/steps/update_step.ex b/lib/ash/reactor/steps/update_step.ex index 62f7f6468..5f0c3b909 100644 --- a/lib/ash/reactor/steps/update_step.ex +++ b/lib/ash/reactor/steps/update_step.ex @@ -29,6 +29,7 @@ defmodule Ash.Reactor.UpdateStep do |> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:load, arguments[:load]) |> maybe_set_kw(:context, arguments[:context]) + |> maybe_set_kw(:notification_metadata, arguments[:notification_metadata]) changeset = arguments[:initial] diff --git a/test/reactor/create_test.exs b/test/reactor/create_test.exs index 71bb0fb3e..0fd39b989 100644 --- a/test/reactor/create_test.exs +++ b/test/reactor/create_test.exs @@ -8,6 +8,16 @@ defmodule Ash.Test.ReactorCreateTest do alias Ash.Test.Domain + defmodule TestNotifier do + @moduledoc false + use Ash.Notifier + + def notify(notification) do + send(self(), {:notification, notification}) + :ok + end + end + defmodule Author do @moduledoc false use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain @@ -48,7 +58,10 @@ defmodule Ash.Test.ReactorCreateTest do defmodule Post do @moduledoc false - use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain + use Ash.Resource, + data_layer: Ash.DataLayer.Ets, + domain: Domain, + notifiers: [TestNotifier] ets do private? true @@ -91,17 +104,13 @@ defmodule Ash.Test.ReactorCreateTest do test "it can create a post" do defmodule SimpleCreatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :title input :sub_title create :create_post, Post, :create do - inputs(%{title: input(:title), sub_title: input(:sub_title)}) + inputs %{title: input(:title), sub_title: input(:sub_title)} end end @@ -116,17 +125,13 @@ defmodule Ash.Test.ReactorCreateTest do test "it defaults to the primary action when the action is not supplied" do defmodule InferredActionNameCreatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :title input :sub_title create :create_post, Post do - inputs(%{title: input(:title), sub_title: input(:sub_title)}) + inputs %{title: input(:title), sub_title: input(:sub_title)} end end @@ -140,18 +145,14 @@ defmodule Ash.Test.ReactorCreateTest do test "it merges multiple `inputs` entities together" do defmodule MergedInputsCreatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :title input :sub_title create :create_post, Post, :create do - inputs(%{title: input(:title)}) - inputs(%{sub_title: input(:sub_title)}) + inputs %{title: input(:title)} + inputs %{sub_title: input(:sub_title)} end end @@ -168,11 +169,7 @@ defmodule Ash.Test.ReactorCreateTest do test "`inputs` entities can be transformed separately" do defmodule TransformedInputsCreatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :title @@ -187,7 +184,10 @@ defmodule Ash.Test.ReactorCreateTest do end end - assert {:ok, post} = Reactor.run(TransformedInputsCreatePostReactor, %{title: "Title"}) + assert {:ok, post} = + Reactor.run(TransformedInputsCreatePostReactor, %{ + title: "Title" + }) assert post.title == "TITLE" assert post.sub_title == "title" @@ -196,23 +196,23 @@ defmodule Ash.Test.ReactorCreateTest do test "it can provide an actor" do defmodule CreateWithActorCreatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :author_name input :title input :sub_title create :create_author, Author, :create do - inputs(%{name: input(:author_name)}) + inputs %{name: input(:author_name)} end create :create_post, Post, :with_actor_as_author do - inputs(%{title: input(:title), sub_title: input(:sub_title)}) - actor(result(:create_author)) + inputs %{ + title: input(:title), + sub_title: input(:sub_title) + } + + actor result(:create_author) end end @@ -230,18 +230,14 @@ defmodule Ash.Test.ReactorCreateTest do test "it can provide a tenant" do defmodule TenantedCreateAuthorReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :author_name input :organisation_name create :create_author, Author, :create do - inputs(%{name: input(:author_name)}) - tenant(input(:organisation_name)) + inputs %{name: input(:author_name)} + tenant input(:organisation_name) end end @@ -260,17 +256,13 @@ defmodule Ash.Test.ReactorCreateTest do @moduledoc false use Ash.Reactor - ash do - default_domain(Domain) - end - input :author_name create :create_author, Author, :create do - inputs(%{name: input(:author_name)}) + inputs %{name: input(:author_name)} undo :always - undo_action(:undo_create) + undo_action :undo_create end step :fail do @@ -284,8 +276,12 @@ defmodule Ash.Test.ReactorCreateTest do end end - UndoingCreateAuthorReactor - |> Reactor.run(%{author_name: "Marty McFly"}, %{}, async?: false) + Reactor.run( + UndoingCreateAuthorReactor, + %{author_name: "Marty McFly"}, + %{}, + async?: false + ) |> Ash.Test.assert_has_error(fn %Reactor.Error.Invalid.RunStepError{error: %RuntimeError{message: "hell"}} -> true @@ -302,18 +298,17 @@ defmodule Ash.Test.ReactorCreateTest do @moduledoc false use Ash.Reactor - ash do - default_domain(Domain) - end - input :title input :sub_title create :create_post, Post, :create do - inputs(%{title: input(:title), sub_title: input(:sub_title)}) + inputs %{ + title: input(:title), + sub_title: input(:sub_title) + } undo :always - undo_action(:soft_delete) + undo_action :soft_delete end step :fail do @@ -328,8 +323,12 @@ defmodule Ash.Test.ReactorCreateTest do end end - UndoingCreatePostWithUpdateReactor - |> Reactor.run(%{title: "Test Post", sub_title: "Test Sub"}, %{}, async?: false) + Reactor.run( + UndoingCreatePostWithUpdateReactor, + %{title: "Test Post", sub_title: "Test Sub"}, + %{}, + async?: false + ) |> Ash.Test.assert_has_error(fn %Reactor.Error.Invalid.RunStepError{error: %RuntimeError{message: "hell"}} -> true @@ -352,23 +351,116 @@ defmodule Ash.Test.ReactorCreateTest do step :create_changeset do run fn _ -> changeset = - Post - |> Ash.Changeset.for_create(:create, %{title: "Foo", sub_title: "Bar"}) + Ash.Changeset.for_create(Post, :create, %{ + title: "Foo", + sub_title: "Bar" + }) {:ok, changeset} end end create :create_post, Post do - initial(result(:create_changeset)) + initial result(:create_changeset) end return :create_post end - assert {:ok, post} = Reactor.run(CreateFromChangesetReactor, %{}, %{}, async?: false) + assert {:ok, post} = Reactor.run(CreateFromChangesetReactor, %{}) assert post.title == "Foo" assert post.sub_title == "Bar" end + + test "it can provide notification metadata" do + defmodule CreateWithNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :title + input :sub_title + + create :create_post, Post, :create do + inputs %{title: input(:title), sub_title: input(:sub_title)} + notification_metadata value(%{source: "reactor", operation: "create"}) + end + end + + assert {:ok, post} = + Reactor.run(CreateWithNotificationMetadataReactor, %{ + title: "Title", + sub_title: "Sub-title" + }) + + assert post.title == "Title" + + assert_receive {:notification, + %Ash.Notifier.Notification{ + metadata: %{source: "reactor", operation: "create"} + }} + end + + test "it can provide notification metadata from a template" do + defmodule CreateWithTemplateNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :title + input :metadata + + create :create_post, Post, :create do + inputs %{title: input(:title)} + notification_metadata input(:metadata) + end + end + + metadata = %{request_id: "req_123", user_id: "user_456"} + + assert {:ok, post} = + Reactor.run(CreateWithTemplateNotificationMetadataReactor, %{ + title: "Title", + metadata: metadata + }) + + assert post.title == "Title" + + assert_receive {:notification, + %Ash.Notifier.Notification{ + metadata: ^metadata + }} + end + + test "it can transform notification metadata" do + defmodule CreateWithTransformedNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :title + input :metadata + + create :create_post, Post, :create do + inputs %{title: input(:title)} + + notification_metadata input(:metadata) do + transform fn val -> + Map.put(val, :doubled, val.count * 2) + end + end + end + end + + assert {:ok, post} = + Reactor.run(CreateWithTransformedNotificationMetadataReactor, %{ + title: "Title", + metadata: %{count: 1} + }) + + assert post.title == "Title" + + assert_receive {:notification, + %Ash.Notifier.Notification{ + metadata: %{count: 1, doubled: 2} + }} + end end diff --git a/test/reactor/destroy_test.exs b/test/reactor/destroy_test.exs index 5d6b129bf..93c1517fc 100644 --- a/test/reactor/destroy_test.exs +++ b/test/reactor/destroy_test.exs @@ -8,9 +8,22 @@ defmodule Ash.Test.ReactorDestroyTest do alias Ash.Test.Domain + defmodule TestNotifier do + @moduledoc false + use Ash.Notifier + + def notify(notification) do + send(self(), {:notification, notification}) + :ok + end + end + defmodule Post do @moduledoc false - use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain + use Ash.Resource, + data_layer: Ash.DataLayer.Ets, + domain: Domain, + notifiers: [TestNotifier] ets do private? true @@ -54,48 +67,48 @@ defmodule Ash.Test.ReactorDestroyTest do test "it can destroy a post" do defmodule SimpleDestroyPostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :post destroy :delete_post, Post, :destroy do - initial(input(:post)) + initial input(:post) end end - {:ok, original_post} = - Post.create(%{title: "Title", sub_title: "Sub-title"}) + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) assert {:ok, :ok} = - Reactor.run(SimpleDestroyPostReactor, %{post: original_post}, %{}, async?: false) + Reactor.run( + SimpleDestroyPostReactor, + %{post: original_post}, + %{}, + async?: false + ) end test "it can destroy and return a post" do defmodule ReturningDestroyPostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :post destroy :delete_post, Post, :destroy do - initial(input(:post)) - return_destroyed?(true) + initial input(:post) + return_destroyed? true end end - {:ok, original_post} = - Post.create(%{title: "Title", sub_title: "Sub-title"}) + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) assert {:ok, post} = - Reactor.run(ReturningDestroyPostReactor, %{post: original_post}, %{}, async?: false) + Reactor.run( + ReturningDestroyPostReactor, + %{post: original_post}, + %{}, + async?: false + ) assert original_post.__struct__ == post.__struct__ assert original_post.id == post.id @@ -107,17 +120,13 @@ defmodule Ash.Test.ReactorDestroyTest do @moduledoc false use Ash.Reactor - ash do - default_domain(Domain) - end - input :post destroy :delete_post, Post, :destroy do - initial(input(:post)) + initial input(:post) undo :always - undo_action(:undo_destroy) + undo_action :undo_destroy end step :fail do @@ -129,7 +138,7 @@ defmodule Ash.Test.ReactorDestroyTest do end end - {:ok, post} = Post.create(%{title: "Title"}) + post = Post.create!(%{title: "Title"}) assert {:error, _} = Reactor.run( @@ -141,4 +150,112 @@ defmodule Ash.Test.ReactorDestroyTest do assert Post.get(post.id) end + + test "it can provide notification metadata" do + defmodule DestroyWithNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :post + + destroy :delete_post, Post, :destroy do + initial input(:post) + return_destroyed? true + notification_metadata value(%{source: "reactor", operation: "destroy"}) + end + end + + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) + + assert {:ok, post} = + Reactor.run( + DestroyWithNotificationMetadataReactor, + %{post: original_post}, + %{}, + async?: false + ) + + assert post.__meta__.state == :deleted + + # Check the destroy notification has metadata + assert_receive {:notification, + %Ash.Notifier.Notification{ + action: %{type: :destroy}, + metadata: %{source: "reactor", operation: "destroy"} + }} + end + + test "it can provide notification metadata from a template" do + defmodule DestroyWithTemplateNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :post + input :metadata + + destroy :delete_post, Post, :destroy do + initial input(:post) + return_destroyed? true + notification_metadata input(:metadata) + end + end + + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) + metadata = %{reason: "cleanup", batch_id: "batch_999"} + + assert {:ok, post} = + Reactor.run( + DestroyWithTemplateNotificationMetadataReactor, + %{post: original_post, metadata: metadata}, + %{}, + async?: false + ) + + assert post.__meta__.state == :deleted + + # Check the destroy notification has metadata + assert_receive {:notification, + %Ash.Notifier.Notification{ + action: %{type: :destroy}, + metadata: ^metadata + }} + end + + test "it can transform notification metadata" do + defmodule DestroyWithTransformedNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :post + input :metadata_input + + destroy :delete_post, Post, :destroy do + initial input(:post) + return_destroyed? true + + notification_metadata input(:metadata_input) do + transform &Map.put(&1, :total_operations, &1.deleted_count + 10) + end + end + end + + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) + + assert {:ok, post} = + Reactor.run( + DestroyWithTransformedNotificationMetadataReactor, + %{post: original_post, metadata_input: %{deleted_count: 1}}, + %{}, + async?: false + ) + + assert post.__meta__.state == :deleted + + # Check the destroy notification has transformed metadata + assert_receive {:notification, + %Ash.Notifier.Notification{ + action: %{type: :destroy}, + metadata: %{deleted_count: 1, total_operations: 11} + }} + end end diff --git a/test/reactor/update_test.exs b/test/reactor/update_test.exs index 833843db3..72702e1c8 100644 --- a/test/reactor/update_test.exs +++ b/test/reactor/update_test.exs @@ -8,9 +8,22 @@ defmodule Ash.Test.ReactorUpdateTest do alias Ash.Test.Domain + defmodule TestNotifier do + @moduledoc false + use Ash.Notifier + + def notify(notification) do + send(self(), {:notification, notification}) + :ok + end + end + defmodule Post do @moduledoc false - use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain + use Ash.Resource, + data_layer: Ash.DataLayer.Ets, + domain: Domain, + notifiers: [TestNotifier] ets do private? true @@ -55,49 +68,45 @@ defmodule Ash.Test.ReactorUpdateTest do test "it can update a post" do defmodule SimpleUpdatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :post update :publish_post, Post, :update do - initial(input(:post)) - inputs(%{published: value(true)}) + initial input(:post) + inputs %{published: value(true)} end end - {:ok, %{published: false} = original_post} = - Post.create(%{title: "Title", sub_title: "Sub-title"}) + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) + assert original_post.published == false assert {:ok, post} = - Reactor.run(SimpleUpdatePostReactor, %{post: original_post}, %{}, async?: false) + Reactor.run( + SimpleUpdatePostReactor, + %{post: original_post}, + %{}, + async?: false + ) - assert post.published + assert post.published == true end test "it defaults to the primary action when the action is not supplied" do defmodule InferredActionNameUpdatePostReactor do @moduledoc false - use Reactor, extensions: [Ash.Reactor] - - ash do - default_domain(Domain) - end + use Ash.Reactor input :post input :new_title update :update_post, Post do - inputs(%{title: input(:new_title)}) - initial(input(:post)) + inputs %{title: input(:new_title)} + initial input(:post) end end - {:ok, original_post} = - Post.create(%{title: "Title", sub_title: "Sub-title"}) + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) assert {:ok, post} = Reactor.run( @@ -115,28 +124,27 @@ defmodule Ash.Test.ReactorUpdateTest do @moduledoc false use Ash.Reactor, extensions: [Ash.Reactor] - ash do - default_domain(Domain) - end - input :post input :new_title input :new_sub_title update :update_post, Post, :update do - initial(input(:post)) - inputs(%{title: input(:new_title)}) - inputs(%{sub_title: input(:new_sub_title)}) + initial input(:post) + inputs %{title: input(:new_title)} + inputs %{sub_title: input(:new_sub_title)} end end - {:ok, original_post} = - Post.create(%{title: "Title", sub_title: "Sub-title"}) + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) assert {:ok, post} = Reactor.run( MergedInputsCreatePostReactor, - %{post: original_post, new_title: "New Title", new_sub_title: "New Sub-title"}, + %{ + post: original_post, + new_title: "New Title", + new_sub_title: "New Sub-title" + }, %{}, async?: false ) @@ -150,16 +158,12 @@ defmodule Ash.Test.ReactorUpdateTest do @moduledoc false use Ash.Reactor - ash do - default_domain(Domain) - end - input :post input :new_title update :update_post, Post, :update do - initial(input(:post)) - inputs(%{title: input(:new_title)}) + initial input(:post) + inputs %{title: input(:new_title)} undo :always undo_action(:undo_update_post) end @@ -175,18 +179,136 @@ defmodule Ash.Test.ReactorUpdateTest do {:ok, post} = Post.create(%{title: "Title"}) - assert {:error, _} = + Reactor.run( + UndoingUpdateReactor, + %{post: post, new_title: "New title"}, + %{}, + async?: false + ) + |> Ash.Test.assert_has_error(fn + %Reactor.Error.Invalid.RunStepError{error: %RuntimeError{message: "hell"}} -> + true + + _ -> + false + end) + + post_run_post = Post.get!(post.id) + assert post_run_post.title == "Title" + end + + test "it can provide notification metadata" do + defmodule UpdateWithNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :post + input :new_title + + update :update_post, Post, :update do + initial input(:post) + inputs %{title: input(:new_title)} + notification_metadata value(%{source: "reactor", operation: "update"}) + end + end + + {:ok, original_post} = Post.create(%{title: "Title", sub_title: "Sub-title"}) + + assert {:ok, post} = + Reactor.run( + UpdateWithNotificationMetadataReactor, + %{post: original_post, new_title: "Updated Title"}, + %{}, + async?: false + ) + + assert post.title == "Updated Title" + + # Check the update notification has metadata + assert_receive {:notification, + %Ash.Notifier.Notification{ + action: %{type: :update}, + metadata: %{source: "reactor", operation: "update"} + }} + end + + test "it can provide notification metadata from a template" do + defmodule UpdateWithTemplateNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :post + input :new_title + input :metadata + + update :update_post, Post, :update do + initial input(:post) + inputs %{title: input(:new_title)} + notification_metadata input(:metadata) + end + end + + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) + metadata = %{request_id: "req_789", user_id: "user_xyz"} + + assert {:ok, post} = + Reactor.run( + UpdateWithTemplateNotificationMetadataReactor, + %{post: original_post, new_title: "Updated Title", metadata: metadata}, + %{}, + async?: false + ) + + assert post.title == "Updated Title" + + # Check the update notification has metadata + assert_receive {:notification, + %Ash.Notifier.Notification{ + action: %{type: :update}, + metadata: ^metadata + }} + end + + test "it can transform notification metadata" do + defmodule UpdateWithTransformedNotificationMetadataReactor do + @moduledoc false + use Ash.Reactor + + input :post + input :new_title + input :metadata + + update :update_post, Post, :update do + initial input(:post) + inputs %{title: input(:new_title)} + + notification_metadata input(:metadata) do + transform &Map.put(&1, :next_version, &1.version + 1) + end + end + end + + original_post = Post.create!(%{title: "Title", sub_title: "Sub-title"}) + + assert {:ok, post} = Reactor.run( - UndoingUpdateReactor, + UpdateWithTransformedNotificationMetadataReactor, %{ - post: post, - new_title: "New title" + post: original_post, + new_title: "Updated Title", + metadata: %{version: 1} }, %{}, async?: false ) - post_run_post = Post.get!(post.id) - assert post_run_post.title == "Title" + assert post.title == "Updated Title" + + # Check the update notification has transformed metadata + assert_receive {:notification, + %Ash.Notifier.Notification{ + action: %{type: :update}, + metadata: %{version: 1, next_version: 2} + }} end end