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, %{relationships: nil}, _), do: {:ok, created}
|
||||||
|
|
||||||
def load(api, created, changeset, opts) do
|
def load(api, created, changeset, opts) do
|
||||||
|
if Ash.Changeset.ManagedRelationshipHelpers.must_load?(opts) do
|
||||||
api.load(created, Map.keys(changeset.relationships),
|
api.load(created, Map.keys(changeset.relationships),
|
||||||
authorize?: opts[:authorize?],
|
authorize?: opts[:authorize?],
|
||||||
actor: opts[:actor]
|
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)
|
||||||
|
|
|
@ -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,22 +1633,20 @@ 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
|
||||||
|
with {:ok, casted} <- cast_input(argument.type, value, argument.constraints),
|
||||||
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
|
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
|
||||||
{:constrained,
|
{:constrained,
|
||||||
Ash.Type.apply_constraints(argument.type, casted, argument.constraints),
|
Ash.Type.apply_constraints(argument.type, casted, argument.constraints),
|
||||||
argument} do
|
argument} do
|
||||||
%{changeset | arguments: Map.put(changeset.arguments, argument.name, casted)}
|
%{changeset | arguments: Map.put(changeset.arguments, argument.name, casted)}
|
||||||
else
|
else
|
||||||
{:arg, nil} ->
|
|
||||||
changeset
|
|
||||||
|
|
||||||
{:constrained, {:ok, nil}, _argument} ->
|
{:constrained, {:ok, nil}, _argument} ->
|
||||||
changeset
|
changeset
|
||||||
|
|
||||||
|
@ -1566,6 +1659,9 @@ defmodule Ash.Changeset do
|
||||||
else
|
else
|
||||||
%{changeset | arguments: Map.put(changeset.arguments, argument, value)}
|
%{changeset | arguments: Map.put(changeset.arguments, argument, value)}
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
%{changeset | arguments: Map.put(changeset.arguments, argument, value)}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
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}
|
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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue