feat(Ash.Reactor): Add load option to existing actions, and add new step type. (#1435)

This commit is contained in:
James Harton 2024-09-05 13:59:22 +12:00 committed by GitHub
parent 24cc008410
commit e6a7006c30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 789 additions and 90 deletions

View file

@ -429,6 +429,7 @@ Caveats/differences from `Ash.bulk_create/4`:
### Nested DSLs ### Nested DSLs
* [actor](#reactor-bulk_create-actor) * [actor](#reactor-bulk_create-actor)
* [load](#reactor-bulk_create-load)
* [tenant](#reactor-bulk_create-tenant) * [tenant](#reactor-bulk_create-tenant)
* [wait_for](#reactor-bulk_create-wait_for) * [wait_for](#reactor-bulk_create-wait_for)
@ -461,7 +462,6 @@ end
| [`authorize_changeset_with`](#reactor-bulk_create-authorize_changeset_with){: #reactor-bulk_create-authorize_changeset_with } | `:filter \| :error` | `:filter` | If set to `:error`, instead of filtering unauthorized changes, unauthorized changes will raise an appropriate forbidden error | | [`authorize_changeset_with`](#reactor-bulk_create-authorize_changeset_with){: #reactor-bulk_create-authorize_changeset_with } | `:filter \| :error` | `:filter` | If set to `:error`, instead of filtering unauthorized changes, unauthorized changes will raise an appropriate forbidden error |
| [`authorize_query_with`](#reactor-bulk_create-authorize_query_with){: #reactor-bulk_create-authorize_query_with } | `:filter \| :error` | `:filter` | If set to `:error`, instead of filtering unauthorized query results, unauthorized query results will raise an appropriate forbidden error | | [`authorize_query_with`](#reactor-bulk_create-authorize_query_with){: #reactor-bulk_create-authorize_query_with } | `:filter \| :error` | `:filter` | If set to `:error`, instead of filtering unauthorized query results, unauthorized query results will raise an appropriate forbidden error |
| [`batch_size`](#reactor-bulk_create-batch_size){: #reactor-bulk_create-batch_size } | `nil \| pos_integer` | | The number of records to include in each batch. Defaults to the `default_limit` or `max_page_size` of the action, or 100. | | [`batch_size`](#reactor-bulk_create-batch_size){: #reactor-bulk_create-batch_size } | `nil \| pos_integer` | | The number of records to include in each batch. Defaults to the `default_limit` or `max_page_size` of the action, or 100. |
| [`load`](#reactor-bulk_create-load){: #reactor-bulk_create-load } | `atom \| list(atom)` | `[]` | A load statement to apply to records. Ignored if `return_records?` is not true. |
| [`max_concurrency`](#reactor-bulk_create-max_concurrency){: #reactor-bulk_create-max_concurrency } | `non_neg_integer` | `0` | If set to a value greater than 0, up to that many tasks will be started to run batches asynchronously. | | [`max_concurrency`](#reactor-bulk_create-max_concurrency){: #reactor-bulk_create-max_concurrency } | `non_neg_integer` | `0` | If set to a value greater than 0, up to that many tasks will be started to run batches asynchronously. |
| [`notification_metadata`](#reactor-bulk_create-notification_metadata){: #reactor-bulk_create-notification_metadata } | `map \| Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | `%{}` | Metadata to be merged into the metadata field for all notifications sent from this operation. | | [`notification_metadata`](#reactor-bulk_create-notification_metadata){: #reactor-bulk_create-notification_metadata } | `map \| Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | `%{}` | Metadata to be merged into the metadata field for all notifications sent from this operation. |
| [`notify?`](#reactor-bulk_create-notify?){: #reactor-bulk_create-notify? } | `boolean` | `false` | Whether or not to generate any notifications. This may be intensive for large bulk actions. | | [`notify?`](#reactor-bulk_create-notify?){: #reactor-bulk_create-notify? } | `boolean` | `false` | Whether or not to generate any notifications. This may be intensive for large bulk actions. |
@ -519,6 +519,37 @@ Specifies the action actor
Target: `Ash.Reactor.Dsl.Actor` Target: `Ash.Reactor.Dsl.Actor`
## reactor.bulk_create.load
```elixir
load source
```
Allows the addition of an Ash load statement to the action
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-bulk_create-load-source){: #reactor-bulk_create-load-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the load |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-bulk_create-load-transform){: #reactor-bulk_create-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.ActionLoad`
## reactor.bulk_create.tenant ## reactor.bulk_create.tenant
```elixir ```elixir
tenant source tenant source
@ -660,7 +691,6 @@ end
| [`authorize_query?`](#reactor-bulk_update-authorize_query?){: #reactor-bulk_update-authorize_query? } | `boolean` | `true` | If a query is given, determines whether or not authorization is run on that query. | | [`authorize_query?`](#reactor-bulk_update-authorize_query?){: #reactor-bulk_update-authorize_query? } | `boolean` | `true` | If a query is given, determines whether or not authorization is run on that query. |
| [`batch_size`](#reactor-bulk_update-batch_size){: #reactor-bulk_update-batch_size } | `nil \| pos_integer` | | The number of records to include in each batch. Defaults to the `default_limit` or `max_page_size` of the action, or 100. | | [`batch_size`](#reactor-bulk_update-batch_size){: #reactor-bulk_update-batch_size } | `nil \| pos_integer` | | The number of records to include in each batch. Defaults to the `default_limit` or `max_page_size` of the action, or 100. |
| [`filter`](#reactor-bulk_update-filter){: #reactor-bulk_update-filter } | `map \| keyword` | | A filter to apply to records. This is also applied to a stream of inputs. | | [`filter`](#reactor-bulk_update-filter){: #reactor-bulk_update-filter } | `map \| keyword` | | A filter to apply to records. This is also applied to a stream of inputs. |
| [`load`](#reactor-bulk_update-load){: #reactor-bulk_update-load } | `atom \| list(atom)` | `[]` | A load statement to apply to records. Ignored if `return_records?` is not true. |
| [`lock`](#reactor-bulk_update-lock){: #reactor-bulk_update-lock } | `any` | | A lock statement to add onto the query. | | [`lock`](#reactor-bulk_update-lock){: #reactor-bulk_update-lock } | `any` | | A lock statement to add onto the query. |
| [`max_concurrency`](#reactor-bulk_update-max_concurrency){: #reactor-bulk_update-max_concurrency } | `non_neg_integer` | `0` | If set to a value greater than 0, up to that many tasks will be started to run batches asynchronously. | | [`max_concurrency`](#reactor-bulk_update-max_concurrency){: #reactor-bulk_update-max_concurrency } | `non_neg_integer` | `0` | If set to a value greater than 0, up to that many tasks will be started to run batches asynchronously. |
| [`notification_metadata`](#reactor-bulk_update-notification_metadata){: #reactor-bulk_update-notification_metadata } | `map \| Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | `%{}` | Metadata to be merged into the metadata field for all notifications sent from this operation. | | [`notification_metadata`](#reactor-bulk_update-notification_metadata){: #reactor-bulk_update-notification_metadata } | `map \| Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | `%{}` | Metadata to be merged into the metadata field for all notifications sent from this operation. |
@ -1010,6 +1040,7 @@ Declares a step that will call a create action on a resource.
### Nested DSLs ### Nested DSLs
* [actor](#reactor-create-actor) * [actor](#reactor-create-actor)
* [inputs](#reactor-create-inputs) * [inputs](#reactor-create-inputs)
* [load](#reactor-create-load)
* [tenant](#reactor-create-tenant) * [tenant](#reactor-create-tenant)
* [wait_for](#reactor-create-wait_for) * [wait_for](#reactor-create-wait_for)
@ -1128,6 +1159,37 @@ inputs(author: result(:get_user))
Target: `Ash.Reactor.Dsl.Inputs` Target: `Ash.Reactor.Dsl.Inputs`
## reactor.create.load
```elixir
load source
```
Allows the addition of an Ash load statement to the action
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-create-load-source){: #reactor-create-load-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the load |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-create-load-transform){: #reactor-create-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.ActionLoad`
## reactor.create.tenant ## reactor.create.tenant
```elixir ```elixir
tenant source tenant source
@ -1224,6 +1286,7 @@ Declares a step that will call a destroy action on a resource.
### Nested DSLs ### Nested DSLs
* [actor](#reactor-destroy-actor) * [actor](#reactor-destroy-actor)
* [inputs](#reactor-destroy-inputs) * [inputs](#reactor-destroy-inputs)
* [load](#reactor-destroy-load)
* [tenant](#reactor-destroy-tenant) * [tenant](#reactor-destroy-tenant)
* [wait_for](#reactor-destroy-wait_for) * [wait_for](#reactor-destroy-wait_for)
@ -1338,6 +1401,37 @@ inputs(author: result(:get_user))
Target: `Ash.Reactor.Dsl.Inputs` Target: `Ash.Reactor.Dsl.Inputs`
## reactor.destroy.load
```elixir
load source
```
Allows the addition of an Ash load statement to the action
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-destroy-load-source){: #reactor-destroy-load-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the load |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-destroy-load-transform){: #reactor-destroy-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.ActionLoad`
## reactor.destroy.tenant ## reactor.destroy.tenant
```elixir ```elixir
tenant source tenant source
@ -1413,6 +1507,149 @@ Target: `Ash.Reactor.Dsl.Destroy`
## reactor.load
```elixir
load name, records, load
```
Declares a step that will load additional data on a resource.
### Nested DSLs
* [actor](#reactor-load-actor)
* [tenant](#reactor-load-tenant)
* [wait_for](#reactor-load-wait_for)
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`name`](#reactor-load-name){: #reactor-load-name .spark-required} | `atom` | | A unique name for the step. |
| [`records`](#reactor-load-records){: #reactor-load-records .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | The records upon which to add extra loaded data |
| [`load`](#reactor-load-load){: #reactor-load-load .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | An Ash load statement |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`domain`](#reactor-load-domain){: #reactor-load-domain } | `module` | | The Domain to use when calling the action. Defaults to the Domain set on the resource or in the `ash` section. |
| [`async?`](#reactor-load-async?){: #reactor-load-async? } | `boolean` | `true` | When set to true the step will be executed asynchronously via Reactor's `TaskSupervisor`. |
| [`authorize?`](#reactor-load-authorize?){: #reactor-load-authorize? } | `boolean \| nil` | | Explicitly enable or disable authorization for the action. |
| [`description`](#reactor-load-description){: #reactor-load-description } | `String.t` | | A description for the step |
| [`transform`](#reactor-load-transform){: #reactor-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load statement before it is passed to the load. |
| [`lazy?`](#reactor-load-lazy?){: #reactor-load-lazy? } | `boolean` | | If set to true, values will only be loaded if the related value isn't currently loaded. |
| [`reuse_values?`](#reactor-load-reuse_values?){: #reactor-load-reuse_values? } | `boolean` | | Whether calculations are allowed to reuse values that have already been loaded, or must refetch them from the data layer. |
| [`strict?`](#reactor-load-strict?){: #reactor-load-strict? } | `boolean` | | If set to true, only specified attributes will be loaded when passing a list of fields to fetch on a relationship, which allows for more optimized data-fetching. |
## reactor.load.actor
```elixir
actor source
```
Specifies the action actor
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-load-actor-source){: #reactor-load-actor-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the actor. |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-load-actor-transform){: #reactor-load-actor-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the actor before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.Actor`
## reactor.load.tenant
```elixir
tenant source
```
Specifies the action tenant
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-load-tenant-source){: #reactor-load-tenant-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the tenant. |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-load-tenant-transform){: #reactor-load-tenant-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the tenant before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.Tenant`
## reactor.load.wait_for
```elixir
wait_for names
```
Wait for the named step to complete before allowing this one to start.
Desugars to `argument :_, result(step_to_wait_for)`
### Examples
```
wait_for :create_user
```
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`names`](#reactor-load-wait_for-names){: #reactor-load-wait_for-names .spark-required} | `atom \| list(atom)` | | The name of the step to wait for. |
### Introspection
Target: `Reactor.Dsl.WaitFor`
### Introspection
Target: `Ash.Reactor.Dsl.Load`
## reactor.read_one ## reactor.read_one
```elixir ```elixir
read_one name, resource, action \\ nil read_one name, resource, action \\ nil
@ -1424,6 +1661,7 @@ Declares a step that will call a read action on a resource returning a single re
### Nested DSLs ### Nested DSLs
* [actor](#reactor-read_one-actor) * [actor](#reactor-read_one-actor)
* [inputs](#reactor-read_one-inputs) * [inputs](#reactor-read_one-inputs)
* [load](#reactor-read_one-load)
* [tenant](#reactor-read_one-tenant) * [tenant](#reactor-read_one-tenant)
* [wait_for](#reactor-read_one-wait_for) * [wait_for](#reactor-read_one-wait_for)
@ -1533,6 +1771,37 @@ inputs(author: result(:get_user))
Target: `Ash.Reactor.Dsl.Inputs` Target: `Ash.Reactor.Dsl.Inputs`
## reactor.read_one.load
```elixir
load source
```
Allows the addition of an Ash load statement to the action
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-read_one-load-source){: #reactor-read_one-load-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the load |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-read_one-load-transform){: #reactor-read_one-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.ActionLoad`
## reactor.read_one.tenant ## reactor.read_one.tenant
```elixir ```elixir
tenant source tenant source
@ -1619,6 +1888,7 @@ Declares a step that will call a read action on a resource.
### Nested DSLs ### Nested DSLs
* [actor](#reactor-read-actor) * [actor](#reactor-read-actor)
* [inputs](#reactor-read-inputs) * [inputs](#reactor-read-inputs)
* [load](#reactor-read-load)
* [tenant](#reactor-read-tenant) * [tenant](#reactor-read-tenant)
* [wait_for](#reactor-read-wait_for) * [wait_for](#reactor-read-wait_for)
@ -1732,6 +2002,37 @@ inputs(author: result(:get_user))
Target: `Ash.Reactor.Dsl.Inputs` Target: `Ash.Reactor.Dsl.Inputs`
## reactor.read.load
```elixir
load source
```
Allows the addition of an Ash load statement to the action
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-read-load-source){: #reactor-read-load-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the load |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-read-load-transform){: #reactor-read-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.ActionLoad`
## reactor.read.tenant ## reactor.read.tenant
```elixir ```elixir
tenant source tenant source
@ -1900,6 +2201,7 @@ Declares a step that will call an update action on a resource.
### Nested DSLs ### Nested DSLs
* [actor](#reactor-update-actor) * [actor](#reactor-update-actor)
* [inputs](#reactor-update-inputs) * [inputs](#reactor-update-inputs)
* [load](#reactor-update-load)
* [tenant](#reactor-update-tenant) * [tenant](#reactor-update-tenant)
* [wait_for](#reactor-update-wait_for) * [wait_for](#reactor-update-wait_for)
@ -2016,6 +2318,37 @@ inputs(author: result(:get_user))
Target: `Ash.Reactor.Dsl.Inputs` Target: `Ash.Reactor.Dsl.Inputs`
## reactor.update.load
```elixir
load source
```
Allows the addition of an Ash load statement to the action
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`source`](#reactor-update-load-source){: #reactor-update-load-source .spark-required} | `Reactor.Template.Element \| Reactor.Template.Input \| Reactor.Template.Result \| Reactor.Template.Value` | | What to use as the source of the load |
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`transform`](#reactor-update-load-transform){: #reactor-update-load-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the load before it is passed to the action. |
### Introspection
Target: `Ash.Reactor.Dsl.ActionLoad`
## reactor.update.tenant ## reactor.update.tenant
```elixir ```elixir
tenant source tenant source

View file

@ -0,0 +1,6 @@
defimpl Reactor.Argument.Build, for: Ash.Reactor.Dsl.ActionLoad do
@doc false
@impl true
def build(load),
do: {:ok, [%Reactor.Argument{name: :load, source: load.source, transform: load.transform}]}
end

View file

@ -27,6 +27,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.BulkCreate do
[initial, notification_metadata] [initial, notification_metadata]
|> maybe_append(bulk_create.actor) |> maybe_append(bulk_create.actor)
|> maybe_append(bulk_create.tenant) |> maybe_append(bulk_create.tenant)
|> maybe_append(bulk_create.load)
|> Enum.concat(bulk_create.wait_for) |> Enum.concat(bulk_create.wait_for)
action_options = action_options =

View file

@ -28,6 +28,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.BulkUpdate do
arguments arguments
|> maybe_append(bulk_update.actor) |> maybe_append(bulk_update.actor)
|> maybe_append(bulk_update.tenant) |> maybe_append(bulk_update.tenant)
|> maybe_append(bulk_update.load)
|> Enum.concat(bulk_update.wait_for) |> Enum.concat(bulk_update.wait_for)
|> Enum.concat([initial, notification_metadata]) |> Enum.concat([initial, notification_metadata])
@ -45,7 +46,6 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.BulkUpdate do
:batch_size, :batch_size,
:domain, :domain,
:filter, :filter,
:load,
:lock, :lock,
:max_concurrency, :max_concurrency,
:notify?, :notify?,

View file

@ -29,6 +29,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Create do
arguments arguments
|> maybe_append(create.actor) |> maybe_append(create.actor)
|> maybe_append(create.tenant) |> maybe_append(create.tenant)
|> maybe_append(create.load)
|> Enum.concat(create.wait_for) |> Enum.concat(create.wait_for)
|> Enum.concat([initial]) |> Enum.concat([initial])

View file

@ -16,6 +16,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Destroy do
arguments arguments
|> maybe_append(destroy.actor) |> maybe_append(destroy.actor)
|> maybe_append(destroy.tenant) |> maybe_append(destroy.tenant)
|> maybe_append(destroy.load)
|> Enum.concat(destroy.wait_for) |> Enum.concat(destroy.wait_for)
|> Enum.concat([%Argument{name: :initial, source: destroy.initial}]) |> Enum.concat([%Argument{name: :initial, source: destroy.initial}])

View file

@ -0,0 +1,45 @@
defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Load do
@moduledoc false
alias Ash.Reactor.LoadStep
alias Reactor.{Argument, Builder}
import Ash.Reactor.BuilderUtils
@doc false
@impl true
def build(load, reactor) do
with {:ok, reactor} <- ensure_hooked(reactor) do
arguments =
[
Argument.from_template(:records, load.records),
Argument.from_template(:load, load.load, load.transform)
]
|> maybe_append(load.actor)
|> maybe_append(load.tenant)
|> Enum.concat(load.wait_for)
load_options =
load
|> Map.take([:authorize?, :domain, :lazy?, :reuse_values?, :strict?])
|> Enum.reject(&is_nil(elem(&1, 1)))
step_options =
load
|> Map.take([:async?])
|> Map.put(:ref, :step_name)
|> Enum.to_list()
Builder.add_step(
reactor,
load.name,
{LoadStep, load_options},
arguments,
step_options
)
end
end
@doc false
@impl true
def verify(_load, _dsl_state), do: :ok
end

View file

@ -6,11 +6,13 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Read do
@doc false @doc false
@impl true @impl true
def build(read, reactor) do def build(read, reactor) do
with {:ok, reactor, arguments} <- build_input_arguments(reactor, read) do with {:ok, reactor} <- ensure_hooked(reactor),
{:ok, reactor, arguments} <- build_input_arguments(reactor, read) do
arguments = arguments =
arguments arguments
|> maybe_append(read.actor) |> maybe_append(read.actor)
|> maybe_append(read.tenant) |> maybe_append(read.tenant)
|> maybe_append(read.load)
|> Enum.concat(read.wait_for) |> Enum.concat(read.wait_for)
action_options = action_options =

View file

@ -6,11 +6,13 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.ReadOne do
@doc false @doc false
@impl true @impl true
def build(read_one, reactor) do def build(read_one, reactor) do
with {:ok, reactor, arguments} <- build_input_arguments(reactor, read_one) do with {:ok, reactor} <- ensure_hooked(reactor),
{:ok, reactor, arguments} <- build_input_arguments(reactor, read_one) do
arguments = arguments =
arguments arguments
|> maybe_append(read_one.actor) |> maybe_append(read_one.actor)
|> maybe_append(read_one.tenant) |> maybe_append(read_one.tenant)
|> maybe_append(read_one.load)
|> Enum.concat(read_one.wait_for) |> Enum.concat(read_one.wait_for)
action_options = action_options =

View file

@ -16,6 +16,7 @@ defimpl Reactor.Dsl.Build, for: Ash.Reactor.Dsl.Update do
arguments arguments
|> maybe_append(update.actor) |> maybe_append(update.actor)
|> maybe_append(update.tenant) |> maybe_append(update.tenant)
|> maybe_append(update.load)
|> Enum.concat(update.wait_for) |> Enum.concat(update.wait_for)
|> Enum.concat([%Argument{name: :initial, source: update.initial}]) |> Enum.concat([%Argument{name: :initial, source: update.initial}])

View file

@ -25,7 +25,7 @@ defmodule Ash.Reactor.Dsl.Action do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
async?: boolean, async?: boolean,
authorize?: boolean | nil, authorize?: boolean | nil,
description: String.t() | nil, description: String.t() | nil,
@ -33,7 +33,7 @@ defmodule Ash.Reactor.Dsl.Action do
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
name: atom, name: atom,
resource: module, resource: module,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
type: :action, type: :action,
undo_action: atom, undo_action: atom,
undo: :always | :never | :outside_transaction, undo: :always | :never | :outside_transaction,

View file

@ -0,0 +1,43 @@
defmodule Ash.Reactor.Dsl.ActionLoad do
@moduledoc """
Add a load statement to an action.
"""
defstruct __identifier__: nil, source: nil, transform: nil
alias Reactor.Template
require Template
@type t :: %__MODULE__{
__identifier__: any,
source: Template.t(),
transform: nil | (any -> any) | {module, keyword} | mfa
}
@doc false
def __entity__ do
%Spark.Dsl.Entity{
name: :load,
describe: "Allows the addition of an Ash load statement to the action",
args: [:source],
imports: [Reactor.Dsl.Argument],
identifier: {:auto, :unique_integer},
target: __MODULE__,
schema: [
source: [
type: Template.type(),
required: true,
doc: "What to use as the source of the load"
],
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 load before it is passed to the action."
]
]
}
end
end

View file

@ -55,6 +55,11 @@ defmodule Ash.Reactor.Dsl.ActionTransformer do
end end
end end
defp transform_step(entity, dsl_state) when entity.type == :load do
default_domain = Transformer.get_option(dsl_state, [:ash], :default_domain)
{:ok, %{entity | domain: entity.domain || default_domain}, dsl_state}
end
defp transform_step(_entity, _dsl_state), do: :ignore defp transform_step(_entity, _dsl_state), do: :ignore
defp transform_nested_steps(entity, dsl_state) when is_list(entity.steps) do defp transform_nested_steps(entity, dsl_state) when is_list(entity.steps) do

View file

@ -9,7 +9,7 @@ defmodule Ash.Reactor.Dsl.Actor do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
__identifier__: any, __identifier__: any,
source: Template.Input.t() | Template.Result.t() | Template.Value.t(), source: Template.t(),
transform: nil | (any -> any) | {module, keyword} | mfa transform: nil | (any -> any) | {module, keyword} | mfa
} }

View file

@ -16,7 +16,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
description: nil, description: nil,
domain: nil, domain: nil,
initial: nil, initial: nil,
load: [], load: nil,
max_concurrency: 0, max_concurrency: 0,
name: nil, name: nil,
notification_metadata: %{}, notification_metadata: %{},
@ -48,7 +48,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
assume_casted?: boolean, assume_casted?: boolean,
async?: boolean, async?: boolean,
authorize_changeset_with: :filter | :error, authorize_changeset_with: :filter | :error,
@ -58,7 +58,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
description: String.t() | nil, description: String.t() | nil,
domain: Ash.Domain.t(), domain: Ash.Domain.t(),
initial: Reactor.Template.t(), initial: Reactor.Template.t(),
load: [atom], load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
max_concurrency: non_neg_integer(), max_concurrency: non_neg_integer(),
name: atom, name: atom,
notification_metadata: map, notification_metadata: map,
@ -74,7 +74,7 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
sorted?: boolean, sorted?: boolean,
stop_on_error?: boolean, stop_on_error?: boolean,
success_state: :success | :partial_success, success_state: :success | :partial_success,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
timeout: nil | timeout, timeout: nil | timeout,
transaction: :all | :batch | false, transaction: :all | :batch | false,
type: :bulk_create, type: :bulk_create,
@ -119,10 +119,11 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
imports: [Reactor.Dsl.Argument], imports: [Reactor.Dsl.Argument],
entities: [ entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()], actor: [Ash.Reactor.Dsl.Actor.__entity__()],
load: [Ash.Reactor.Dsl.ActionLoad.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()] wait_for: [Reactor.Dsl.WaitFor.__entity__()]
], ],
singleton_entity_keys: [:actor, :tenant], singleton_entity_keys: [:actor, :tenant, :load],
recursive_as: :steps, recursive_as: :steps,
schema: schema:
[ [
@ -159,13 +160,6 @@ defmodule Ash.Reactor.Dsl.BulkCreate do
doc: doc:
"A collection of inputs to pass to the create action. Must implement the `Enumerable` protocol." "A collection of inputs to pass to the create action. Must implement the `Enumerable` protocol."
], ],
load: [
type: {:wrap_list, :atom},
doc:
"A load statement to apply to records. Ignored if `return_records?` is not true.",
required: false,
default: []
],
max_concurrency: [ max_concurrency: [
type: :non_neg_integer, type: :non_neg_integer,
doc: doc:

View file

@ -21,7 +21,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
filter: %{}, filter: %{},
initial: nil, initial: nil,
inputs: [], inputs: [],
load: [], load: nil,
lock: nil, lock: nil,
max_concurrency: 0, max_concurrency: 0,
name: nil, name: nil,
@ -56,7 +56,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
allow_stream_with: :keyset | :offset | :full_read, allow_stream_with: :keyset | :offset | :full_read,
assume_casted?: boolean, assume_casted?: boolean,
async?: boolean, async?: boolean,
@ -73,7 +73,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
| Keyword.t(Keyword.t(String.t() | number | boolean)), | Keyword.t(Keyword.t(String.t() | number | boolean)),
initial: Reactor.Template.t(), initial: Reactor.Template.t(),
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
load: [atom], load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
lock: nil | Ash.DataLayer.lock_type(), lock: nil | Ash.DataLayer.lock_type(),
max_concurrency: non_neg_integer(), max_concurrency: non_neg_integer(),
name: atom, name: atom,
@ -95,7 +95,7 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
stream_batch_size: nil | pos_integer(), stream_batch_size: nil | pos_integer(),
stream_with: nil | :keyset | :offset | :full_read, stream_with: nil | :keyset | :offset | :full_read,
success_state: :success | :partial_success, success_state: :success | :partial_success,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
timeout: nil | timeout, timeout: nil | timeout,
transaction: :all | :batch | false, transaction: :all | :batch | false,
type: :bulk_create, type: :bulk_create,
@ -204,13 +204,6 @@ defmodule Ash.Reactor.Dsl.BulkUpdate do
doc: doc:
"A collection of inputs to pass to the create action. Must implement the `Enumerable` protocol." "A collection of inputs to pass to the create action. Must implement the `Enumerable` protocol."
], ],
load: [
type: {:wrap_list, :atom},
doc:
"A load statement to apply to records. Ignored if `return_records?` is not true.",
required: false,
default: []
],
lock: [ lock: [
type: :any, type: :any,
doc: "A lock statement to add onto the query.", doc: "A lock statement to add onto the query.",

View file

@ -13,6 +13,7 @@ defmodule Ash.Reactor.Dsl.Create do
domain: nil, domain: nil,
initial: nil, initial: nil,
inputs: [], inputs: [],
load: nil,
name: nil, name: nil,
resource: nil, resource: nil,
tenant: [], tenant: [],
@ -28,16 +29,17 @@ defmodule Ash.Reactor.Dsl.Create do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
async?: boolean, async?: boolean,
authorize?: boolean | nil, authorize?: boolean | nil,
description: String.t() | nil, description: String.t() | nil,
domain: Ash.Domain.t(), domain: Ash.Domain.t(),
initial: nil | Ash.Resource.t() | Reactor.Template.t(), initial: nil | Ash.Resource.t() | Reactor.Template.t(),
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
name: atom, name: atom,
resource: module, resource: module,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
type: :create, type: :create,
undo_action: atom, undo_action: atom,
undo: :always | :never | :outside_transaction, undo: :always | :never | :outside_transaction,
@ -75,10 +77,11 @@ defmodule Ash.Reactor.Dsl.Create do
entities: [ entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()], actor: [Ash.Reactor.Dsl.Actor.__entity__()],
inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()],
load: [Ash.Reactor.Dsl.ActionLoad.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()] wait_for: [Reactor.Dsl.WaitFor.__entity__()]
], ],
singleton_entity_keys: [:actor, :tenant], singleton_entity_keys: [:actor, :tenant, :load],
recursive_as: :steps, recursive_as: :steps,
schema: schema:
[ [

View file

@ -13,6 +13,7 @@ defmodule Ash.Reactor.Dsl.Destroy do
domain: nil, domain: nil,
initial: nil, initial: nil,
inputs: [], inputs: [],
load: nil,
name: nil, name: nil,
resource: nil, resource: nil,
return_destroyed?: false, return_destroyed?: false,
@ -27,17 +28,18 @@ defmodule Ash.Reactor.Dsl.Destroy do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
async?: boolean, async?: boolean,
authorize?: boolean | nil, authorize?: boolean | nil,
description: String.t() | nil, description: String.t() | nil,
domain: Ash.Domain.t(), domain: Ash.Domain.t(),
initial: Reactor.Template.t(), initial: Reactor.Template.t(),
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
name: atom, name: atom,
resource: module, resource: module,
return_destroyed?: boolean, return_destroyed?: boolean,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
type: :destroy, type: :destroy,
undo_action: atom, undo_action: atom,
undo: :always | :never | :outside_transaction, undo: :always | :never | :outside_transaction,
@ -70,10 +72,11 @@ defmodule Ash.Reactor.Dsl.Destroy do
entities: [ entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()], actor: [Ash.Reactor.Dsl.Actor.__entity__()],
inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()],
load: [Ash.Reactor.Dsl.ActionLoad.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()] wait_for: [Reactor.Dsl.WaitFor.__entity__()]
], ],
singleton_entity_keys: [:actor, :tenant], singleton_entity_keys: [:actor, :tenant, :load],
recursive_as: :steps, recursive_as: :steps,
schema: schema:
[ [

104
lib/ash/reactor/dsl/load.ex Normal file
View file

@ -0,0 +1,104 @@
defmodule Ash.Reactor.Dsl.Load do
@moduledoc """
The `load` step entity for the `Ash.Reactor` reactor extension.
"""
defstruct __identifier__: nil,
action_step?: false,
action: nil,
actor: nil,
async?: true,
authorize?: nil,
description: nil,
domain: nil,
lazy?: nil,
load: nil,
name: nil,
records: nil,
reuse_values?: nil,
strict?: nil,
tenant: nil,
transform: nil,
type: :load,
wait_for: []
@type t :: %__MODULE__{
__identifier__: any,
action: nil | atom,
action_step?: false,
actor: nil | Ash.Reactor.Dsl.Actor.t(),
async?: boolean,
authorize?: nil | boolean,
description: nil | String.t(),
domain: nil | Ash.Domain.t(),
lazy?: nil | boolean,
load: Reactor.Template.t(),
name: atom,
records: Reactor.Template.t(),
reuse_values?: nil | boolean,
strict?: nil | boolean,
tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
transform: nil | (any -> any) | {module, keyword} | mfa,
type: :load,
wait_for: [Reactor.Dsl.WaitFor.t()]
}
@doc false
def __entity__,
do: %Spark.Dsl.Entity{
name: :load,
describe: "Declares a step that will load additional data on a resource.",
target: __MODULE__,
args: [:name, :records, :load],
imports: [Reactor.Dsl.Argument],
entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()]
],
singleton_entity_keys: [:actor, :tenant],
recursive_as: :steps,
schema:
false
|> Ash.Reactor.Dsl.Action.__shared_action_option_schema__()
|> Keyword.take([:domain, :async?, :authorize?, :description, :name])
|> Keyword.merge(
records: [
type: Reactor.Template.type(),
required: true,
doc: "The records upon which to add extra loaded data"
],
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 load statement before it is passed to the load."
],
load: [
type: Reactor.Template.type(),
required: true,
doc: "An Ash load statement"
],
lazy?: [
type: :boolean,
required: false,
doc:
"If set to true, values will only be loaded if the related value isn't currently loaded."
],
reuse_values?: [
type: :boolean,
required: false,
doc:
"Whether calculations are allowed to reuse values that have already been loaded, or must refetch them from the data layer."
],
strict?: [
type: :boolean,
required: false,
doc:
"If set to true, only specified attributes will be loaded when passing a list of fields to fetch on a relationship, which allows for more optimized data-fetching."
]
)
}
end

View file

@ -6,15 +6,16 @@ defmodule Ash.Reactor.Dsl.Read do
defstruct __identifier__: nil, defstruct __identifier__: nil,
action_step?: true, action_step?: true,
action: nil, action: nil,
actor: [], actor: nil,
domain: nil, domain: nil,
async?: true, async?: true,
authorize?: nil, authorize?: nil,
description: nil, description: nil,
inputs: [], inputs: [],
load: nil,
name: nil, name: nil,
resource: nil, resource: nil,
tenant: [], tenant: nil,
transform: nil, transform: nil,
type: :read, type: :read,
wait_for: [] wait_for: []
@ -23,15 +24,16 @@ defmodule Ash.Reactor.Dsl.Read do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
domain: Ash.Domain.t(), domain: Ash.Domain.t(),
async?: boolean, async?: boolean,
authorize?: boolean | nil, authorize?: boolean | nil,
description: String.t() | nil, description: String.t() | nil,
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
name: atom, name: atom,
resource: module, resource: module,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
type: :create, type: :create,
wait_for: [Reactor.Dsl.WaitFor.t()] wait_for: [Reactor.Dsl.WaitFor.t()]
} }
@ -59,10 +61,11 @@ defmodule Ash.Reactor.Dsl.Read do
entities: [ entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()], actor: [Ash.Reactor.Dsl.Actor.__entity__()],
inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()],
load: [Ash.Reactor.Dsl.ActionLoad.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()] wait_for: [Reactor.Dsl.WaitFor.__entity__()]
], ],
singleton_entity_keys: [:actor, :tenant], singleton_entity_keys: [:actor, :tenant, :load],
recursive_as: :steps, recursive_as: :steps,
schema: Ash.Reactor.Dsl.Action.__shared_action_option_schema__(false) schema: Ash.Reactor.Dsl.Action.__shared_action_option_schema__(false)
} }

View file

@ -13,6 +13,7 @@ defmodule Ash.Reactor.Dsl.ReadOne do
domain: nil, domain: nil,
fail_on_not_found?: nil, fail_on_not_found?: nil,
inputs: [], inputs: [],
load: nil,
name: nil, name: nil,
resource: nil, resource: nil,
tenant: [], tenant: [],
@ -24,16 +25,17 @@ defmodule Ash.Reactor.Dsl.ReadOne do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
async?: boolean, async?: boolean,
authorize?: boolean | nil, authorize?: boolean | nil,
description: String.t() | nil, description: String.t() | nil,
domain: Ash.Domain.t(), domain: Ash.Domain.t(),
fail_on_not_found?: boolean, fail_on_not_found?: boolean,
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
name: atom, name: atom,
resource: module, resource: module,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
type: :create, type: :create,
wait_for: [Reactor.Dsl.WaitFor.t()] wait_for: [Reactor.Dsl.WaitFor.t()]
} }
@ -59,10 +61,11 @@ defmodule Ash.Reactor.Dsl.ReadOne do
entities: [ entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()], actor: [Ash.Reactor.Dsl.Actor.__entity__()],
inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()],
load: [Ash.Reactor.Dsl.ActionLoad.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()] wait_for: [Reactor.Dsl.WaitFor.__entity__()]
], ],
singleton_entity_keys: [:actor, :tenant], singleton_entity_keys: [:actor, :tenant, :load],
recursive_as: :steps, recursive_as: :steps,
schema: schema:
[ [

View file

@ -1,6 +1,6 @@
defmodule Ash.Reactor.Dsl.Tenant do defmodule Ash.Reactor.Dsl.Tenant do
@moduledoc """ @moduledoc """
Specify the actor used to execute an action. Specify the tenant used to execute an action.
""" """
defstruct __identifier__: nil, source: nil, transform: nil defstruct __identifier__: nil, source: nil, transform: nil
@ -9,7 +9,7 @@ defmodule Ash.Reactor.Dsl.Tenant do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
__identifier__: any, __identifier__: any,
source: Template.Input.t() | Template.Result.t() | Template.Value.t(), source: Template.t(),
transform: nil | (any -> any) | {module, keyword} | mfa transform: nil | (any -> any) | {module, keyword} | mfa
} }

View file

@ -13,6 +13,7 @@ defmodule Ash.Reactor.Dsl.Update do
domain: nil, domain: nil,
initial: nil, initial: nil,
inputs: [], inputs: [],
load: nil,
name: nil, name: nil,
resource: nil, resource: nil,
tenant: [], tenant: [],
@ -26,16 +27,17 @@ defmodule Ash.Reactor.Dsl.Update do
__identifier__: any, __identifier__: any,
action_step?: true, action_step?: true,
action: atom, action: atom,
actor: [Ash.Reactor.Dsl.Actor.t()], actor: nil | Ash.Reactor.Dsl.Actor.t(),
async?: boolean, async?: boolean,
authorize?: boolean | nil, authorize?: boolean | nil,
description: String.t() | nil, description: String.t() | nil,
domain: Ash.Domain.t(), domain: Ash.Domain.t(),
initial: Reactor.Template.t(), initial: Reactor.Template.t(),
inputs: [Ash.Reactor.Dsl.Inputs.t()], inputs: [Ash.Reactor.Dsl.Inputs.t()],
load: nil | Ash.Reactor.Dsl.ActionLoad.t(),
name: atom, name: atom,
resource: module, resource: module,
tenant: [Ash.Reactor.Dsl.Tenant.t()], tenant: nil | Ash.Reactor.Dsl.Tenant.t(),
type: :update, type: :update,
undo_action: atom, undo_action: atom,
undo: :always | :never | :outside_transaction, undo: :always | :never | :outside_transaction,
@ -71,10 +73,11 @@ defmodule Ash.Reactor.Dsl.Update do
entities: [ entities: [
actor: [Ash.Reactor.Dsl.Actor.__entity__()], actor: [Ash.Reactor.Dsl.Actor.__entity__()],
inputs: [Ash.Reactor.Dsl.Inputs.__entity__()], inputs: [Ash.Reactor.Dsl.Inputs.__entity__()],
load: [Ash.Reactor.Dsl.ActionLoad.__entity__()],
tenant: [Ash.Reactor.Dsl.Tenant.__entity__()], tenant: [Ash.Reactor.Dsl.Tenant.__entity__()],
wait_for: [Reactor.Dsl.WaitFor.__entity__()] wait_for: [Reactor.Dsl.WaitFor.__entity__()]
], ],
singleton_entity_keys: [:actor, :tenant], singleton_entity_keys: [:actor, :tenant, :load],
recursive_as: :steps, recursive_as: :steps,
schema: schema:
[ [

View file

@ -25,6 +25,7 @@ defmodule Ash.Reactor do
| Ash.Reactor.Dsl.BulkUpdate.t() | Ash.Reactor.Dsl.BulkUpdate.t()
| Ash.Reactor.Dsl.Create.t() | Ash.Reactor.Dsl.Create.t()
| Ash.Reactor.Dsl.Destroy.t() | Ash.Reactor.Dsl.Destroy.t()
| Ash.Reactor.Dsl.Load.t()
| Ash.Reactor.Dsl.Read.t() | Ash.Reactor.Dsl.Read.t()
| Ash.Reactor.Dsl.ReadOne.t() | Ash.Reactor.Dsl.ReadOne.t()
| Ash.Reactor.Dsl.Update.t() | Ash.Reactor.Dsl.Update.t()
@ -41,6 +42,7 @@ defmodule Ash.Reactor do
Ash.Reactor.Dsl.Change, Ash.Reactor.Dsl.Change,
Ash.Reactor.Dsl.Create, Ash.Reactor.Dsl.Create,
Ash.Reactor.Dsl.Destroy, Ash.Reactor.Dsl.Destroy,
Ash.Reactor.Dsl.Load,
Ash.Reactor.Dsl.ReadOne, Ash.Reactor.Dsl.ReadOne,
Ash.Reactor.Dsl.Read, Ash.Reactor.Dsl.Read,
Ash.Reactor.Dsl.Transaction, Ash.Reactor.Dsl.Transaction,

View file

@ -21,12 +21,12 @@ defmodule Ash.Reactor.ActionStep do
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
action_options = action_options =
[] [domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
options[:resource] options[:resource]
|> ActionInput.for_action(options[:action], arguments[:input], action_input_options) |> ActionInput.for_action(options[:action], arguments[:input], action_input_options)
|> options[:domain].run_action(action_options) |> Ash.run_action(action_options)
end end
@doc false @doc false
@ -45,6 +45,7 @@ defmodule Ash.Reactor.ActionStep do
action_options = action_options =
[] []
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
|> maybe_set_kw(:domain, options[:domain])
inputs = inputs =
arguments[:input] arguments[:input]
@ -52,7 +53,7 @@ defmodule Ash.Reactor.ActionStep do
options[:resource] options[:resource]
|> ActionInput.for_action(options[:action], inputs, action_input_options) |> ActionInput.for_action(options[:action], inputs, action_input_options)
|> options[:domain].run_action(action_options) |> Ash.run_action(action_options)
end end
@doc false @doc false

View file

@ -19,7 +19,6 @@ defmodule Ash.Reactor.BulkCreateStep do
:authorize?, :authorize?,
:batch_size, :batch_size,
:domain, :domain,
:load,
:max_concurrency, :max_concurrency,
:notify?, :notify?,
:read_action, :read_action,
@ -45,6 +44,7 @@ defmodule Ash.Reactor.BulkCreateStep do
|> maybe_set_kw(:actor, arguments[:actor]) |> maybe_set_kw(:actor, arguments[:actor])
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
|> maybe_set_kw(:notification_metadata, arguments[:notification_metadata]) |> maybe_set_kw(:notification_metadata, arguments[:notification_metadata])
|> maybe_set_kw(:load, arguments[:load])
success_states = success_states =
options[:success_state] options[:success_state]

View file

@ -23,7 +23,6 @@ defmodule Ash.Reactor.BulkUpdateStep do
:batch_size, :batch_size,
:domain, :domain,
:filter, :filter,
:load,
:lock, :lock,
:max_concurrency, :max_concurrency,
:notify?, :notify?,
@ -51,6 +50,7 @@ defmodule Ash.Reactor.BulkUpdateStep do
|> maybe_set_kw(:actor, context[:actor]) |> maybe_set_kw(:actor, context[:actor])
|> maybe_set_kw(:tenant, context[:tenant]) |> maybe_set_kw(:tenant, context[:tenant])
|> maybe_set_kw(:tracer, context[:tracer]) |> maybe_set_kw(:tracer, context[:tracer])
|> maybe_set_kw(:load, arguments[:load])
success_states = success_states =
options[:success_state] options[:success_state]

View file

@ -25,6 +25,7 @@ defmodule Ash.Reactor.CreateStep do
action_options = action_options =
[return_notifications?: true, domain: options[:domain]] [return_notifications?: true, domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
|> maybe_set_kw(:load, arguments[:load])
changeset = changeset =
case arguments.initial do case arguments.initial do

View file

@ -24,13 +24,14 @@ defmodule Ash.Reactor.DestroyStep do
|> maybe_set_kw(:return_destroyed?, return_destroyed?) |> maybe_set_kw(:return_destroyed?, return_destroyed?)
action_options = action_options =
[return_notifications?: true] [return_notifications?: true, domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
|> maybe_set_kw(:return_destroyed?, return_destroyed?) |> maybe_set_kw(:return_destroyed?, return_destroyed?)
|> maybe_set_kw(:load, arguments[:load])
arguments[:initial] arguments[:initial]
|> Changeset.for_destroy(options[:action], arguments[:input], changeset_options) |> Changeset.for_destroy(options[:action], arguments[:input], changeset_options)
|> options[:domain].destroy(action_options) |> Ash.destroy(action_options)
|> case do |> case do
:ok -> :ok ->
{:ok, :ok} {:ok, :ok}
@ -65,12 +66,12 @@ defmodule Ash.Reactor.DestroyStep do
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
action_options = action_options =
[return_notifications?: false] [return_notifications?: false, domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
options[:resource] options[:resource]
|> Changeset.for_create(options[:undo_action], %{record: record}, changeset_options) |> Changeset.for_create(options[:undo_action], %{record: record}, changeset_options)
|> options[:domain].create(action_options) |> Ash.create(action_options)
|> case do |> case do
{:ok, _record} -> :ok {:ok, _record} -> :ok
{:ok, _record, _notifications} -> :ok {:ok, _record, _notifications} -> :ok

View file

@ -0,0 +1,21 @@
defmodule Ash.Reactor.LoadStep do
@moduledoc """
The Reactor step which is used to execute load steps.
"""
use Reactor.Step
import Ash.Reactor.StepUtils
def run(arguments, context, options) do
load_options =
options
|> maybe_set_kw(:authorize?, context[:authorize?])
|> maybe_set_kw(:actor, context[:actor])
|> maybe_set_kw(:tenant, context[:tenant])
|> maybe_set_kw(:tracer, context[:tracer])
|> maybe_set_kw(:actor, arguments[:actor])
|> maybe_set_kw(:tenant, arguments[:tenant])
arguments.records
|> Ash.load(arguments.load, load_options)
end
end

View file

@ -19,28 +19,13 @@ defmodule Ash.Reactor.ReadOneStep do
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
action_options = action_options =
[] [domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
|> maybe_set_kw(:load, arguments[:load])
|> maybe_set_kw(:not_found_error?, options[:fail_on_not_found?])
options[:resource] options[:resource]
|> Query.for_read(options[:action], arguments[:input], query_options) |> Query.for_read(options[:action], arguments[:input], query_options)
|> options[:domain].read_one(action_options) |> Ash.read_one(action_options)
|> case do
{:ok, nil} ->
if options[:fail_on_not_found?] do
raise Ash.Error.Query.NotFound, resource: options[:resource]
else
{:ok, nil}
end
{:ok, record} ->
{:ok, record}
{:ok, records, _} ->
{:ok, records}
{:error, reason} ->
{:error, reason}
end
end end
end end

View file

@ -19,16 +19,12 @@ defmodule Ash.Reactor.ReadStep do
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
action_options = action_options =
[] [domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
|> maybe_set_kw(:load, arguments[:load])
options[:resource] options[:resource]
|> Query.for_read(options[:action], arguments[:input], query_options) |> Query.for_read(options[:action], arguments[:input], query_options)
|> options[:domain].read(action_options) |> Ash.read(action_options)
|> case do
{:ok, records} -> {:ok, records}
{:ok, records, _} -> {:ok, records}
{:error, reason} -> {:error, reason}
end
end end
end end

View file

@ -21,15 +21,16 @@ defmodule Ash.Reactor.UpdateStep do
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
action_options = action_options =
[return_notifications?: true] [return_notifications?: true, domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
|> maybe_set_kw(:load, arguments[:load])
changeset = changeset =
arguments[:initial] arguments[:initial]
|> Changeset.for_update(options[:action], arguments[:input], changeset_options) |> Changeset.for_update(options[:action], arguments[:input], changeset_options)
changeset changeset
|> options[:domain].update(action_options) |> Ash.update(action_options)
|> case do |> case do
{:ok, record} -> {:ok, record} ->
{:ok, store_changeset_in_metadata(context.current_step.name, record, changeset)} {:ok, store_changeset_in_metadata(context.current_step.name, record, changeset)}
@ -57,7 +58,7 @@ defmodule Ash.Reactor.UpdateStep do
|> maybe_set_kw(:tenant, arguments[:tenant]) |> maybe_set_kw(:tenant, arguments[:tenant])
action_options = action_options =
[return_notifications?: false] [return_notifications?: false, domain: options[:domain]]
|> maybe_set_kw(:authorize?, options[:authorize?]) |> maybe_set_kw(:authorize?, options[:authorize?])
attributes = attributes =
@ -65,7 +66,7 @@ defmodule Ash.Reactor.UpdateStep do
record record
|> Changeset.for_update(options[:undo_action], attributes, changeset_options) |> Changeset.for_update(options[:undo_action], attributes, changeset_options)
|> options[:domain].update(action_options) |> Ash.update(action_options)
|> case do |> case do
{:ok, _record} -> :ok {:ok, _record} -> :ok
{:ok, _record, _notifications} -> :ok {:ok, _record, _notifications} -> :ok

View file

@ -0,0 +1,86 @@
defmodule Ash.Test.ReactorLoadTest do
@moduledoc false
use ExUnit.Case, async: true
alias __MODULE__, as: Self
alias Ash.Test.Domain
defmodule Post do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain
ets do
private? true
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false, public?: true
end
relationships do
has_many :comments, Self.Comment, public?: true
end
actions do
default_accept :*
defaults [:read, create: :*]
end
code_interface do
define :create
end
end
defmodule Comment do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain
ets do
private? true
end
attributes do
uuid_primary_key :id
attribute :comment, :string, allow_nil?: false, public?: true
end
relationships do
belongs_to :post, Self.Post, public?: true
end
actions do
default_accept :*
defaults [:read, create: :*]
end
code_interface do
define :create
end
end
defmodule SimpleLoadReactor do
@moduledoc false
use Reactor, extensions: [Ash.Reactor]
ash do
default_domain(Domain)
end
input :post
load(:post_with_comments, input(:post), value(comments: :comment))
end
test "it performs loading" do
post = Post.create!(%{title: "Marty"})
comments = ["This is heavy", "You made a time machine... out of a Delorean?"]
for comment <- comments do
Comment.create!(%{post_id: post.id, comment: comment})
end
assert {:ok, post} = Reactor.run(SimpleLoadReactor, %{post: post}, %{}, async?: false)
assert Enum.sort(Enum.map(post.comments, & &1.comment)) == comments
end
end

View file

@ -2,6 +2,7 @@ defmodule Ash.Test.ReactorReadOneTest do
@moduledoc false @moduledoc false
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias __MODULE__, as: Self
alias Ash.Test.Domain alias Ash.Test.Domain
defmodule Post do defmodule Post do
@ -17,6 +18,37 @@ defmodule Ash.Test.ReactorReadOneTest do
attribute :title, :string, allow_nil?: false, public?: true attribute :title, :string, allow_nil?: false, public?: true
end end
relationships do
has_many :comments, Self.Comment, public?: true
end
actions do
default_accept :*
defaults [:read, create: :*]
end
code_interface do
define :create
end
end
defmodule Comment do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Ets, domain: Domain
ets do
private? true
end
attributes do
uuid_primary_key :id
attribute :comment, :string, allow_nil?: false, public?: true
end
relationships do
belongs_to :post, Self.Post, public?: true
end
actions do actions do
default_accept :* default_accept :*
defaults [:read, create: :*] defaults [:read, create: :*]
@ -74,7 +106,9 @@ defmodule Ash.Test.ReactorReadOneTest do
NotFoundReactor NotFoundReactor
|> Reactor.run(%{}, %{}, async?: false) |> Reactor.run(%{}, %{}, async?: false)
|> Ash.Test.assert_has_error(fn |> Ash.Test.assert_has_error(fn
%Reactor.Error.Invalid.RunStepError{error: %Ash.Error.Query.NotFound{}} -> %Reactor.Error.Invalid.RunStepError{
error: %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{}]}
} ->
true true
_ -> _ ->
@ -88,4 +122,29 @@ defmodule Ash.Test.ReactorReadOneTest do
assert {:ok, actual} = Reactor.run(SimpleReadOneReactor, %{}, %{}, async?: false) assert {:ok, actual} = Reactor.run(SimpleReadOneReactor, %{}, %{}, async?: false)
assert expected.id == actual.id assert expected.id == actual.id
end end
test "it can load related data when asked" do
defmodule LoadRelatedReactor do
@moduledoc false
use Reactor, extensions: [Ash.Reactor]
ash do
default_domain(Domain)
end
read_one :read_one_post, Ash.Test.ReactorReadOneTest.Post, :read do
load value(comments: [:comment])
end
end
post = Post.create!(%{title: "Marty"})
comments = ["This is heavy", "You made a time machine... out of a Delorean?"]
for comment <- comments do
Comment.create!(%{post_id: post.id, comment: comment})
end
assert {:ok, post} = Reactor.run(LoadRelatedReactor, %{}, %{}, async?: false)
assert Enum.sort(Enum.map(post.comments, & &1.comment)) == comments
end
end end