mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
improvement: support depending on requests that will be added
docs: improve flow docs closes: #418 improvement: support dynamic action steps in `Ash.Flow`
This commit is contained in:
parent
652342c41d
commit
6751afc683
7 changed files with 482 additions and 289 deletions
|
@ -65,6 +65,16 @@ end
|
||||||
- `%{step_name: :key}` will return a map of each key to the provided step name, i.e `%{key: <step_name_result>}`
|
- `%{step_name: :key}` will return a map of each key to the provided step name, i.e `%{key: <step_name_result>}`
|
||||||
- `[:step_name]` - which is equivalent to `%{step_name: :step_name}`
|
- `[:step_name]` - which is equivalent to `%{step_name: :step_name}`
|
||||||
|
|
||||||
|
A flow always returns an `%Ash.Flow.Result{}`, and the return value of a successful flow will be available in `%Ash.Flow.Result{result: result}` when the flow did not encounter an error.
|
||||||
|
|
||||||
|
If the flow resulted in an error, `error?` is set to `true`, and the result will be `nil`.
|
||||||
|
|
||||||
|
## Halting and Resuming Flows
|
||||||
|
|
||||||
|
A flow can be halted by using the `halt_if` option on a step, or by a custom step returning `{:error, Ash.Flow.Error.Halted.exception(reason: reason)}`
|
||||||
|
|
||||||
|
In this case, the flow will be marked as `complete?: false`. The result of each step up until this point is saved, and you can then rerun the flow with different inputs by passing the incomplete result into the `resume` option when running the flow again. Individual steps can be rerun by deleting them from the `data` field of the flow.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
Currently, any error anywhere in the flow will fail the flow and will return an error. Over time, error handling behavior will be added, as well as the ability to customize how transactions are rolled back, and to handle errors in a custom way.
|
Currently, any error anywhere in the flow will fail the flow and will return an error. Over time, error handling behavior will be added, as well as the ability to customize how transactions are rolled back, and to handle errors in a custom way.
|
||||||
|
|
|
@ -61,6 +61,7 @@ defmodule Ash.Engine do
|
||||||
opts: [],
|
opts: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
data: %{},
|
data: %{},
|
||||||
|
dependencies_waiting_on_request: MapSet.new(),
|
||||||
unsent_dependencies: [],
|
unsent_dependencies: [],
|
||||||
dependencies_seen: MapSet.new(),
|
dependencies_seen: MapSet.new(),
|
||||||
dependencies: %{},
|
dependencies: %{},
|
||||||
|
@ -466,7 +467,6 @@ defmodule Ash.Engine do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_run_iteration(state, request) do
|
defp do_run_iteration(state, request) do
|
||||||
log(state, fn -> breakdown(state) end)
|
|
||||||
{state, notifications, dependencies} = fully_advance_request(state, request)
|
{state, notifications, dependencies} = fully_advance_request(state, request)
|
||||||
|
|
||||||
state
|
state
|
||||||
|
@ -516,13 +516,6 @@ defmodule Ash.Engine do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp breakdown(state) do
|
|
||||||
"""
|
|
||||||
State breakdown:
|
|
||||||
#{Enum.map_join(state.requests, "\n", &"#{&1.name}: #{&1.state}")}
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def long_breakdown(state) do
|
def long_breakdown(state) do
|
||||||
"""
|
"""
|
||||||
#{errors(state)}
|
#{errors(state)}
|
||||||
|
@ -724,32 +717,36 @@ defmodule Ash.Engine do
|
||||||
|
|
||||||
depended_on_request = Enum.find(state.requests, &(&1.path == dep_path))
|
depended_on_request = Enum.find(state.requests, &(&1.path == dep_path))
|
||||||
|
|
||||||
if !depended_on_request do
|
if depended_on_request do
|
||||||
raise "Engine Error in request #{inspect(request_path)}: No request found with path #{inspect(dep_path)}. Available paths:\n #{Enum.map_join(state.requests, "\n", &inspect(&1.path))}"
|
# we want to send things from non async requests
|
||||||
|
# after we've sent all info to async requests
|
||||||
|
unsent_dependencies =
|
||||||
|
if depended_on_request && depended_on_request.async? do
|
||||||
|
state.unsent_dependencies ++ [{request_path, dep}]
|
||||||
|
else
|
||||||
|
[{request_path, dep} | state.unsent_dependencies]
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| dependencies:
|
||||||
|
state.dependencies
|
||||||
|
|> Map.put_new_lazy(request_path, fn -> MapSet.new() end)
|
||||||
|
|> Map.update!(request_path, &MapSet.put(&1, {dep_path, dep_field})),
|
||||||
|
reverse_dependencies:
|
||||||
|
state.reverse_dependencies
|
||||||
|
|> Map.put_new_lazy(dep_path, fn -> MapSet.new() end)
|
||||||
|
|> Map.update!(dep_path, &MapSet.put(&1, {request_path, dep_field})),
|
||||||
|
dependencies_seen: MapSet.put(state.dependencies_seen, seen_dep),
|
||||||
|
unsent_dependencies: unsent_dependencies
|
||||||
|
}
|
||||||
|
else
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| dependencies_waiting_on_request:
|
||||||
|
MapSet.put(state.dependencies_waiting_on_request, {request_path, dep})
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# we want to send things from non async requests
|
|
||||||
# after we've sent all info to async requests
|
|
||||||
unsent_dependencies =
|
|
||||||
if depended_on_request.async? do
|
|
||||||
state.unsent_dependencies ++ [{request_path, dep}]
|
|
||||||
else
|
|
||||||
[{request_path, dep} | state.unsent_dependencies]
|
|
||||||
end
|
|
||||||
|
|
||||||
%{
|
|
||||||
state
|
|
||||||
| dependencies:
|
|
||||||
state.dependencies
|
|
||||||
|> Map.put_new_lazy(request_path, fn -> MapSet.new() end)
|
|
||||||
|> Map.update!(request_path, &MapSet.put(&1, {dep_path, dep_field})),
|
|
||||||
reverse_dependencies:
|
|
||||||
state.reverse_dependencies
|
|
||||||
|> Map.put_new_lazy(dep_path, fn -> MapSet.new() end)
|
|
||||||
|> Map.update!(dep_path, &MapSet.put(&1, {request_path, dep_field})),
|
|
||||||
dependencies_seen: MapSet.put(state.dependencies_seen, seen_dep),
|
|
||||||
unsent_dependencies: unsent_dependencies
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -830,6 +827,12 @@ defmodule Ash.Engine do
|
||||||
{async, non_async} =
|
{async, non_async} =
|
||||||
requests
|
requests
|
||||||
|> Enum.map(fn request ->
|
|> Enum.map(fn request ->
|
||||||
|
if Enum.find(state.requests, &(&1.path == request.path)) do
|
||||||
|
raise """
|
||||||
|
Attempted to add request #{inspect(request.path)} but it has already been added!
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
authorize? = request.authorize? and state.authorize?
|
authorize? = request.authorize? and state.authorize?
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
@ -847,6 +850,26 @@ defmodule Ash.Engine do
|
||||||
|> Enum.split_with(& &1.async?)
|
|> Enum.split_with(& &1.async?)
|
||||||
|
|
||||||
%{state | requests: async ++ state.requests ++ non_async}
|
%{state | requests: async ++ state.requests ++ non_async}
|
||||||
|
|> add_dependencies_waiting_on_request()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_dependencies_waiting_on_request(state) do
|
||||||
|
state.dependencies_waiting_on_request
|
||||||
|
|> Enum.reduce(state, fn
|
||||||
|
{request_path, dep}, state ->
|
||||||
|
dep_path = :lists.droplast(dep)
|
||||||
|
|
||||||
|
if Enum.any?(state.requests, &(&1.path == dep_path)) do
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| unsent_dependencies: [{request_path, dep} | state.unsent_dependencies],
|
||||||
|
dependencies_waiting_on_request:
|
||||||
|
MapSet.delete(state.dependencies_waiting_on_request, {request_path, dep})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_dependencies(request, dependencies) do
|
defp build_dependencies(request, dependencies) do
|
||||||
|
|
|
@ -1018,6 +1018,15 @@ defmodule Ash.Engine.Request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
result =
|
||||||
|
case result do
|
||||||
|
{:requests, requests} ->
|
||||||
|
{:requests, requests, []}
|
||||||
|
|
||||||
|
other ->
|
||||||
|
other
|
||||||
|
end
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:new_deps, new_deps} ->
|
{:new_deps, new_deps} ->
|
||||||
log(request, fn -> "New dependencies for #{field}: #{inspect(new_deps)}" end)
|
log(request, fn -> "New dependencies for #{field}: #{inspect(new_deps)}" end)
|
||||||
|
@ -1031,7 +1040,7 @@ defmodule Ash.Engine.Request do
|
||||||
|
|
||||||
{:skipped, new_request, notifications, new_deps}
|
{:skipped, new_request, notifications, new_deps}
|
||||||
|
|
||||||
{:requests, requests} ->
|
{:requests, requests, new_deps} ->
|
||||||
log(request, fn ->
|
log(request, fn ->
|
||||||
paths =
|
paths =
|
||||||
Enum.map(requests, fn
|
Enum.map(requests, fn
|
||||||
|
@ -1046,13 +1055,15 @@ defmodule Ash.Engine.Request do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
new_deps =
|
new_deps =
|
||||||
Enum.flat_map(requests, fn
|
requests
|
||||||
|
|> Enum.flat_map(fn
|
||||||
{request, key} ->
|
{request, key} ->
|
||||||
[request.path ++ [key]]
|
[request.path ++ [key]]
|
||||||
|
|
||||||
_request ->
|
_request ->
|
||||||
[]
|
[]
|
||||||
end)
|
end)
|
||||||
|
|> Enum.concat(new_deps)
|
||||||
|
|
||||||
new_unresolved =
|
new_unresolved =
|
||||||
Map.update!(
|
Map.update!(
|
||||||
|
|
|
@ -37,6 +37,10 @@ defmodule Ash.Expr do
|
||||||
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
|
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def do_expr({:__aliases__, _, _} = expr, _escape?) do
|
||||||
|
expr
|
||||||
|
end
|
||||||
|
|
||||||
def do_expr({:^, _, [value]}, _escape?) do
|
def do_expr({:^, _, [value]}, _escape?) do
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
|
@ -115,7 +115,18 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@deps_keys [:input, :over, :record, :wait_for, :halt_if, :tenant, :condition]
|
@deps_keys [
|
||||||
|
:input,
|
||||||
|
:over,
|
||||||
|
:record,
|
||||||
|
:wait_for,
|
||||||
|
:halt_if,
|
||||||
|
:tenant,
|
||||||
|
:condition,
|
||||||
|
:resource,
|
||||||
|
:api,
|
||||||
|
:action
|
||||||
|
]
|
||||||
|
|
||||||
defp handle_input_templates(run_flow_steps) do
|
defp handle_input_templates(run_flow_steps) do
|
||||||
run_flow_steps
|
run_flow_steps
|
||||||
|
@ -511,14 +522,7 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
context: context,
|
context: context,
|
||||||
transaction_name: transaction_name
|
transaction_name: transaction_name
|
||||||
)
|
)
|
||||||
)
|
), [output_path]}
|
||||||
|> Enum.map(fn request ->
|
|
||||||
if request.path == :lists.droplast(output_path) do
|
|
||||||
{request, :data}
|
|
||||||
else
|
|
||||||
request
|
|
||||||
end
|
|
||||||
end)}
|
|
||||||
else
|
else
|
||||||
{:ok, nil}
|
{:ok, nil}
|
||||||
end
|
end
|
||||||
|
@ -804,60 +808,76 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
halt_reason: halt_reason
|
halt_reason: halt_reason
|
||||||
} = read
|
} = read
|
||||||
|
|
||||||
%{
|
List.wrap(
|
||||||
action: action,
|
maybe_dynamic(
|
||||||
action_input: action_input,
|
[resource, action],
|
||||||
dep_paths: dep_paths,
|
name,
|
||||||
tenant: tenant,
|
[name, :data],
|
||||||
request_deps: request_deps
|
|
||||||
} =
|
|
||||||
action_request_info(
|
|
||||||
all_steps,
|
|
||||||
resource,
|
|
||||||
action,
|
|
||||||
action_input,
|
|
||||||
input,
|
input,
|
||||||
|
all_steps,
|
||||||
transaction_name,
|
transaction_name,
|
||||||
tenant,
|
additional_context,
|
||||||
[wait_for, halt_if]
|
fn resource, action ->
|
||||||
|
%{
|
||||||
|
action: action,
|
||||||
|
action_input: action_input,
|
||||||
|
resource: resource,
|
||||||
|
api: api,
|
||||||
|
dep_paths: dep_paths,
|
||||||
|
tenant: tenant,
|
||||||
|
request_deps: request_deps
|
||||||
|
} =
|
||||||
|
action_request_info(
|
||||||
|
all_steps,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
api,
|
||||||
|
action_input,
|
||||||
|
input,
|
||||||
|
transaction_name,
|
||||||
|
tenant,
|
||||||
|
[wait_for, halt_if]
|
||||||
|
)
|
||||||
|
|
||||||
|
Ash.Actions.Read.as_requests([name], resource, api, action,
|
||||||
|
error_path: List.wrap(name),
|
||||||
|
authorize?: opts[:authorize?],
|
||||||
|
actor: opts[:actor],
|
||||||
|
tracer: opts[:tracer],
|
||||||
|
query_dependencies: request_deps,
|
||||||
|
get?: get? || action.get?,
|
||||||
|
tenant: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
tenant
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end,
|
||||||
|
modify_query: fn query, context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
halt_if(halt_if, halt_reason, name, results, context, fn -> query end)
|
||||||
|
end,
|
||||||
|
query_input: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
action_input
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
Ash.Actions.Read.as_requests([name], resource, api, action,
|
|
||||||
error_path: List.wrap(name),
|
|
||||||
authorize?: opts[:authorize?],
|
|
||||||
actor: opts[:actor],
|
|
||||||
tracer: opts[:tracer],
|
|
||||||
query_dependencies: request_deps,
|
|
||||||
get?: get? || action.get?,
|
|
||||||
tenant: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
tenant
|
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
|
||||||
results: results,
|
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
|
||||||
})
|
|
||||||
|> Ash.Flow.handle_modifiers()
|
|
||||||
end,
|
|
||||||
modify_query: fn query, context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
halt_if(halt_if, halt_reason, name, results, context, fn -> query end)
|
|
||||||
end,
|
|
||||||
query_input: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
action_input
|
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
|
||||||
results: results,
|
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
|
||||||
})
|
|
||||||
|> Ash.Flow.handle_modifiers()
|
|
||||||
end
|
|
||||||
)
|
)
|
||||||
|
|
||||||
%Step{step: %Ash.Flow.Step.Create{} = create, input: input} ->
|
%Step{step: %Ash.Flow.Step.Create{} = create, input: input} ->
|
||||||
|
@ -872,53 +892,69 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
halt_if: halt_if
|
halt_if: halt_if
|
||||||
} = create
|
} = create
|
||||||
|
|
||||||
%{
|
List.wrap(
|
||||||
action: action,
|
maybe_dynamic(
|
||||||
action_input: action_input,
|
[resource, action],
|
||||||
dep_paths: dep_paths,
|
name,
|
||||||
tenant: tenant,
|
[name, :commit],
|
||||||
request_deps: request_deps
|
|
||||||
} =
|
|
||||||
action_request_info(
|
|
||||||
all_steps,
|
|
||||||
resource,
|
|
||||||
action,
|
|
||||||
action_input,
|
|
||||||
input,
|
input,
|
||||||
|
all_steps,
|
||||||
transaction_name,
|
transaction_name,
|
||||||
tenant,
|
additional_context,
|
||||||
[wait_for, halt_if]
|
fn resource, action ->
|
||||||
|
%{
|
||||||
|
action: action,
|
||||||
|
resource: resource,
|
||||||
|
api: api,
|
||||||
|
action_input: action_input,
|
||||||
|
dep_paths: dep_paths,
|
||||||
|
tenant: tenant,
|
||||||
|
request_deps: request_deps
|
||||||
|
} =
|
||||||
|
action_request_info(
|
||||||
|
all_steps,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
api,
|
||||||
|
action_input,
|
||||||
|
input,
|
||||||
|
transaction_name,
|
||||||
|
tenant,
|
||||||
|
[wait_for, halt_if]
|
||||||
|
)
|
||||||
|
|
||||||
|
Ash.Actions.Create.as_requests([name], resource, api, action,
|
||||||
|
error_path: List.wrap(name),
|
||||||
|
authorize?: opts[:authorize?],
|
||||||
|
actor: opts[:actor],
|
||||||
|
tracer: opts[:tracer],
|
||||||
|
changeset_dependencies: request_deps,
|
||||||
|
tenant: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
tenant
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end,
|
||||||
|
changeset_input: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
action_input
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
Ash.Actions.Create.as_requests([name], resource, api, action,
|
|
||||||
error_path: List.wrap(name),
|
|
||||||
authorize?: opts[:authorize?],
|
|
||||||
actor: opts[:actor],
|
|
||||||
tracer: opts[:tracer],
|
|
||||||
changeset_dependencies: request_deps,
|
|
||||||
tenant: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
tenant
|
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
|
||||||
results: results,
|
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
|
||||||
})
|
|
||||||
|> Ash.Flow.handle_modifiers()
|
|
||||||
end,
|
|
||||||
changeset_input: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
action_input
|
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
|
||||||
results: results,
|
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
|
||||||
})
|
|
||||||
|> Ash.Flow.handle_modifiers()
|
|
||||||
end
|
|
||||||
)
|
)
|
||||||
|
|
||||||
%Step{step: %Ash.Flow.Step.Validate{only_keys: keys} = validate, input: input} ->
|
%Step{step: %Ash.Flow.Step.Validate{only_keys: keys} = validate, input: input} ->
|
||||||
|
@ -936,6 +972,7 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
|
|
||||||
%{
|
%{
|
||||||
action: action,
|
action: action,
|
||||||
|
resource: resource,
|
||||||
action_input: action_input,
|
action_input: action_input,
|
||||||
dep_paths: dep_paths,
|
dep_paths: dep_paths,
|
||||||
tenant: tenant,
|
tenant: tenant,
|
||||||
|
@ -945,6 +982,7 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
all_steps,
|
all_steps,
|
||||||
resource,
|
resource,
|
||||||
action,
|
action,
|
||||||
|
nil,
|
||||||
action_input,
|
action_input,
|
||||||
input,
|
input,
|
||||||
transaction_name,
|
transaction_name,
|
||||||
|
@ -1065,78 +1103,94 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
halt_reason: halt_reason
|
halt_reason: halt_reason
|
||||||
} = update
|
} = update
|
||||||
|
|
||||||
%{
|
List.wrap(
|
||||||
action: action,
|
maybe_dynamic(
|
||||||
action_input: action_input,
|
[resource, action],
|
||||||
dep_paths: dep_paths,
|
|
||||||
tenant: tenant,
|
|
||||||
request_deps: request_deps
|
|
||||||
} =
|
|
||||||
action_request_info(
|
|
||||||
all_steps,
|
|
||||||
resource,
|
|
||||||
action,
|
|
||||||
action_input,
|
|
||||||
input,
|
|
||||||
transaction_name,
|
|
||||||
tenant,
|
|
||||||
[wait_for, halt_if]
|
|
||||||
)
|
|
||||||
|
|
||||||
get_request =
|
|
||||||
get_request(
|
|
||||||
record,
|
|
||||||
input,
|
|
||||||
all_steps,
|
|
||||||
transaction_name,
|
|
||||||
name,
|
name,
|
||||||
resource,
|
[name, :commit],
|
||||||
additional_context
|
input,
|
||||||
|
all_steps,
|
||||||
|
transaction_name,
|
||||||
|
additional_context,
|
||||||
|
fn resource, action ->
|
||||||
|
%{
|
||||||
|
action: action,
|
||||||
|
action_input: action_input,
|
||||||
|
resource: resource,
|
||||||
|
api: api,
|
||||||
|
dep_paths: dep_paths,
|
||||||
|
tenant: tenant,
|
||||||
|
request_deps: request_deps
|
||||||
|
} =
|
||||||
|
action_request_info(
|
||||||
|
all_steps,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
api,
|
||||||
|
action_input,
|
||||||
|
input,
|
||||||
|
transaction_name,
|
||||||
|
tenant,
|
||||||
|
[wait_for, halt_if]
|
||||||
|
)
|
||||||
|
|
||||||
|
get_request =
|
||||||
|
get_request(
|
||||||
|
record,
|
||||||
|
input,
|
||||||
|
all_steps,
|
||||||
|
transaction_name,
|
||||||
|
name,
|
||||||
|
resource,
|
||||||
|
additional_context
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
get_request
|
||||||
|
| Ash.Actions.Update.as_requests([name], resource, api, action,
|
||||||
|
error_path: List.wrap(name),
|
||||||
|
authorize?: opts[:authorize?],
|
||||||
|
actor: opts[:actor],
|
||||||
|
tracer: opts[:tracer],
|
||||||
|
changeset_dependencies: [[name, :fetch, :data] | request_deps],
|
||||||
|
skip_on_nil_record?: true,
|
||||||
|
modify_changeset: fn changeset, context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
halt_if(halt_if, halt_reason, name, results, context, fn -> changeset end)
|
||||||
|
end,
|
||||||
|
tenant: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
tenant
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end,
|
||||||
|
record: fn context ->
|
||||||
|
Ash.Flow.do_get_in(context, [name, :fetch, :data])
|
||||||
|
end,
|
||||||
|
changeset_input: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
action_input
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
)
|
)
|
||||||
|
)
|
||||||
[
|
|
||||||
get_request
|
|
||||||
| Ash.Actions.Update.as_requests([name], resource, api, action,
|
|
||||||
error_path: List.wrap(name),
|
|
||||||
authorize?: opts[:authorize?],
|
|
||||||
actor: opts[:actor],
|
|
||||||
tracer: opts[:tracer],
|
|
||||||
changeset_dependencies: [[name, :fetch, :data] | request_deps],
|
|
||||||
skip_on_nil_record?: true,
|
|
||||||
modify_changeset: fn changeset, context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
halt_if(halt_if, halt_reason, name, results, context, fn -> changeset end)
|
|
||||||
end,
|
|
||||||
tenant: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
tenant
|
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
|
||||||
results: results,
|
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
|
||||||
})
|
|
||||||
|> Ash.Flow.handle_modifiers()
|
|
||||||
end,
|
|
||||||
record: fn context ->
|
|
||||||
Ash.Flow.do_get_in(context, [name, :fetch, :data])
|
|
||||||
end,
|
|
||||||
changeset_input: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
action_input
|
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
|
||||||
results: results,
|
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
|
||||||
})
|
|
||||||
|> Ash.Flow.handle_modifiers()
|
|
||||||
end
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
%Step{step: %Ash.Flow.Step.Destroy{} = destroy, input: input} ->
|
%Step{step: %Ash.Flow.Step.Destroy{} = destroy, input: input} ->
|
||||||
%{
|
%{
|
||||||
|
@ -1152,78 +1206,148 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
halt_reason: halt_reason
|
halt_reason: halt_reason
|
||||||
} = destroy
|
} = destroy
|
||||||
|
|
||||||
%{
|
List.wrap(
|
||||||
action: action,
|
maybe_dynamic(
|
||||||
action_input: action_input,
|
[resource, action],
|
||||||
dep_paths: dep_paths,
|
|
||||||
tenant: tenant,
|
|
||||||
request_deps: request_deps
|
|
||||||
} =
|
|
||||||
action_request_info(
|
|
||||||
all_steps,
|
|
||||||
resource,
|
|
||||||
action,
|
|
||||||
action_input,
|
|
||||||
input,
|
|
||||||
transaction_name,
|
|
||||||
tenant,
|
|
||||||
[wait_for, halt_if]
|
|
||||||
)
|
|
||||||
|
|
||||||
get_request =
|
|
||||||
get_request(
|
|
||||||
record,
|
|
||||||
input,
|
|
||||||
all_steps,
|
|
||||||
transaction_name,
|
|
||||||
name,
|
name,
|
||||||
resource,
|
[name, :commit],
|
||||||
additional_context
|
input,
|
||||||
|
all_steps,
|
||||||
|
transaction_name,
|
||||||
|
additional_context,
|
||||||
|
fn resource, action ->
|
||||||
|
%{
|
||||||
|
action: action,
|
||||||
|
action_input: action_input,
|
||||||
|
resource: resource,
|
||||||
|
api: api,
|
||||||
|
dep_paths: dep_paths,
|
||||||
|
tenant: tenant,
|
||||||
|
request_deps: request_deps
|
||||||
|
} =
|
||||||
|
action_request_info(
|
||||||
|
all_steps,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
api,
|
||||||
|
action_input,
|
||||||
|
input,
|
||||||
|
transaction_name,
|
||||||
|
tenant,
|
||||||
|
[wait_for, halt_if]
|
||||||
|
)
|
||||||
|
|
||||||
|
get_request =
|
||||||
|
get_request(
|
||||||
|
record,
|
||||||
|
input,
|
||||||
|
all_steps,
|
||||||
|
transaction_name,
|
||||||
|
name,
|
||||||
|
resource,
|
||||||
|
additional_context
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
get_request
|
||||||
|
| Ash.Actions.Destroy.as_requests([name], resource, api, action,
|
||||||
|
error_path: List.wrap(name),
|
||||||
|
authorize?: opts[:authorize?],
|
||||||
|
actor: opts[:actor],
|
||||||
|
tracer: opts[:tracer],
|
||||||
|
changeset_dependencies: [[name, :fetch, :data] | request_deps],
|
||||||
|
skip_on_nil_record?: true,
|
||||||
|
record: fn context ->
|
||||||
|
Ash.Flow.do_get_in(context, [name, :fetch, :data])
|
||||||
|
end,
|
||||||
|
modify_changeset: fn changeset, context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
halt_if(halt_if, halt_reason, name, results, context, fn -> changeset end)
|
||||||
|
end,
|
||||||
|
tenant: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
tenant
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end,
|
||||||
|
changeset_input: fn context ->
|
||||||
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
|
||||||
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
|
action_input
|
||||||
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
|
results: results,
|
||||||
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|
})
|
||||||
|
|> Ash.Flow.handle_modifiers()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
[
|
defp maybe_dynamic(
|
||||||
get_request
|
items,
|
||||||
| Ash.Actions.Destroy.as_requests([name], resource, api, action,
|
name,
|
||||||
error_path: List.wrap(name),
|
request_path,
|
||||||
authorize?: opts[:authorize?],
|
input,
|
||||||
actor: opts[:actor],
|
all_steps,
|
||||||
tracer: opts[:tracer],
|
transaction_name,
|
||||||
changeset_dependencies: [[name, :fetch, :data] | request_deps],
|
additional_context,
|
||||||
skip_on_nil_record?: true,
|
fun
|
||||||
record: fn context ->
|
) do
|
||||||
Ash.Flow.do_get_in(context, [name, :fetch, :data])
|
# TODO: this only really works for resource/action right now
|
||||||
end,
|
if Enum.all?(items, &is_atom/1) do
|
||||||
modify_changeset: fn changeset, context ->
|
apply(fun, items)
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
else
|
||||||
results = results(dep_paths, context)
|
{items, deps} =
|
||||||
|
Enum.reduce(items, {[], []}, fn item, {items, deps} ->
|
||||||
|
{item, new_deps} = Ash.Flow.handle_input_template(item, input)
|
||||||
|
{[item | items], deps ++ new_deps}
|
||||||
|
end)
|
||||||
|
|
||||||
halt_if(halt_if, halt_reason, name, results, context, fn -> changeset end)
|
dep_paths = get_dep_paths(all_steps, deps, transaction_name, [])
|
||||||
end,
|
request_deps = dependable_request_paths(dep_paths)
|
||||||
tenant: fn context ->
|
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
|
||||||
results = results(dep_paths, context)
|
|
||||||
|
|
||||||
tenant
|
Ash.Engine.Request.new(
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
authorize?: false,
|
||||||
results: results,
|
async?: false,
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
name: "Run dynamic step #{inspect(name)}",
|
||||||
})
|
path: [:dynamic, request_path],
|
||||||
|> Ash.Flow.handle_modifiers()
|
error_path: [],
|
||||||
end,
|
data:
|
||||||
changeset_input: fn context ->
|
Ash.Engine.Request.resolve(request_deps, fn context ->
|
||||||
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
context = Ash.Helpers.deep_merge_maps(context, additional_context)
|
||||||
|
|
||||||
results = results(dep_paths, context)
|
results = results(dep_paths, context)
|
||||||
|
|
||||||
action_input
|
items =
|
||||||
|> Ash.Flow.set_dependent_values(%{
|
items
|
||||||
results: results,
|
|> Ash.Flow.set_dependent_values(%{
|
||||||
elements: Map.get(context, :_ash_engine_elements)
|
results: results,
|
||||||
})
|
elements: Map.get(context, :_ash_engine_elements)
|
||||||
|> Ash.Flow.handle_modifiers()
|
})
|
||||||
end
|
|> Ash.Flow.handle_modifiers()
|
||||||
)
|
|
||||||
]
|
requests =
|
||||||
|
fun
|
||||||
|
|> apply(Enum.reverse(items))
|
||||||
|
|> List.wrap()
|
||||||
|
|
||||||
|
{:ok, nil, %{requests: requests}}
|
||||||
|
end)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1486,25 +1610,37 @@ defmodule Ash.Flow.Executor.AshEngine do
|
||||||
all_steps,
|
all_steps,
|
||||||
resource,
|
resource,
|
||||||
action,
|
action,
|
||||||
|
api,
|
||||||
action_input,
|
action_input,
|
||||||
input,
|
input,
|
||||||
transaction_name,
|
transaction_name,
|
||||||
tenant,
|
tenant,
|
||||||
additional
|
additional
|
||||||
) do
|
) do
|
||||||
|
{action_input, deps} = Ash.Flow.handle_input_template(action_input, input)
|
||||||
|
{tenant, tenant_deps} = Ash.Flow.handle_input_template(tenant, input)
|
||||||
|
{_, additional_deps} = Ash.Flow.handle_input_template(additional, input)
|
||||||
|
{resource, resource_deps} = Ash.Flow.handle_input_template(resource, input)
|
||||||
|
{api, api_deps} = Ash.Flow.handle_input_template(api, input)
|
||||||
|
{action, action_deps} = Ash.Flow.handle_input_template(action, input)
|
||||||
|
|
||||||
action =
|
action =
|
||||||
Ash.Resource.Info.action(resource, action) ||
|
Ash.Resource.Info.action(resource, action) ||
|
||||||
raise "No such action #{action} for #{resource}"
|
raise "No such action #{action} for #{resource}"
|
||||||
|
|
||||||
{action_input, deps} = Ash.Flow.handle_input_template(action_input, input)
|
dep_paths =
|
||||||
{tenant, tenant_deps} = Ash.Flow.handle_input_template(tenant, input)
|
get_dep_paths(
|
||||||
{_, additional_deps} = Ash.Flow.handle_input_template(additional, input)
|
all_steps,
|
||||||
|
deps ++ tenant_deps ++ api_deps ++ resource_deps ++ action_deps,
|
||||||
dep_paths = get_dep_paths(all_steps, deps ++ tenant_deps, transaction_name, additional_deps)
|
transaction_name,
|
||||||
|
additional_deps
|
||||||
|
)
|
||||||
|
|
||||||
request_deps = dependable_request_paths(dep_paths)
|
request_deps = dependable_request_paths(dep_paths)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
resource: resource,
|
||||||
|
api: api,
|
||||||
action: action,
|
action: action,
|
||||||
action_input: action_input,
|
action_input: action_input,
|
||||||
dep_paths: dep_paths,
|
dep_paths: dep_paths,
|
||||||
|
|
|
@ -2,7 +2,7 @@ defmodule Ash.Flow do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A flow is a static definition of a set of steps to be .
|
A flow is a static definition of a set of steps to be .
|
||||||
|
|
||||||
Seeuthe {{link:ash:guide:Flows}} guide for more.
|
See the {{link:ash:guide:Flows}} guide for more.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: module
|
@type t :: module
|
||||||
|
@ -50,7 +50,6 @@ defmodule Ash.Flow do
|
||||||
input: input,
|
input: input,
|
||||||
result: result,
|
result: result,
|
||||||
notifications: metadata[:notifications] || [],
|
notifications: metadata[:notifications] || [],
|
||||||
runner_metadata: metadata[:runner_metadata],
|
|
||||||
valid?: true,
|
valid?: true,
|
||||||
complete?: true
|
complete?: true
|
||||||
}
|
}
|
||||||
|
@ -66,14 +65,16 @@ defmodule Ash.Flow do
|
||||||
}
|
}
|
||||||
|
|
||||||
{:error, metadata, error} ->
|
{:error, metadata, error} ->
|
||||||
|
complete? = complete?(error)
|
||||||
|
|
||||||
%Ash.Flow.Result{
|
%Ash.Flow.Result{
|
||||||
flow: flow,
|
flow: flow,
|
||||||
params: params,
|
params: params,
|
||||||
input: input,
|
input: input,
|
||||||
notifications: metadata[:notifications] || [],
|
notifications: metadata[:notifications] || [],
|
||||||
runner_metadata: metadata[:runner_metadata],
|
runner_metadata: if(not complete?, do: metadata[:runner_metadata]),
|
||||||
valid?: false,
|
valid?: false,
|
||||||
complete?: false,
|
complete?: complete?,
|
||||||
errors: List.wrap(error)
|
errors: List.wrap(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +84,7 @@ defmodule Ash.Flow do
|
||||||
params: params,
|
params: params,
|
||||||
input: input,
|
input: input,
|
||||||
valid?: false,
|
valid?: false,
|
||||||
complete?: false,
|
complete?: complete?(error),
|
||||||
errors: List.wrap(error)
|
errors: List.wrap(error)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -94,7 +95,7 @@ defmodule Ash.Flow do
|
||||||
params: input,
|
params: input,
|
||||||
input: new_input,
|
input: new_input,
|
||||||
valid?: false,
|
valid?: false,
|
||||||
complete?: false,
|
complete?: complete?(error),
|
||||||
errors: List.wrap(error)
|
errors: List.wrap(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +105,7 @@ defmodule Ash.Flow do
|
||||||
params: input,
|
params: input,
|
||||||
input: input,
|
input: input,
|
||||||
valid?: false,
|
valid?: false,
|
||||||
complete?: false,
|
complete?: complete?(error),
|
||||||
errors: List.wrap(error)
|
errors: List.wrap(error)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -112,6 +113,14 @@ defmodule Ash.Flow do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp complete?(%Ash.Error.Flow.Halted{}) do
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
defp complete?(_) do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
defp add_actor(opts) do
|
defp add_actor(opts) do
|
||||||
if Keyword.has_key?(opts, :actor) do
|
if Keyword.has_key?(opts, :actor) do
|
||||||
opts
|
opts
|
||||||
|
|
|
@ -75,19 +75,19 @@ defmodule Ash.Flow.Step do
|
||||||
def shared_action_opts do
|
def shared_action_opts do
|
||||||
[
|
[
|
||||||
resource: [
|
resource: [
|
||||||
type: :atom,
|
type: :any,
|
||||||
required: true,
|
required: true,
|
||||||
doc: "The resource to call the action on.",
|
doc: "The resource to call the action on.",
|
||||||
links: []
|
links: []
|
||||||
],
|
],
|
||||||
action: [
|
action: [
|
||||||
type: :atom,
|
type: :any,
|
||||||
required: true,
|
required: true,
|
||||||
doc: "The action to call on the resource.",
|
doc: "The action to call on the resource.",
|
||||||
links: []
|
links: []
|
||||||
],
|
],
|
||||||
api: [
|
api: [
|
||||||
type: :atom,
|
type: :any,
|
||||||
doc:
|
doc:
|
||||||
"The api to use when calling the action. Defaults to the api set in the `flow` section.",
|
"The api to use when calling the action. Defaults to the api set in the `flow` section.",
|
||||||
links: []
|
links: []
|
||||||
|
|
Loading…
Reference in a new issue