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, 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)

View file

@ -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)}

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}
```
"""
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

View file

@ -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))

View file

@ -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