Packages

ash

0.6.1
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash actions relationships.ex
Raw

lib/ash/actions/relationships.ex

defmodule Ash.Actions.Relationships do
@moduledoc false
alias Ash.Actions.PrimaryKeyHelpers
alias Ash.Engine.Request
def validate_not_changing_relationship_and_source_field(relationships, attributes, resource) do
resource
|> Ash.relationships()
|> Enum.reduce_while({:ok, relationships}, fn relationship, {:ok, relationships} ->
with {:ok, _} <- fetch_string_or_atom(attributes, relationship.source_field),
{:ok, _} <- fetch_string_or_atom(relationships, relationship.name) do
{:halt,
{:error,
"Providing data for relationship #{relationship.name} and changing its source field"}}
else
_ ->
{:cont, {:ok, relationships}}
end
end)
end
def field_changes_into_relationship_changes(
relationships,
attributes,
resource
) do
resource
|> Ash.relationships()
|> Enum.reduce_while({:ok, attributes, relationships}, fn
%{
type: :belongs_to,
source_field: source_field,
name: name,
destination_field: destination_field
},
{:ok, attr_acc, relationships} ->
case fetch_string_or_atom(attributes, source_field) do
{:ok, value} ->
new_attr_acc =
attr_acc
|> Map.delete(source_field)
|> Map.delete(to_string(source_field))
{:cont,
{:ok, new_attr_acc, Map.put(relationships, name, [{destination_field, value}])}}
:error ->
{:cont, {:ok, attr_acc, relationships}}
end
_, {:ok, attr_acc, relationships} ->
{:cont, {:ok, attr_acc, relationships}}
end)
end
def handle_relationship_changes(changeset, api, relationships_input, action_type) do
Enum.reduce(relationships_input, changeset, fn {name, data}, changeset ->
case Ash.relationship(changeset.data.__struct__, name) do
nil ->
Ecto.Changeset.add_error(changeset, name, "Invalid relationship")
relationship ->
do_handle_relationship_changes(api, changeset, relationship, data, action_type)
end
end)
end
defp do_handle_relationship_changes(api, changeset, relationship, data, action_type) do
case validate_relationship_change(relationship, data, action_type) do
{:ok, input} ->
add_relationship_read_requests(changeset, api, relationship, input, action_type)
{:error, error} ->
{:error, error}
end
end
defp add_relationship_read_requests(changeset, api, relationship, input, :update) do
changeset
|> add_replace_requests(api, relationship, input)
|> add_remove_requests(api, relationship, input)
|> add_add_requests(api, relationship, input)
end
defp add_relationship_read_requests(changeset, api, relationship, input, :create) do
input =
case Map.fetch(input, :replace) do
{:ok, replacing} ->
Map.update(input, :add, replacing, &Kernel.++(&1, replacing))
_ ->
input
end
add_add_requests(changeset, api, relationship, input)
end
defp add_add_requests(changeset, api, relationship, input) do
case Map.fetch(input, :add) do
{:ok, identifiers} ->
changeset =
case relationship do
%{type: :belongs_to, source_field: source_field, destination_field: destination_field} ->
add_belongs_to_change(changeset, identifiers, source_field, destination_field)
_ ->
changeset
end
do_add_relationship_read_requests(changeset, api, relationship, identifiers, :add)
:error ->
changeset
end
end
defp add_belongs_to_change(changeset, identifiers, source_field, destination_field) do
case Keyword.fetch(identifiers, destination_field) do
{:ok, field_value} ->
Ecto.Changeset.put_change(changeset, source_field, field_value)
_ ->
changeset
end
end
defp add_replace_requests(changeset, api, relationship, input) do
case Map.fetch(input, :replace) do
{:ok, identifiers} ->
changeset
|> do_add_relationship_read_requests(api, relationship, identifiers, :replace)
|> add_relationship_currently_related_request(api, relationship)
:error ->
changeset
end
end
defp add_remove_requests(changeset, api, relationship, input) do
case Map.fetch(input, :remove) do
{:ok, identifiers} ->
changeset =
case relationship do
%{type: :belongs_to, source_field: source_field} ->
Ecto.Changeset.put_change(changeset, source_field, nil)
_ ->
changeset
end
do_add_relationship_read_requests(
changeset,
api,
relationship,
identifiers,
:remove
)
:error ->
changeset
end
end
defp do_add_relationship_read_requests(
changeset,
api,
%{destination: destination} = relationship,
identifiers,
type
) do
relationship_name = relationship.name
{possible?, filter} =
case identifiers do
[single_identifier] ->
if Keyword.keyword?(single_identifier) do
{true, single_identifier}
else
{true, [single_identifier]}
end
[] ->
{false, []}
many ->
{true, [or: many]}
end
query =
api
|> Ash.Query.new(destination)
|> Ash.Query.filter(filter)
request =
Request.new(
api: api,
resource: relationship.destination,
action: Ash.primary_action!(relationship.destination, :read),
query: query,
path: [:relationships, relationship_name, type],
authorize?: possible?,
data:
Request.resolve(fn _data ->
if possible? do
query
|> api.read()
else
[]
end
end),
name: "read prior to write related #{relationship.name}"
)
changeset
|> add_requests(request)
|> changes_depend_on([:relationships, relationship_name, type, :data])
end
defp validate_relationship_change(relationship, data, action_type) do
keyword? = Keyword.keyword?(data)
cond do
relationship.cardinality == :many && keyword? && data != [] ->
{:error, "Relationship change invalid for #{relationship.name} 1"}
keyword? ->
{:ok, %{replace: [data]}}
is_list(data) ->
validate_list_replace(relationship, data)
!is_map(data) ->
validate_map_replace(relationship, data)
Map.get(data, :__struct__) ->
validate_struct_replace(relationship, data, action_type)
true ->
validate_specified_replace(relationship, data, action_type)
end
end
defp validate_specified_replace(relationship, data, action_type) do
cond do
Map.has_key?(data, :remove) and action_type == :create ->
{:error, "Cannot remove from a relationship on create."}
Map.has_key?(data, :replace) and action_type == :create ->
data = Map.put(data, :add, data.replace)
validate_relationship_change(relationship, data, action_type)
relationship.cardinality == :one ->
validate_to_one_relationship_data(relationship, data)
relationship.cardinality == :many ->
validate_to_many_relationship_data(relationship, data)
end
end
defp validate_struct_replace(relationship, data, action_type) do
new_data =
data
|> Map.take(Ash.primary_key(data.__struct__))
|> Map.to_list()
validate_relationship_change(relationship, new_data, action_type)
end
defp validate_map_replace(relationship, data) do
case PrimaryKeyHelpers.value_to_primary_key_filter(
relationship.destination,
data
) do
{:ok, identifier} ->
{:ok, %{replace: identifier}}
{:error, _} ->
{:error, "Relationship change invalid map for #{relationship.name}"}
end
end
defp validate_list_replace(relationship, data) do
case PrimaryKeyHelpers.values_to_primary_key_filters(
relationship.destination,
data
) do
{:ok, identifiers} ->
{:ok, %{replace: identifiers}}
{:error, _} ->
{:error, "Relationship change invalid list for #{relationship.name}"}
end
end
defp validate_to_one_relationship_data(%{destination: destination}, data) do
keys = Enum.sort(Map.keys(data))
case keys do
[:add, :remove] ->
with {:ok, add} <-
PrimaryKeyHelpers.value_to_primary_key_filter(destination, data.add),
{:remove, remove} <-
PrimaryKeyHelpers.value_to_primary_key_filter(destination, data.remove) do
{:ok, %{add: add, remove: remove}}
end
[key] when key in [:add, :replace] ->
case PrimaryKeyHelpers.value_to_primary_key_filter(
destination,
Map.get(data, key)
) do
{:ok, add} -> {:ok, %{replace: add}}
{:error, error} -> {:error, error}
end
[:remove] ->
case PrimaryKeyHelpers.value_to_primary_key_filter(destination, data.remove) do
{:ok, remove} -> {:ok, %{remove: remove}}
{:error, error} -> {:error, error}
end
end
end
defp validate_to_many_relationship_data(%{destination: destination}, data) do
keys = Enum.sort(Map.keys(data))
case keys do
[:add, :remove] ->
with {:ok, add} <-
PrimaryKeyHelpers.values_to_primary_key_filters(destination, data.add),
{:remove, remove} <-
PrimaryKeyHelpers.values_to_primary_key_filters(
destination,
data.remove
) do
{:ok, %{add: add, remove: remove}}
end
[key] when key in [:add, :replace, :remove] ->
case PrimaryKeyHelpers.values_to_primary_key_filters(
destination,
Map.get(data, key)
) do
{:ok, add} -> {:ok, %{key => add}}
{:error, error} -> {:error, error}
end
end
end
def changeset(changeset, api, relationships) do
if relationships == %{} do
changeset
else
dependencies = Map.get(changeset, :__changes_depend_on__, [])
Request.resolve(dependencies, fn data ->
new_changeset =
data
|> Map.get(:relationships, %{})
|> Enum.reduce(changeset, fn {relationship, relationship_data}, changeset ->
relationship = Ash.relationship(changeset.data.__struct__, relationship)
relationship_data =
relationship_data
|> Enum.into(%{}, fn {key, value} ->
{key, value.data}
end)
|> Map.put_new(:current, [])
add_relationship_to_changeset(changeset, api, relationship, relationship_data)
end)
{:ok, new_changeset}
end)
end
end
defp add_relationship_to_changeset(
changeset,
api,
%{type: :has_one, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.primary_key(destination)
case relationship_data do
%{current: [], replace: [new]} ->
changeset
|> relate_has_one(api, relationship, new)
|> add_relationship_change_metadata(relationship.name, %{add: [new]})
%{current: [current], replace: []} ->
changeset
|> unrelate_has_one(api, relationship, current)
|> relate_has_one(api, relationship, nil)
|> add_relationship_change_metadata(relationship.name, %{remove: [current]})
%{current: [current], replace: [new]} ->
changeset
|> unrelate_has_one(api, relationship, current)
|> relate_has_one(api, relationship, new)
|> add_relationship_change_metadata(relationship.name, %{remove: [current], add: [new]})
%{current: [current], add: [add]} ->
if Map.take(current, pkey) == Map.take(add, pkey) do
relate_has_one(changeset, api, relationship, current)
else
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't add a value to a belongs to when something is already related."
)
end
%{current: [], remove: [_]} ->
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't remove a value from a belongs to when nothing is related"
)
%{current: [current], remove: [remove]} ->
if Map.take(current, pkey) == Map.take(remove, pkey) do
changeset
|> unrelate_has_one(api, relationship, current)
|> relate_has_one(api, relationship, nil)
|> add_relationship_change_metadata(relationship.name, %{remove: [current]})
else
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't remove a has_one related value if a different record is related"
)
end
%{add: [new]} ->
changeset
|> relate_has_one(api, relationship, new)
|> add_relationship_change_metadata(relationship.name, %{add: [new]})
end
end
defp add_relationship_to_changeset(
changeset,
_api,
%{type: :belongs_to, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.primary_key(destination)
relationship_data = Map.put_new(relationship_data, :current, [])
case relationship_data do
%{current: [], replace: [new]} ->
changeset
|> relate_belongs_to(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{add: [new]})
%{current: [], add: [new]} ->
changeset
|> relate_belongs_to(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{add: [new]})
%{current: [current], replace: []} ->
changeset
|> relate_belongs_to(relationship, nil)
|> add_relationship_change_metadata(relationship.name, %{remove: [current]})
%{current: [current], replace: [new]} ->
changeset
|> relate_belongs_to(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{remove: [current], add: [new]})
%{current: [current], add: [add]} ->
if Map.take(current, pkey) == Map.take(add, pkey) do
relate_belongs_to(changeset, relationship, current)
else
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't add a value to a belongs to when something is already related."
)
end
%{current: [], remove: [_]} ->
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't remove a value from a belongs to when nothing is related"
)
%{current: [current], remove: [remove]} ->
if Map.take(current, pkey) == Map.take(remove, pkey) do
changeset
|> add_relationship_change_metadata(relationship.name, %{remove: [current]})
|> relate_belongs_to(relationship, nil)
else
Ecto.Changeset.add_error(
changeset,
relationship.name,
"Can't remove a belongs_to related value if a different record is related"
)
end
end
end
defp add_relationship_to_changeset(
changeset,
api,
%{type: :has_many, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.primary_key(destination)
relationship_data =
case relationship_data do
%{replace: values, current: current} ->
split_relationship_data(current, values, pkey)
other ->
other
end
changeset
|> set_relationship(relationship.name, Map.get(relationship_data, :current, []))
|> remove_has_many(api, relationship, relationship_data, pkey)
|> relate_has_many(api, relationship, relationship_data, pkey)
end
defp add_relationship_to_changeset(
changeset,
api,
%{type: :many_to_many, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.primary_key(destination)
relationship_data =
case relationship_data do
%{replace: values, current: current} ->
split_relationship_data(current, values, pkey)
other ->
other
end
changeset
|> set_relationship(relationship.name, Map.get(relationship_data, :current, []))
|> remove_many_to_many(api, relationship, relationship_data, pkey)
|> relate_many_to_many(api, relationship, relationship_data, pkey)
end
defp split_relationship_data(current, replace, pkey) do
adding = Enum.reject(replace, &any_pkey_matches?(current, &1, pkey))
removing = Enum.reject(current, &any_pkey_matches?(replace, &1, pkey))
%{add: adding, remove: removing, current: current}
end
defp relate_many_to_many(changeset, api, relationship, %{add: add, current: current}, pkey)
when is_list(add) do
Enum.reduce(add, changeset, fn to_relate_record, changeset ->
case find_pkey_match(current, to_relate_record, pkey) do
nil ->
do_relate_many_to_many(api, changeset, relationship, to_relate_record)
_record ->
changeset
end
end)
end
defp relate_many_to_many(changeset, _, _, _, _) do
changeset
end
defp do_relate_many_to_many(api, changeset, relationship, to_relate_record) do
add_after_changes(changeset, fn _changeset, record ->
join_attrs = %{
relationship.source_field_on_join_table() => Map.get(record, relationship.source_field),
relationship.destination_field_on_join_table() =>
Map.get(to_relate_record, relationship.destination_field)
}
relationship.through
|> api.create(attributes: join_attrs, upsert?: true)
|> case do
{:ok, _join_row} ->
{:ok,
add_to_set_relationship(
record,
relationship.name,
to_relate_record
)}
{:error, error} ->
{:error, error}
end
end)
end
defp remove_many_to_many(
changeset,
api,
relationship,
%{current: current, remove: remove},
pkey
) do
Enum.reduce(remove, changeset, fn to_remove_record, changeset ->
case find_pkey_match(current, to_remove_record, pkey) do
nil ->
changeset
to_remove_record ->
do_remove_many_to_many(api, changeset, relationship, to_remove_record, pkey)
end
end)
end
defp remove_many_to_many(changeset, _, _, _, _) do
changeset
end
defp do_remove_many_to_many(api, changeset, relationship, to_remove_record, pkey) do
add_after_changes(changeset, fn _changeset, record ->
filter = [
{relationship.source_field_on_join_table, Map.get(record, relationship.source_field)},
{
relationship.destination_field_on_join_table,
Map.get(to_remove_record, relationship.destination_field)
}
]
case api.get(relationship.through, filter) do
{:ok, nil} ->
changeset
{:error, error} ->
{:error, error}
{:ok, found} ->
destroy_and_remove(api, found, record, relationship, pkey)
end
end)
end
defp destroy_and_remove(api, found, record, relationship, pkey) do
case api.destroy(found) do
:ok ->
{:ok,
remove_from_set_relationship(
record,
relationship.name,
found,
pkey
)}
{:error, error} ->
{:error, error}
end
end
defp relate_has_many(changeset, api, relationship, %{add: add, current: current}, pkey)
when is_list(add) do
Enum.reduce(add, changeset, fn to_relate_record, changeset ->
if any_pkey_matches?(current, to_relate_record, pkey) do
changeset
else
add_after_changes(changeset, fn _changeset, record ->
to_relate_record
|> api.update(
attributes: %{
relationship.destination_field => Map.get(record, relationship.source_field)
}
)
|> case do
{:ok, related} ->
{:ok, add_to_set_relationship(record, relationship.name, related)}
{:error, error} ->
{:error, error}
end
end)
end
end)
end
defp relate_has_many(changeset, _, _, _, _) do
changeset
end
defp remove_has_many(changeset, api, relationship, %{current: current, remove: remove}, pkey) do
Enum.reduce(remove, changeset, fn to_relate_record, changeset ->
if any_pkey_matches?(current, to_relate_record, pkey) do
add_after_changes(changeset, fn _changeset, record ->
to_relate_record
|> api.update(
attributes: %{
relationship.destination_field => nil
}
)
|> case do
{:ok, related} ->
{:ok, remove_from_set_relationship(record, relationship.name, related, pkey)}
{:error, error} ->
{:error, error}
end
end)
else
changeset
end
end)
end
defp remove_has_many(changeset, _, _, _, _) do
changeset
end
defp find_pkey_match(records, to_relate_record, pkey) do
search_pkey = Map.take(to_relate_record, pkey)
Enum.find(records, fn record ->
Map.take(record, pkey) == search_pkey
end)
end
defp any_pkey_matches?(records, to_relate_record, pkey) do
not is_nil(find_pkey_match(records, to_relate_record, pkey))
end
defp set_relationship(changeset, relationship_name, value) do
Map.update!(changeset, :data, fn data ->
case value do
values when is_list(values) ->
Map.put(data, relationship_name, Enum.map(values, &clear_relationships/1))
value ->
Map.put(data, relationship_name, value)
end
end)
end
defp add_to_set_relationship(record, relationship_name, to_relate) do
Map.update!(record, relationship_name, fn
%Ecto.Association.NotLoaded{} -> [clear_relationships(to_relate)]
set_relationship -> [clear_relationships(to_relate) | set_relationship]
end)
end
defp remove_from_set_relationship(record, relationship_name, to_remove, pkey) do
Map.update!(record, relationship_name, fn
%Ecto.Association.NotLoaded{} ->
[]
set_relationship ->
search_pkey = Map.take(to_remove, pkey)
Enum.reject(set_relationship, fn set -> Map.take(set, pkey) == search_pkey end)
end)
end
defp relate_belongs_to(changeset, relationship, new) do
changeset =
if new do
Ecto.Changeset.cast(
changeset,
%{
relationship.source_field => Map.get(new, relationship.destination_field)
},
[relationship.source_field]
)
else
Ecto.Changeset.cast(changeset, %{relationship.source_field => nil}, [
relationship.source_field
])
end
add_after_changes(changeset, fn _changeset, result ->
if new do
{:ok, Map.put(result, relationship.name, clear_relationships(new))}
else
{:ok, Map.put(result, relationship.name, nil)}
end
end)
end
defp relate_has_one(changeset, api, relationship, to_relate_record) do
add_after_changes(changeset, fn _changeset, record ->
if to_relate_record do
to_relate_record
|> api.update(
attributes: %{
relationship.destination_field => Map.get(record, relationship.source_field)
}
)
|> case do
{:ok, related} ->
{:ok, Map.put(record, relationship.name, clear_relationships(related))}
{:error, error} ->
{:error, error}
end
else
{:ok, Map.put(record, relationship.name, nil)}
end
end)
end
defp unrelate_has_one(changeset, api, relationship, to_relate_record) do
add_after_changes(changeset, fn _changeset, record ->
to_relate_record
|> api.update(
attributes: %{
relationship.destination_field => nil
}
)
|> case do
{:ok, _related} ->
{:ok, record}
{:error, error} ->
{:error, error}
end
end)
end
defp add_after_changes(changeset, func) do
Map.update(changeset, :__after_changes__, [func], fn funcs -> [func | funcs] end)
end
defp add_relationship_change_metadata(changeset, relationship_name, data) do
Map.update(
changeset,
:__ash_relationships__,
%{relationship_name => data},
&Map.put(&1, relationship_name, data)
)
end
defp fetch_string_or_atom(map, name) do
case Map.fetch(map, name) do
{:ok, value} -> {:ok, value}
:error -> Map.fetch(map, to_string(name))
end
end
defp add_relationship_currently_related_request(
changeset,
api,
%{type: :many_to_many} = relationship
) do
join_through_request = many_to_many_join_resource_request(api, changeset, relationship)
destination_request = many_to_many_destination_request(api, relationship)
requests = [join_through_request, destination_request]
changeset
|> add_requests(requests)
|> changes_depend_on([:relationships, relationship.name, :current, :data])
end
defp add_relationship_currently_related_request(
changeset,
api,
%{destination: destination} = relationship
) do
value = Ecto.Changeset.get_field(changeset, relationship.source_field)
filter_statement = [{relationship.destination_field, value}]
query =
api
|> Ash.Query.new(destination)
|> Ash.Query.filter(filter_statement)
request =
Request.new(
api: api,
resource: destination,
action: Ash.primary_action!(relationship.destination, :read),
path: [:relationships, relationship.name, :current],
query: query,
data:
Request.resolve(fn _data ->
api.read(query)
end),
name: "Read related #{relationship.name} before replace"
)
changeset
|> add_requests(request)
|> changes_depend_on([:relationships, relationship.name, :current, :data])
end
defp many_to_many_join_resource_request(
api,
changeset,
%{through: through} = relationship
) do
value = Ecto.Changeset.get_field(changeset, relationship.source_field)
filter_statement = [{relationship.source_field_on_join_table, value}]
query =
api
|> Ash.Query.new(through)
|> Ash.Query.filter(filter_statement)
Request.new(
api: api,
resource: through,
action: Ash.primary_action!(relationship.destination, :read),
path: [:relationships, relationship.name, :current_join],
query: query,
data:
Request.resolve(fn _data ->
api.read(query)
end),
name: "Read related join for #{relationship.name} before replace"
)
end
defp many_to_many_destination_request(
api,
%{destination: destination, name: name} = relationship
) do
Request.new(
api: api,
resource: destination,
action: Ash.primary_action!(relationship.destination, :read),
path: [:relationships, name, :current],
query:
Request.resolve(
[[:relationships, name, :current_join, :data]],
fn %{relationships: %{^name => %{current_join: %{data: current_join}}}} ->
field_values =
Enum.map(current_join, &Map.get(&1, relationship.destination_field_on_join_table))
filter_statement = [{relationship.destination_field, in: field_values}]
{:ok,
relationship.destination
|> api.query()
|> Ash.Query.filter(filter_statement)}
end
),
data:
Request.resolve(
[[:relationships, name, :current, :query]],
fn %{
relationships: %{
^name => %{current: %{query: query}}
}
} ->
api.read(query)
end
),
name: "Read related join for #{name} before replace"
)
end
defp changes_depend_on(changeset, path) do
Map.update(changeset, :__changes_depend_on__, [path], fn paths -> [path | paths] end)
end
defp add_requests(changeset, requests) do
requests = List.wrap(requests)
Map.update(changeset, :__requests__, requests, &Kernel.++(&1, requests))
end
defp clear_relationships(%resource{} = record) do
resource
|> Ash.relationships()
|> Enum.reduce(record, fn relationship, record ->
not_loaded = %Ecto.Association.NotLoaded{
__cardinality__: relationship.cardinality,
__field__: relationship.name,
__owner__: relationship.source
}
Map.put(record, relationship.name, not_loaded)
end)
end
end