mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
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:
parent
6eb1b3ae91
commit
2f9fafcbc7
6 changed files with 360 additions and 210 deletions
|
@ -9,10 +9,14 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
def load(_api, created, %{relationships: nil}, _), do: {:ok, created}
|
||||
|
||||
def load(api, created, changeset, opts) do
|
||||
api.load(created, Map.keys(changeset.relationships),
|
||||
authorize?: opts[:authorize?],
|
||||
actor: opts[:actor]
|
||||
)
|
||||
if Ash.Changeset.ManagedRelationshipHelpers.must_load?(opts) do
|
||||
api.load(created, Map.keys(changeset.relationships),
|
||||
authorize?: opts[:authorize?],
|
||||
actor: opts[:actor]
|
||||
)
|
||||
else
|
||||
{:ok, created}
|
||||
end
|
||||
end
|
||||
|
||||
def setup_managed_belongs_to_relationships(changeset, actor) do
|
||||
|
@ -45,7 +49,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
{changeset, instructions} ->
|
||||
pkeys = pkeys(relationship)
|
||||
|
||||
opts = sanitize_opts(relationship, opts)
|
||||
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
|
||||
current_value = Map.get(changeset.data, relationship.name)
|
||||
|
||||
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
|
||||
{:ok, keys} ->
|
||||
relationship.destination
|
||||
|> Ash.Query.for_read(read, input)
|
||||
|> Ash.Query.for_read(read, input, actor: actor)
|
||||
|> Ash.Query.filter(^keys)
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.limit(1)
|
||||
|
@ -223,7 +227,11 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
index
|
||||
) do
|
||||
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_tenant(changeset.tenant)
|
||||
|> changeset.api.create(
|
||||
|
@ -295,119 +303,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
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
|
||||
identities =
|
||||
relationship.destination
|
||||
|
@ -427,7 +322,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
opts
|
||||
) do
|
||||
inputs = List.wrap(inputs)
|
||||
opts = sanitize_opts(relationship, opts)
|
||||
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
|
||||
pkeys = pkeys(relationship)
|
||||
original_value = List.wrap(Map.get(record, relationship.name))
|
||||
|
||||
|
@ -489,7 +384,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
index,
|
||||
opts
|
||||
) do
|
||||
opts = sanitize_opts(relationship, opts)
|
||||
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
|
||||
|
||||
identities =
|
||||
relationship.destination
|
||||
|
@ -619,7 +514,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
{:ok, input}
|
||||
else
|
||||
relationship.destination
|
||||
|> Ash.Query.for_read(read, input)
|
||||
|> Ash.Query.for_read(read, input, actor: actor)
|
||||
|> Ash.Query.filter(^keys)
|
||||
|> Ash.Query.set_context(relationship.context)
|
||||
|> Ash.Query.set_tenant(changeset.tenant)
|
||||
|
@ -714,7 +609,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
|
||||
relationship.through
|
||||
|> 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(
|
||||
relationship.source_field_on_join_table,
|
||||
Map.get(record, relationship.source_field)
|
||||
|
@ -768,7 +663,10 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
end
|
||||
|
||||
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(
|
||||
relationship.destination_field,
|
||||
Map.get(record, relationship.source_field)
|
||||
|
@ -818,7 +716,11 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
{:ok, input, [], []}
|
||||
else
|
||||
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(
|
||||
relationship.destination_field,
|
||||
Map.get(record, relationship.source_field)
|
||||
|
@ -861,7 +763,11 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
{:ok, input, []}
|
||||
else
|
||||
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_tenant(changeset.tenant)
|
||||
|> changeset.api.create(
|
||||
|
@ -878,7 +784,10 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
|
||||
relationship.through
|
||||
|> 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(
|
||||
relationship.source_field_on_join_table,
|
||||
Map.get(record, relationship.source_field)
|
||||
|
@ -967,7 +876,10 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
end
|
||||
|
||||
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_tenant(changeset.tenant)
|
||||
|> 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)
|
||||
|
||||
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_tenant(changeset.tenant)
|
||||
|> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true)
|
||||
|
@ -1028,7 +943,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
)
|
||||
|
||||
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_tenant(changeset.tenant)
|
||||
|> api.update(
|
||||
|
@ -1057,7 +972,13 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
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.any?(pkeys, fn pkey ->
|
||||
matches?(current_value, input, pkey, relationship)
|
||||
|
@ -1166,7 +1087,8 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
result
|
||||
|> Ash.Changeset.for_destroy(
|
||||
join_action_name,
|
||||
%{}
|
||||
%{},
|
||||
actor: actor
|
||||
)
|
||||
|> Ash.Changeset.set_context(join_relationship.context)
|
||||
|> Ash.Changeset.set_tenant(changeset.tenant)
|
||||
|
@ -1180,7 +1102,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
notifications = join_notifications ++ all_notifications
|
||||
|
||||
record
|
||||
|> Ash.Changeset.for_destroy(action_name, %{})
|
||||
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
||||
|> Ash.Changeset.set_context(relationship.context)
|
||||
|> Ash.Changeset.set_tenant(changeset.tenant)
|
||||
|> api.destroy(
|
||||
|
@ -1209,7 +1131,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
|
||||
{:destroy, action_name} ->
|
||||
record
|
||||
|> Ash.Changeset.for_destroy(action_name, %{})
|
||||
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
||||
|> Ash.Changeset.set_context(relationship.context)
|
||||
|> Ash.Changeset.set_tenant(changeset.tenant)
|
||||
|> api.destroy(
|
||||
|
@ -1280,7 +1202,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
|> case do
|
||||
{:ok, 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_tenant(tenant)
|
||||
|> api.destroy(
|
||||
|
@ -1316,7 +1238,10 @@ defmodule Ash.Actions.ManagedRelationships do
|
|||
action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name
|
||||
|
||||
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.set_context(relationship.context)
|
||||
|> 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
|
||||
|
||||
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.set_context(relationship.context)
|
||||
|> Ash.Changeset.set_tenant(tenant)
|
||||
|
|
|
@ -250,24 +250,31 @@ defmodule Ash.Changeset do
|
|||
end
|
||||
end
|
||||
|
||||
@manage_types [:replace, :append, :remove, :direct_control, :create]
|
||||
|
||||
@for_create_opts [
|
||||
relationships: [
|
||||
type: :any,
|
||||
doc: """
|
||||
customize relationship behavior.
|
||||
|
||||
By default, any relationships are *replaced* via `replace_relationship`. To change this behavior, provide the
|
||||
`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`.
|
||||
By default, any relationships are ignored. There are three ways to change relationships with this function:
|
||||
|
||||
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
|
||||
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?: [
|
||||
|
@ -444,7 +451,6 @@ defmodule Ash.Changeset do
|
|||
|> Map.put(:__validated_for_action__, action.name)
|
||||
|> cast_params(action, params || %{}, opts)
|
||||
|> validate_attributes_accepted(action)
|
||||
|> validate_relationships_accepted(action)
|
||||
|> cast_arguments(action, opts[:defaults], true)
|
||||
|> run_action_changes(action, opts[:actor])
|
||||
|> set_defaults(changeset.action_type, false)
|
||||
|
@ -533,20 +539,17 @@ defmodule Ash.Changeset do
|
|||
|
||||
rel = Ash.Resource.Info.public_relationship(changeset.resource, name) ->
|
||||
if rel.writable? do
|
||||
behaviour = opts[:relationships][rel.name] || :replace
|
||||
behaviour = opts[:relationships][rel.name]
|
||||
|
||||
case behaviour do
|
||||
:replace ->
|
||||
replace_relationship(changeset, rel.name, value)
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
:append ->
|
||||
append_to_relationship(changeset, rel.name, value)
|
||||
type when is_atom(type) ->
|
||||
manage_relationship(changeset, rel.name, value, type: type)
|
||||
|
||||
:remove ->
|
||||
remove_from_relationship(changeset, rel.name, value)
|
||||
|
||||
{:manage, opts} ->
|
||||
manage_relationship(changeset, rel.name, value, opts)
|
||||
{:manage, manage_opts} ->
|
||||
manage_relationship(changeset, rel.name, value, manage_opts)
|
||||
end
|
||||
else
|
||||
changeset
|
||||
|
@ -581,24 +584,6 @@ defmodule Ash.Changeset do
|
|||
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
|
||||
Enum.reduce(changes, changeset, fn
|
||||
%{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), 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 [
|
||||
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?: [
|
||||
type: :boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
doc:
|
||||
"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
|
||||
"""
|
||||
],
|
||||
relationships: [
|
||||
type: :any,
|
||||
default: [],
|
||||
doc: "A keyword list of instructions for nested relationships."
|
||||
],
|
||||
meta: [
|
||||
type: :any,
|
||||
doc:
|
||||
|
@ -1267,7 +1347,18 @@ defmodule Ash.Changeset do
|
|||
end
|
||||
|
||||
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
|
||||
nil ->
|
||||
|
@ -1350,6 +1441,10 @@ defmodule Ash.Changeset do
|
|||
end
|
||||
end
|
||||
|
||||
defp map_input_to_list(input) when input == %{} do
|
||||
:error
|
||||
end
|
||||
|
||||
defp map_input_to_list(input) do
|
||||
input
|
||||
|> Enum.reduce_while({:ok, []}, fn
|
||||
|
@ -1538,30 +1633,31 @@ defmodule Ash.Changeset do
|
|||
"""
|
||||
def set_argument(changeset, argument, value) do
|
||||
if changeset.action do
|
||||
with {:arg, argument} when not is_nil(argument) <-
|
||||
{:arg,
|
||||
Enum.find(
|
||||
changeset.action.arguments,
|
||||
&(&1.name == argument || to_string(&1.name) == argument)
|
||||
)},
|
||||
{:ok, casted} <- cast_input(argument.type, value, argument.constraints),
|
||||
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
|
||||
{:constrained,
|
||||
Ash.Type.apply_constraints(argument.type, casted, argument.constraints),
|
||||
argument} do
|
||||
%{changeset | arguments: Map.put(changeset.arguments, argument.name, casted)}
|
||||
argument =
|
||||
Enum.find(
|
||||
changeset.action.arguments,
|
||||
&(&1.name == argument || to_string(&1.name) == argument)
|
||||
)
|
||||
|
||||
if argument do
|
||||
with {:ok, casted} <- cast_input(argument.type, value, argument.constraints),
|
||||
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
|
||||
{:constrained,
|
||||
Ash.Type.apply_constraints(argument.type, casted, argument.constraints),
|
||||
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
|
||||
{:arg, nil} ->
|
||||
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)
|
||||
%{changeset | arguments: Map.put(changeset.arguments, argument, value)}
|
||||
end
|
||||
else
|
||||
%{changeset | arguments: Map.put(changeset.arguments, argument, value)}
|
||||
|
|
131
lib/ash/changeset/managed_relationship_helpers.ex
Normal file
131
lib/ash/changeset/managed_relationship_helpers.ex
Normal 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
|
|
@ -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}
|
||||
```
|
||||
"""
|
||||
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,
|
||||
[argument: argument, relationship: relationship_name, opts: opts]}
|
||||
end
|
||||
|
|
|
@ -6,18 +6,11 @@ defmodule Ash.Resource.Transformers.DefaultAccept do
|
|||
alias Ash.Dsl.Transformer
|
||||
|
||||
def transform(resource, dsl_state) do
|
||||
default_accept_attributes =
|
||||
default_accept =
|
||||
resource
|
||||
|> Ash.Resource.Info.public_attributes()
|
||||
|> 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
|
||||
|> Transformer.get_entities([:actions])
|
||||
|> Enum.reject(&(&1.type == :read))
|
||||
|
|
|
@ -570,7 +570,7 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
|> Api.create!()
|
||||
|
||||
ProfileWithBelongsTo
|
||||
|> Ash.Changeset.for_create(:create, author: author)
|
||||
|> Ash.Changeset.for_create(:create, [author: author], relationships: [author: :replace])
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue