From 2f9fafcbc7cff4b4d2f0ef38fd3df64981d8520d Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 25 Mar 2021 11:33:32 -0400 Subject: [PATCH] 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 --- lib/ash/actions/managed_relationships.ex | 204 ++++++---------- lib/ash/changeset/changeset.ex | 220 +++++++++++++----- .../changeset/managed_relationship_helpers.ex | 131 +++++++++++ lib/ash/resource/change/builtins.ex | 4 +- .../resource/transformers/default_accept.ex | 9 +- test/actions/create_test.exs | 2 +- 6 files changed, 360 insertions(+), 210 deletions(-) create mode 100644 lib/ash/changeset/managed_relationship_helpers.ex diff --git a/lib/ash/actions/managed_relationships.ex b/lib/ash/actions/managed_relationships.ex index fcdf1104..63a23b8b 100644 --- a/lib/ash/actions/managed_relationships.ex +++ b/lib/ash/actions/managed_relationships.ex @@ -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) diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 0752b2e9..f4102c63 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -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)} diff --git a/lib/ash/changeset/managed_relationship_helpers.ex b/lib/ash/changeset/managed_relationship_helpers.ex new file mode 100644 index 00000000..41ae301f --- /dev/null +++ b/lib/ash/changeset/managed_relationship_helpers.ex @@ -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 diff --git a/lib/ash/resource/change/builtins.ex b/lib/ash/resource/change/builtins.ex index c11abb6c..d8dfc4d1 100644 --- a/lib/ash/resource/change/builtins.ex +++ b/lib/ash/resource/change/builtins.ex @@ -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 diff --git a/lib/ash/resource/transformers/default_accept.ex b/lib/ash/resource/transformers/default_accept.ex index e2bee13c..153df9be 100644 --- a/lib/ash/resource/transformers/default_accept.ex +++ b/lib/ash/resource/transformers/default_accept.ex @@ -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)) diff --git a/test/actions/create_test.exs b/test/actions/create_test.exs index 248421e1..51d5daac 100644 --- a/test/actions/create_test.exs +++ b/test/actions/create_test.exs @@ -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