feat: add manage relationship types

improvement: don't accept relationships on actions anymore

improvement: require arguments

This probably broke a lot of people's setups, but it was a
necessary change. Better to get this stuff out while we're
still beta
This commit is contained in:
Zach Daniel 2021-03-25 11:33:32 -04:00
parent 6eb1b3ae91
commit 2f9fafcbc7
6 changed files with 360 additions and 210 deletions

View file

@ -9,10 +9,14 @@ defmodule Ash.Actions.ManagedRelationships do
def load(_api, created, %{relationships: nil}, _), do: {:ok, created} def load(_api, created, %{relationships: nil}, _), do: {:ok, created}
def load(api, created, changeset, opts) do def load(api, created, changeset, opts) do
api.load(created, Map.keys(changeset.relationships), if Ash.Changeset.ManagedRelationshipHelpers.must_load?(opts) do
authorize?: opts[:authorize?], api.load(created, Map.keys(changeset.relationships),
actor: opts[:actor] authorize?: opts[:authorize?],
) actor: opts[:actor]
)
else
{:ok, created}
end
end end
def setup_managed_belongs_to_relationships(changeset, actor) do def setup_managed_belongs_to_relationships(changeset, actor) do
@ -45,7 +49,7 @@ defmodule Ash.Actions.ManagedRelationships do
{changeset, instructions} -> {changeset, instructions} ->
pkeys = pkeys(relationship) pkeys = pkeys(relationship)
opts = sanitize_opts(relationship, opts) opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
current_value = Map.get(changeset.data, relationship.name) current_value = Map.get(changeset.data, relationship.name)
case find_match(List.wrap(current_value), input, pkeys, relationship) do case find_match(List.wrap(current_value), input, pkeys, relationship) do
@ -79,7 +83,7 @@ defmodule Ash.Actions.ManagedRelationships do
case Ash.Filter.get_filter(relationship.destination, input) do case Ash.Filter.get_filter(relationship.destination, input) do
{:ok, keys} -> {:ok, keys} ->
relationship.destination relationship.destination
|> Ash.Query.for_read(read, input) |> Ash.Query.for_read(read, input, actor: actor)
|> Ash.Query.filter(^keys) |> Ash.Query.filter(^keys)
|> Ash.Query.set_context(relationship.context) |> Ash.Query.set_context(relationship.context)
|> Ash.Query.limit(1) |> Ash.Query.limit(1)
@ -223,7 +227,11 @@ defmodule Ash.Actions.ManagedRelationships do
index index
) do ) do
relationship.destination relationship.destination
|> Ash.Changeset.for_create(action_name, input, require?: false) |> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create( |> changeset.api.create(
@ -295,119 +303,6 @@ defmodule Ash.Actions.ManagedRelationships do
end) end)
end end
defp sanitize_opts(relationship, opts) do
[
on_no_match: :ignore,
on_missing: :ignore,
on_match: :ignore,
on_lookup: :ignore
]
|> Keyword.merge(opts)
|> Keyword.update!(:on_no_match, fn
:create when relationship.type == :many_to_many ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :create)
join_action = Ash.Resource.Info.primary_action!(relationship.through_destination, :create)
{:create, action.name, join_action.name, []}
{:create, action_name} when relationship.type == :many_to_many ->
join_action = Ash.Resource.Info.primary_action!(relationship.through_destination, :create)
{:create, action_name, join_action.name, []}
:create ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :create)
{:create, action.name}
other ->
other
end)
|> Keyword.update!(:on_missing, fn
:destroy when relationship.type == :many_to_many ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :destroy)
join_action =
Ash.Resource.Info.primary_action!(relationship.through_destination, :destroy)
{:destroy, action.name, join_action.name, []}
{:destroy, action_name} when relationship.type == :many_to_many ->
join_action =
Ash.Resource.Info.primary_action!(relationship.through_destination, :destroy)
{:destroy, action_name, join_action.name, []}
:destroy ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :destroy)
{:destroy, action.name}
:unrelate ->
{:unrelate, nil}
other ->
other
end)
|> Keyword.update!(:on_match, fn
:update when relationship.type == :many_to_many ->
update = Ash.Resource.Info.primary_action!(relationship.destination, :update)
join_update = Ash.Resource.Info.primary_action!(relationship.through, :update)
{:update, update.name, join_update.name, []}
{:update, update} when relationship.type == :many_to_many ->
join_update = Ash.Resource.Info.primary_action!(relationship.through, :update)
{:update, update, join_update.name, []}
{:update, update, join_update} when relationship.type == :many_to_many ->
{:update, update, join_update, []}
:update ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :update)
{:update, action.name}
:unrelate ->
{:unrelate, nil}
other ->
other
end)
|> Keyword.update!(:on_lookup, fn
operation
when relationship.type == :many_to_many and
operation in [:relate, :relate_and_update] ->
read = Ash.Resource.Info.primary_action(relationship.destination, :read)
create = Ash.Resource.Info.primary_action(relationship.through, :create)
{operation, create.name, read.name, []}
operation
when relationship.type in [:has_many, :has_one] and
operation in [:relate, :relate_and_update] ->
read = Ash.Resource.Info.primary_action(relationship.destination, :read)
update = Ash.Resource.Info.primary_action(relationship.destination, :update)
if relationship.type == :many_to_many do
{operation, update.name, read.name, []}
else
{operation, update.name, read.name}
end
operation when operation in [:relate, :relate_and_update] ->
read = Ash.Resource.Info.primary_action(relationship.destination, :read)
update = Ash.Resource.Info.primary_action(relationship.source, :update)
if relationship.type == :many_to_many do
{operation, update.name, read.name, []}
else
{operation, update.name, read.name}
end
:ignore ->
:ignore
end)
end
defp pkeys(relationship) do defp pkeys(relationship) do
identities = identities =
relationship.destination relationship.destination
@ -427,7 +322,7 @@ defmodule Ash.Actions.ManagedRelationships do
opts opts
) do ) do
inputs = List.wrap(inputs) inputs = List.wrap(inputs)
opts = sanitize_opts(relationship, opts) opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
pkeys = pkeys(relationship) pkeys = pkeys(relationship)
original_value = List.wrap(Map.get(record, relationship.name)) original_value = List.wrap(Map.get(record, relationship.name))
@ -489,7 +384,7 @@ defmodule Ash.Actions.ManagedRelationships do
index, index,
opts opts
) do ) do
opts = sanitize_opts(relationship, opts) opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
identities = identities =
relationship.destination relationship.destination
@ -619,7 +514,7 @@ defmodule Ash.Actions.ManagedRelationships do
{:ok, input} {:ok, input}
else else
relationship.destination relationship.destination
|> Ash.Query.for_read(read, input) |> Ash.Query.for_read(read, input, actor: actor)
|> Ash.Query.filter(^keys) |> Ash.Query.filter(^keys)
|> Ash.Query.set_context(relationship.context) |> Ash.Query.set_context(relationship.context)
|> Ash.Query.set_tenant(changeset.tenant) |> Ash.Query.set_tenant(changeset.tenant)
@ -714,7 +609,7 @@ defmodule Ash.Actions.ManagedRelationships do
relationship.through relationship.through
|> Ash.Changeset.new() |> Ash.Changeset.new()
|> Ash.Changeset.for_create(create_or_update, join_input) |> Ash.Changeset.for_create(create_or_update, join_input, actor: actor)
|> Ash.Changeset.force_change_attribute( |> Ash.Changeset.force_change_attribute(
relationship.source_field_on_join_table, relationship.source_field_on_join_table,
Map.get(record, relationship.source_field) Map.get(record, relationship.source_field)
@ -768,7 +663,10 @@ defmodule Ash.Actions.ManagedRelationships do
end end
found found
|> Ash.Changeset.for_update(create_or_update, input) |> Ash.Changeset.for_update(create_or_update, input,
relationships: opts[:relationships] || [],
actor: actor
)
|> Ash.Changeset.force_change_attribute( |> Ash.Changeset.force_change_attribute(
relationship.destination_field, relationship.destination_field,
Map.get(record, relationship.source_field) Map.get(record, relationship.source_field)
@ -818,7 +716,11 @@ defmodule Ash.Actions.ManagedRelationships do
{:ok, input, [], []} {:ok, input, [], []}
else else
relationship.destination relationship.destination
|> Ash.Changeset.for_create(action_name, input, require?: false) |> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
relationships: opts[:relationships]
)
|> Ash.Changeset.force_change_attribute( |> Ash.Changeset.force_change_attribute(
relationship.destination_field, relationship.destination_field,
Map.get(record, relationship.source_field) Map.get(record, relationship.source_field)
@ -861,7 +763,11 @@ defmodule Ash.Actions.ManagedRelationships do
{:ok, input, []} {:ok, input, []}
else else
relationship.destination relationship.destination
|> Ash.Changeset.for_create(action_name, regular_params, require?: false) |> Ash.Changeset.for_create(action_name, regular_params,
require?: false,
relationships: opts[:relationships],
actor: actor
)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create( |> changeset.api.create(
@ -878,7 +784,10 @@ defmodule Ash.Actions.ManagedRelationships do
relationship.through relationship.through
|> Ash.Changeset.new() |> Ash.Changeset.new()
|> Ash.Changeset.for_create(join_action_name, join_params, require?: false) |> Ash.Changeset.for_create(join_action_name, join_params,
require?: false,
actor: actor
)
|> Ash.Changeset.force_change_attribute( |> Ash.Changeset.force_change_attribute(
relationship.source_field_on_join_table, relationship.source_field_on_join_table,
Map.get(record, relationship.source_field) Map.get(record, relationship.source_field)
@ -967,7 +876,10 @@ defmodule Ash.Actions.ManagedRelationships do
end end
match match
|> Ash.Changeset.for_update(action_name, input) |> Ash.Changeset.for_update(action_name, input,
actor: actor,
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true) |> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true)
@ -993,7 +905,10 @@ defmodule Ash.Actions.ManagedRelationships do
source_value = Map.get(source_record, relationship.source_field) source_value = Map.get(source_record, relationship.source_field)
match match
|> Ash.Changeset.for_update(action_name, regular_params) |> Ash.Changeset.for_update(action_name, regular_params,
actor: actor,
relationships: opts[:relationships]
)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true) |> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true)
@ -1028,7 +943,7 @@ defmodule Ash.Actions.ManagedRelationships do
) )
result result
|> Ash.Changeset.for_update(join_action_name, join_params) |> Ash.Changeset.for_update(join_action_name, join_params, actor: actor)
|> Ash.Changeset.set_context(join_relationship.context) |> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update( |> api.update(
@ -1057,7 +972,13 @@ defmodule Ash.Actions.ManagedRelationships do
end end
end end
defp find_match(current_value, input, pkeys, relationship \\ nil) do defp find_match(current_value, input, pkeys, relationship \\ nil)
defp find_match(%Ash.NotLoaded{}, _input, _pkeys, _relationship) do
nil
end
defp find_match(current_value, input, pkeys, relationship) do
Enum.find(current_value, fn current_value -> Enum.find(current_value, fn current_value ->
Enum.any?(pkeys, fn pkey -> Enum.any?(pkeys, fn pkey ->
matches?(current_value, input, pkey, relationship) matches?(current_value, input, pkey, relationship)
@ -1166,7 +1087,8 @@ defmodule Ash.Actions.ManagedRelationships do
result result
|> Ash.Changeset.for_destroy( |> Ash.Changeset.for_destroy(
join_action_name, join_action_name,
%{} %{},
actor: actor
) )
|> Ash.Changeset.set_context(join_relationship.context) |> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
@ -1180,7 +1102,7 @@ defmodule Ash.Actions.ManagedRelationships do
notifications = join_notifications ++ all_notifications notifications = join_notifications ++ all_notifications
record record
|> Ash.Changeset.for_destroy(action_name, %{}) |> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy( |> api.destroy(
@ -1209,7 +1131,7 @@ defmodule Ash.Actions.ManagedRelationships do
{:destroy, action_name} -> {:destroy, action_name} ->
record record
|> Ash.Changeset.for_destroy(action_name, %{}) |> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant) |> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy( |> api.destroy(
@ -1280,7 +1202,7 @@ defmodule Ash.Actions.ManagedRelationships do
|> case do |> case do
{:ok, result} -> {:ok, result} ->
result result
|> Ash.Changeset.for_destroy(action_name, %{}) |> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant) |> Ash.Changeset.set_tenant(tenant)
|> api.destroy( |> api.destroy(
@ -1316,7 +1238,10 @@ defmodule Ash.Actions.ManagedRelationships do
action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name
record record
|> Ash.Changeset.for_update(action_name, %{}) |> Ash.Changeset.for_update(action_name, %{},
relationships: opts[:relationships] || [],
actor: actor
)
|> Ash.Changeset.force_change_attribute(relationship.destination_field, nil) |> Ash.Changeset.force_change_attribute(relationship.destination_field, nil)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant) |> Ash.Changeset.set_tenant(tenant)
@ -1344,7 +1269,10 @@ defmodule Ash.Actions.ManagedRelationships do
action_name || Ash.Resource.Info.primary_action(relationship.source, :update).name action_name || Ash.Resource.Info.primary_action(relationship.source, :update).name
source_record source_record
|> Ash.Changeset.for_update(action_name, %{}) |> Ash.Changeset.for_update(action_name, %{},
relationships: opts[:relationships] || [],
actor: actor
)
|> Ash.Changeset.force_change_attribute(relationship.source_field, nil) |> Ash.Changeset.force_change_attribute(relationship.source_field, nil)
|> Ash.Changeset.set_context(relationship.context) |> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant) |> Ash.Changeset.set_tenant(tenant)

View file

@ -250,24 +250,31 @@ defmodule Ash.Changeset do
end end
end end
@manage_types [:replace, :append, :remove, :direct_control, :create]
@for_create_opts [ @for_create_opts [
relationships: [ relationships: [
type: :any, type: :any,
doc: """ doc: """
customize relationship behavior. customize relationship behavior.
By default, any relationships are *replaced* via `replace_relationship`. To change this behavior, provide the By default, any relationships are ignored. There are three ways to change relationships with this function:
`relationships` option. The values for each relationship can be
* `:append` - passes input to `append_to_relatinship/3`
* `:remove` - passes input to `remove_from_relationship/3`
* `:replace` - passes input to `replace_relationship/3`
* `{:manage, opts}` - passes the input to `manage_relationship/4`, with the given `opts`.
For example: ### Action Arguments (preferred)
Create an argument on the action and add a `Ash.Resource.Change.Builtins.manage_relationship/3` change to the action.
### Overrides
You can pass the `relationships` option to specify the behavior. It is a keyword list of relationship and either
* one of the preset manage types: #{inspect(@manage_types)}
* explicit options, in the form of `{:manage, [...opts]}`
```elixir ```elixir
Ash.Changeset.for_create(MyResource, :create, params, relationships: [relationship: :append, other_relationship: :remove]) Ash.Changeset.for_create(MyResource, :create, params, relationships: [relationship: :append, other_relationship: {:manage, [...opts]}])
``` ```
You can also use explicit calls to `manage_relationship/4`.
""" """
], ],
require?: [ require?: [
@ -444,7 +451,6 @@ defmodule Ash.Changeset do
|> Map.put(:__validated_for_action__, action.name) |> Map.put(:__validated_for_action__, action.name)
|> cast_params(action, params || %{}, opts) |> cast_params(action, params || %{}, opts)
|> validate_attributes_accepted(action) |> validate_attributes_accepted(action)
|> validate_relationships_accepted(action)
|> cast_arguments(action, opts[:defaults], true) |> cast_arguments(action, opts[:defaults], true)
|> run_action_changes(action, opts[:actor]) |> run_action_changes(action, opts[:actor])
|> set_defaults(changeset.action_type, false) |> set_defaults(changeset.action_type, false)
@ -533,20 +539,17 @@ defmodule Ash.Changeset do
rel = Ash.Resource.Info.public_relationship(changeset.resource, name) -> rel = Ash.Resource.Info.public_relationship(changeset.resource, name) ->
if rel.writable? do if rel.writable? do
behaviour = opts[:relationships][rel.name] || :replace behaviour = opts[:relationships][rel.name]
case behaviour do case behaviour do
:replace -> nil ->
replace_relationship(changeset, rel.name, value) changeset
:append -> type when is_atom(type) ->
append_to_relationship(changeset, rel.name, value) manage_relationship(changeset, rel.name, value, type: type)
:remove -> {:manage, manage_opts} ->
remove_from_relationship(changeset, rel.name, value) manage_relationship(changeset, rel.name, value, manage_opts)
{:manage, opts} ->
manage_relationship(changeset, rel.name, value, opts)
end end
else else
changeset changeset
@ -581,24 +584,6 @@ defmodule Ash.Changeset do
end) end)
end end
defp validate_relationships_accepted(changeset, %{accept: nil}), do: changeset
defp validate_relationships_accepted(changeset, %{accept: accepted_relationships}) do
changeset.relationships
|> Enum.reject(fn {key, _value} ->
key in accepted_relationships
end)
|> Enum.reduce(changeset, fn {key, _}, changeset ->
add_error(
changeset,
InvalidRelationship.exception(
relationship: key,
message: "Cannot be changed"
)
)
end)
end
defp run_action_changes(changeset, %{changes: changes}, actor) do defp run_action_changes(changeset, %{changes: changes}, actor) do
Enum.reduce(changes, changeset, fn Enum.reduce(changes, changeset, fn
%{change: {module, opts}}, changeset -> %{change: {module, opts}}, changeset ->
@ -1041,10 +1026,100 @@ defmodule Ash.Changeset do
defp argument_default(value) when is_function(value, 0), do: value.() defp argument_default(value) when is_function(value, 0), do: value.()
defp argument_default(value), do: value defp argument_default(value), do: value
@type manage_relationship_type :: :replace | :append | :remove | :direct_control
@spec manage_relationship_opts(manage_relationship_type()) :: Keyword.t()
def manage_relationship_opts(:replace) do
[
on_lookup: :relate,
on_no_match: :error,
on_match: :ignore,
on_missing: :unrelate
]
end
def manage_relationship_opts(:append) do
[
on_lookup: :relate,
on_no_match: :error,
on_match: :ignore,
on_missing: :ignore
]
end
def manage_relationship_opts(:remove) do
[
on_no_match: :error,
on_match: :unrelate,
on_missing: :ignore
]
end
def manage_relationship_opts(:create) do
[
on_no_match: :create,
on_match: :ignore
]
end
def manage_relationship_opts(:direct_control) do
[
on_lookup: :ignore,
on_no_match: :create,
on_match: :update,
on_missing: :destroy
]
end
@manage_opts [ @manage_opts [
type: [
type: {:one_of, @manage_types},
doc: """
If the `type` is specified, the default values of each option is modified to match that `type` of operation.
This allows for specifying certain operations much more succinctly. The defaults that are modified are listed below
## `:replace`
[
on_lookup: :relate,
on_no_match: :error,
on_match: :ignore,
on_missing: :unrelate
]
## `:append`
[
on_lookup: :relate,
on_no_match: :error,
on_match: :ignore,
on_missing: :ignore
]
## `:remove`
[
on_no_match: :error,
on_match: :unrelate,
on_missing: :ignore
]
## `:direct_control`
[
on_lookup: :ignore,
on_no_match: :create,
on_match: :update,
on_missing: :destroy
]
## `:create`
[
on_no_match: :create,
on_match: :ignore
]
"""
],
authorize?: [ authorize?: [
type: :boolean, type: :boolean,
default: false, default: true,
doc: doc:
"Authorize reads and changes to the destination records, if the primary change is being authorized as well." "Authorize reads and changes to the destination records, if the primary change is being authorized as well."
], ],
@ -1135,6 +1210,11 @@ defmodule Ash.Changeset do
* belongs_to - an update action on the source resource * belongs_to - an update action on the source resource
""" """
], ],
relationships: [
type: :any,
default: [],
doc: "A keyword list of instructions for nested relationships."
],
meta: [ meta: [
type: :any, type: :any,
doc: doc:
@ -1267,7 +1347,18 @@ defmodule Ash.Changeset do
end end
def manage_relationship(changeset, relationship, input, opts) do def manage_relationship(changeset, relationship, input, opts) do
opts = Ash.OptionsHelpers.validate!(opts, @manage_opts) manage_opts =
if opts[:type] do
defaults = manage_relationship_opts(opts[:type])
Enum.reduce(defaults, @manage_opts, fn {key, value}, manage_opts ->
Ash.OptionsHelpers.set_default!(manage_opts, key, value)
end)
else
@manage_opts
end
opts = Ash.OptionsHelpers.validate!(opts, manage_opts)
case Ash.Resource.Info.relationship(changeset.resource, relationship) do case Ash.Resource.Info.relationship(changeset.resource, relationship) do
nil -> nil ->
@ -1350,6 +1441,10 @@ defmodule Ash.Changeset do
end end
end end
defp map_input_to_list(input) when input == %{} do
:error
end
defp map_input_to_list(input) do defp map_input_to_list(input) do
input input
|> Enum.reduce_while({:ok, []}, fn |> Enum.reduce_while({:ok, []}, fn
@ -1538,30 +1633,31 @@ defmodule Ash.Changeset do
""" """
def set_argument(changeset, argument, value) do def set_argument(changeset, argument, value) do
if changeset.action do if changeset.action do
with {:arg, argument} when not is_nil(argument) <- argument =
{:arg, Enum.find(
Enum.find( changeset.action.arguments,
changeset.action.arguments, &(&1.name == argument || to_string(&1.name) == argument)
&(&1.name == argument || to_string(&1.name) == argument) )
)},
{:ok, casted} <- cast_input(argument.type, value, argument.constraints), if argument do
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <- with {:ok, casted} <- cast_input(argument.type, value, argument.constraints),
{:constrained, {:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
Ash.Type.apply_constraints(argument.type, casted, argument.constraints), {:constrained,
argument} do Ash.Type.apply_constraints(argument.type, casted, argument.constraints),
%{changeset | arguments: Map.put(changeset.arguments, argument.name, casted)} argument} do
%{changeset | arguments: Map.put(changeset.arguments, argument.name, casted)}
else
{:constrained, {:ok, nil}, _argument} ->
changeset
{:constrained, {:error, error}, argument} ->
add_invalid_errors(:argument, changeset, argument, error)
{:error, error} ->
add_invalid_errors(:argument, changeset, argument, error)
end
else else
{:arg, nil} -> %{changeset | arguments: Map.put(changeset.arguments, argument, value)}
changeset
{:constrained, {:ok, nil}, _argument} ->
changeset
{:constrained, {:error, error}, argument} ->
add_invalid_errors(:argument, changeset, argument, error)
{:error, error} ->
add_invalid_errors(:argument, changeset, argument, error)
end end
else else
%{changeset | arguments: Map.put(changeset.arguments, argument, value)} %{changeset | arguments: Map.put(changeset.arguments, argument, value)}

View file

@ -0,0 +1,131 @@
defmodule Ash.Changeset.ManagedRelationshipHelpers do
@moduledoc """
Tools for introspecting managed relationships.
Extensions can use this to look at an argument that will be passed
to a `manage_relationship` change and determine what their behavior
should be. For example, AshAdmin uses these to find out what kind of
nested form it should offer for each argument that manages a relationship.
"""
def sanitize_opts(relationship, opts) do
[
on_no_match: :ignore,
on_missing: :ignore,
on_match: :ignore,
on_lookup: :ignore
]
|> Keyword.merge(opts)
|> Keyword.update!(:on_no_match, fn
:create when relationship.type == :many_to_many ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :create)
join_action = Ash.Resource.Info.primary_action!(relationship.through_destination, :create)
{:create, action.name, join_action.name, []}
{:create, action_name} when relationship.type == :many_to_many ->
join_action = Ash.Resource.Info.primary_action!(relationship.through_destination, :create)
{:create, action_name, join_action.name, []}
:create ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :create)
{:create, action.name}
other ->
other
end)
|> Keyword.update!(:on_missing, fn
:destroy when relationship.type == :many_to_many ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :destroy)
join_action =
Ash.Resource.Info.primary_action!(relationship.through_destination, :destroy)
{:destroy, action.name, join_action.name, []}
{:destroy, action_name} when relationship.type == :many_to_many ->
join_action =
Ash.Resource.Info.primary_action!(relationship.through_destination, :destroy)
{:destroy, action_name, join_action.name, []}
:destroy ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :destroy)
{:destroy, action.name}
:unrelate ->
{:unrelate, nil}
other ->
other
end)
|> Keyword.update!(:on_match, fn
:update when relationship.type == :many_to_many ->
update = Ash.Resource.Info.primary_action!(relationship.destination, :update)
join_update = Ash.Resource.Info.primary_action!(relationship.through, :update)
{:update, update.name, join_update.name, []}
{:update, update} when relationship.type == :many_to_many ->
join_update = Ash.Resource.Info.primary_action!(relationship.through, :update)
{:update, update, join_update.name, []}
{:update, update, join_update} when relationship.type == :many_to_many ->
{:update, update, join_update, []}
:update ->
action = Ash.Resource.Info.primary_action!(relationship.destination, :update)
{:update, action.name}
:unrelate ->
{:unrelate, nil}
other ->
other
end)
|> Keyword.update!(:on_lookup, fn
operation
when relationship.type == :many_to_many and
operation in [:relate, :relate_and_update] ->
read = Ash.Resource.Info.primary_action(relationship.destination, :read)
create = Ash.Resource.Info.primary_action(relationship.through, :create)
{operation, create.name, read.name, []}
operation
when relationship.type in [:has_many, :has_one] and
operation in [:relate, :relate_and_update] ->
read = Ash.Resource.Info.primary_action(relationship.destination, :read)
update = Ash.Resource.Info.primary_action(relationship.destination, :update)
{operation, update.name, read.name}
operation when operation in [:relate, :relate_and_update] ->
read = Ash.Resource.Info.primary_action(relationship.destination, :read)
update = Ash.Resource.Info.primary_action(relationship.source, :update)
{operation, update.name, read.name}
:ignore ->
:ignore
end)
end
def could_lookup?(opts) do
opts[:on_lookup] != :ignore
end
def must_load?(opts) do
only_creates? = unwrap(opts[:on_match]) == :create && unwrap(opts[:on_no_match]) == :create
only_ignores? = opts[:on_no_match] == :ignore && opts[:on_match] == :ignore
can_skip_load? = opts[:on_missing] == :ignore && (only_creates? || only_ignores?)
not can_skip_load?
end
defp unwrap(value) when is_atom(value), do: true
defp unwrap(tuple) when is_tuple(tuple), do: elem(tuple, 0)
defp unwrap(value), do: value
end

View file

@ -35,7 +35,9 @@ defmodule Ash.Resource.Change.Builtins do
change manage_relationship(:add_comments, :comments, on_missing: :ignore, on_match: :create, on_no_match: {:create, :add_comment_to_post} change manage_relationship(:add_comments, :comments, on_missing: :ignore, on_match: :create, on_no_match: {:create, :add_comment_to_post}
``` ```
""" """
def manage_relationship(argument, relationship_name, opts) do def manage_relationship(argument, relationship_name \\ nil, opts) do
relationship_name = relationship_name || argument
{Ash.Resource.Change.ManageRelationship, {Ash.Resource.Change.ManageRelationship,
[argument: argument, relationship: relationship_name, opts: opts]} [argument: argument, relationship: relationship_name, opts: opts]}
end end

View file

@ -6,18 +6,11 @@ defmodule Ash.Resource.Transformers.DefaultAccept do
alias Ash.Dsl.Transformer alias Ash.Dsl.Transformer
def transform(resource, dsl_state) do def transform(resource, dsl_state) do
default_accept_attributes = default_accept =
resource resource
|> Ash.Resource.Info.public_attributes() |> Ash.Resource.Info.public_attributes()
|> Enum.map(& &1.name) |> Enum.map(& &1.name)
default_accept_relationships =
resource
|> Ash.Resource.Info.public_relationships()
|> Enum.map(& &1.name)
default_accept = Enum.concat(default_accept_attributes, default_accept_relationships)
dsl_state dsl_state
|> Transformer.get_entities([:actions]) |> Transformer.get_entities([:actions])
|> Enum.reject(&(&1.type == :read)) |> Enum.reject(&(&1.type == :read))

View file

@ -570,7 +570,7 @@ defmodule Ash.Test.Actions.CreateTest do
|> Api.create!() |> Api.create!()
ProfileWithBelongsTo ProfileWithBelongsTo
|> Ash.Changeset.for_create(:create, author: author) |> Ash.Changeset.for_create(:create, [author: author], relationships: [author: :replace])
|> Api.create!() |> Api.create!()
end end