Packages

ash

1.23.2
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.Changeset
alias Ash.Engine.Request
require Ash.Query
def handle_relationship_changes(changeset) do
Enum.reduce(changeset.relationships, changeset, fn {name, data}, changeset ->
relationship = Ash.Resource.relationship(changeset.resource, name)
add_relationship_read_requests(changeset, relationship, data)
end)
end
defp add_relationship_read_requests(%{action_type: :update} = changeset, relationship, input) do
changeset
|> add_replace_requests(relationship, input)
|> add_remove_requests(relationship, input)
|> add_add_requests(relationship, input)
|> add_belongs_to_change(relationship, input)
end
defp add_relationship_read_requests(%{action_type: :create} = changeset, relationship, input) do
{input, key} =
if relationship.cardinality == :many do
case Map.fetch(input, :replace) do
{:ok, replacing} ->
{Map.update(input, :add, replacing, &Kernel.++(&1, replacing)), :add}
_ ->
{input, :add}
end
else
{input, :replace}
end
changeset
|> add_add_requests(relationship, Map.delete(input, :remove), key)
|> add_belongs_to_change(relationship, input)
end
defp add_belongs_to_change(
changeset,
%{type: :belongs_to, source_field: source_field, destination_field: destination_field},
input
) do
case Map.fetch(input, :replace) do
{:ok, nil} ->
changeset
:error ->
changeset
{:ok, replace} ->
add_belongs_to_change(changeset, replace, source_field, destination_field)
end
end
defp add_belongs_to_change(changeset, _, _), do: changeset
defp add_add_requests(changeset, relationship, input, key \\ :add) do
case Map.fetch(input, key) do
{:ok, identifiers} ->
do_add_relationship_read_requests(changeset, relationship, identifiers, key)
:error ->
changeset
end
end
defp add_belongs_to_change(changeset, identifiers, source_field, destination_field) do
case Map.get(identifiers, destination_field) do
{:ok, field_value} ->
Changeset.force_change_attribute(changeset, source_field, field_value)
_ ->
changeset
end
end
defp add_replace_requests(changeset, relationship, input) do
case Map.fetch(input, :replace) do
{:ok, identifiers} ->
changeset
|> do_add_relationship_read_requests(relationship, identifiers, :replace)
|> add_relationship_currently_related_request(relationship)
:error ->
changeset
end
end
defp add_remove_requests(changeset, relationship, input) do
case Map.fetch(input, :remove) do
{:ok, identifiers} ->
do_add_relationship_read_requests(
changeset,
relationship,
identifiers,
:remove
)
:error ->
changeset
end
end
defp do_add_relationship_read_requests(
changeset,
%{destination: destination} = relationship,
identifiers,
type
) do
relationship_name = relationship.name
{possible?, filter} =
case identifiers do
[{single_identifier, _changeset}] ->
{true, single_identifier}
[single_identifier] ->
{true, single_identifier}
nil ->
{false, []}
[] ->
{false, []}
single when is_map(single) ->
{true, Map.to_list(single)}
many ->
case Ash.Resource.primary_key(relationship.destination) do
[field] ->
{true, [{field, in: get_many_field(many, field)}]}
_ ->
{true, [or: get_many_records(many)]}
end
end
query = Ash.Query.filter(destination, ^filter)
dependencies =
if possible? do
[[:relationships, relationship_name, type, :query]]
else
[]
end
request =
Request.new(
api: changeset.api,
resource: relationship.destination,
action: Ash.Resource.primary_action!(relationship.destination, :read),
query: query,
path: [:relationships, relationship_name, type],
async?: not possible?,
authorize?: possible?,
data:
get_in(changeset.context, [
:destination_entities,
relationship.name,
relationship.destination
]) ||
Request.resolve(dependencies, fn data ->
if possible? do
query = get_in(data, [:relationships, relationship_name, type, :query])
primary_key = Ash.Resource.primary_key(query.resource)
query =
if changeset.tenant do
Ash.Query.set_tenant(query, changeset.tenant)
else
query
end
with {:ok, results} <- Ash.Actions.Read.unpaginated_read(query),
:ok <-
ensure_all_found(
changeset,
relationship_name,
type,
relationship.cardinality,
destination,
primary_key,
results
) do
{:ok, add_changes_to_results(changeset.resource, results, identifiers)}
end
else
{:ok, []}
end
end),
name: "read prior to write related #{relationship.name}"
)
changeset
|> Changeset.add_requests(request)
|> Changeset.changes_depend_on([:relationships, relationship_name, type, :data])
end
defp ensure_all_found(
changeset,
relationship_name,
type,
cardinality,
destination,
primary_key,
results
) do
search_keys =
case cardinality do
:one ->
changeset.relationships
|> get_in([relationship_name, type])
|> List.wrap()
:many ->
changeset.relationships
|> get_in([relationship_name, type])
|> Kernel.||([])
end
search_keys
|> Enum.map(fn
{pkey, %Ash.Changeset{}} ->
Map.take(pkey, primary_key)
pkey ->
Map.take(pkey, primary_key)
end)
|> Enum.reject(fn pkey ->
Enum.any?(results, fn result ->
result
|> Map.take(primary_key)
|> Kernel.==(pkey)
end)
end)
|> case do
[] ->
:ok
not_found ->
{:error,
Enum.map(not_found, fn key ->
Ash.Error.Query.NotFound.exception(resource: destination, primary_key: key)
end)}
end
end
defp add_changes_to_results(resource, results, identifiers) do
pkey = Ash.Resource.primary_key(resource)
Enum.map(results, fn result ->
case find_changes(identifiers, result, pkey) do
nil ->
result
changes ->
{result, changes}
end
end)
end
defp find_changes(identifiers, result, pkey) do
Enum.find_value(identifiers, fn
{identifier, changes} ->
cond do
is_map(identifier) && Map.take(identifier, pkey) == Map.take(result, pkey) ->
changes
match?([_], pkey) && identifier == Map.get(result, List.first(pkey)) ->
changes
true ->
nil
end
_ ->
nil
end)
end
defp get_many_field(records, field) do
Enum.map(records, fn
{record, _changes} ->
Map.get(record, field)
record ->
Map.get(record, field)
end)
end
defp get_many_records(records) do
Enum.map(records, fn
{record, _changes} ->
record
record ->
record
end)
end
def changeset(changeset) do
if changeset.relationships == %{} do
changeset
else
Request.resolve(changeset.change_dependencies, fn data ->
new_changeset =
data
|> Map.get(:relationships, %{})
|> Enum.reduce(changeset, fn {relationship, relationship_data}, changeset ->
relationship = Ash.Resource.relationship(changeset.resource, relationship)
relationship_data =
relationship_data
|> Enum.into(%{}, fn {key, value} ->
{key, value.data}
end)
|> Map.put_new(:current, [])
add_relationship_to_changeset(changeset, relationship, relationship_data)
end)
{:ok, new_changeset}
end)
end
end
defp add_relationship_to_changeset(
changeset,
%{type: :has_one} = relationship,
relationship_data
) do
case relationship_data do
%{current: [current], replace: []} ->
changeset
|> unrelate_has_one(relationship, current)
|> relate_has_one(relationship, nil)
|> add_relationship_change_metadata(relationship.name, %{current: current, replace: nil})
%{current: [current], replace: [new]} ->
changeset
|> unrelate_has_one(relationship, current)
|> relate_has_one(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{current: current, replace: new})
%{current: [], replace: [new]} ->
changeset
|> relate_has_one(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{current: nil, replace: new})
end
end
defp add_relationship_to_changeset(
changeset,
%{type: :belongs_to} = relationship,
relationship_data
) do
relationship_data = Map.put_new(relationship_data, :current, [])
case relationship_data do
%{current: [current], replace: []} ->
changeset
|> relate_belongs_to(relationship, nil)
|> add_relationship_change_metadata(relationship.name, %{current: current, replace: nil})
%{current: [], replace: [new]} ->
changeset
|> relate_belongs_to(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{current: nil, replace: new})
%{current: [current], replace: [new]} ->
changeset
|> relate_belongs_to(relationship, new)
|> add_relationship_change_metadata(relationship.name, %{
current: current,
replace: new
})
end
end
defp add_relationship_to_changeset(
changeset,
%{type: :has_many, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.Resource.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(relationship, relationship_data, pkey)
|> relate_has_many(relationship, relationship_data, pkey)
|> add_relationship_change_metadata(relationship.name, relationship_data)
end
defp add_relationship_to_changeset(
changeset,
%{type: :many_to_many, destination: destination} = relationship,
relationship_data
) do
pkey = Ash.Resource.primary_key(destination)
join_pkey = Ash.Resource.primary_key(relationship.through)
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(relationship, relationship_data, join_pkey, pkey)
|> relate_many_to_many(relationship, relationship_data, pkey)
|> add_relationship_change_metadata(relationship.name, relationship_data)
end
defp split_relationship_data(current, replace, pkey) do
adding =
Enum.reject(replace, fn
{_, _} ->
false
replacing ->
any_pkey_matches?(current, replacing, pkey)
end)
removing = Enum.reject(current, &any_pkey_matches?(replace, &1, pkey))
%{add: adding, remove: removing, current: current}
end
defp relate_many_to_many(
changeset,
relationship,
%{add: add, current: current},
pkey
)
when is_list(add) do
Enum.reduce(add, changeset, fn
{to_relate_record, join_changeset}, changeset ->
do_relate_many_to_many(changeset, relationship, to_relate_record, join_changeset)
to_relate_record, changeset ->
case find_pkey_match(current, to_relate_record, pkey) do
nil ->
do_relate_many_to_many(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(changeset, relationship, to_relate_record, join_changeset \\ nil) do
Changeset.after_action(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)
}
join_changeset
|> Kernel.||(Ash.Changeset.new(relationship.through))
|> Ash.Changeset.force_change_attributes(join_attrs)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create(upsert?: true, return_notifications?: true)
|> case do
{:ok, join_row, notifications} ->
{:ok,
record
|> remove_from_set_relationship(
relationship.name,
to_relate_record,
Ash.Resource.primary_key(relationship.destination)
)
|> add_to_set_relationship(relationship.name, to_relate_record)
|> remove_from_set_relationship(
relationship.join_relationship,
join_row,
Ash.Resource.primary_key(relationship.through)
)
|> add_to_set_relationship(relationship.join_relationship, join_row), notifications}
{:error, error} ->
{:error, error}
end
end)
end
defp remove_many_to_many(
changeset,
relationship,
%{current: current, remove: remove},
join_pkey,
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(changeset, relationship, to_remove_record, join_pkey, pkey)
end
end)
end
defp remove_many_to_many(changeset, _, _, _, _) do
changeset
end
defp do_remove_many_to_many(changeset, relationship, to_remove_record, join_pkey, pkey) do
Changeset.after_action(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)
}
]
relationship.through
|> Ash.Query.set_tenant(changeset.tenant)
|> Ash.Query.filter(^filter)
|> changeset.api.read_one()
|> case do
{:ok, nil} ->
changeset
{:error, error} ->
{:error, error}
{:ok, join_row} ->
destroy_and_remove(
changeset.api,
join_row,
to_remove_record,
record,
relationship,
join_pkey,
pkey,
changeset.tenant
)
end
end)
end
defp destroy_and_remove(
api,
join_row,
to_remove_record,
record,
relationship,
join_pkey,
pkey,
tenant
) do
join_row
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:ok,
record
|> remove_from_set_relationship(relationship.join_relationship, join_row, join_pkey)
|> remove_from_set_relationship(relationship.name, to_remove_record, pkey),
notifications}
{:error, error} ->
{:error, error}
end
end
defp relate_has_many(changeset, 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
Changeset.after_action(changeset, fn changeset, record ->
to_relate_record
|> Ash.Changeset.new()
|> Ash.Changeset.force_change_attribute(
relationship.destination_field,
Map.get(record, relationship.source_field)
)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.update(return_notifications?: true)
|> case do
{:ok, related, notifications} ->
{:ok, add_to_set_relationship(record, relationship.name, related), notifications}
{:error, error} ->
{:error, error}
end
end)
end
end)
end
defp relate_has_many(changeset, _, _, _) do
changeset
end
defp remove_has_many(changeset, 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
Changeset.after_action(changeset, fn changeset, record ->
to_relate_record
|> Ash.Changeset.new()
|> Ash.Changeset.force_change_attribute(relationship.destination_field, nil)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.update(return_notifications?: true)
|> case do
{:ok, related, notifications} ->
{:ok, remove_from_set_relationship(record, relationship.name, related, pkey),
notifications}
{: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, _changes} ->
Map.take(record, pkey) == search_pkey
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, clear_relationships(value))
end
end)
end
defp add_to_set_relationship(record, relationship_name, to_relate) do
Map.update!(record, relationship_name, fn
%Ash.NotLoaded{type: :relationship} -> [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
%Ash.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
Changeset.force_change_attribute(
changeset,
relationship.source_field,
Map.get(new, relationship.destination_field)
)
else
Changeset.force_change_attribute(changeset, relationship.source_field, nil)
end
Changeset.after_action(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, relationship, to_relate_record) do
Changeset.after_action(changeset, fn _changeset, record ->
if to_relate_record do
to_relate_record
|> Ash.Changeset.new()
|> Ash.Changeset.force_change_attribute(
relationship.destination_field,
Map.get(record, relationship.source_field)
)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.update(return_notifications?: true)
|> case do
{:ok, related, notifications} ->
{:ok, Map.put(record, relationship.name, clear_relationships(related)), notifications}
{:error, error} ->
{:error, error}
end
else
{:ok, Map.put(record, relationship.name, nil)}
end
end)
end
defp unrelate_has_one(changeset, relationship, to_relate_record) do
Changeset.after_action(changeset, fn changeset, record ->
to_relate_record
|> Changeset.new()
|> Changeset.force_change_attribute(relationship.destination_field, nil)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.update(return_notifications?: true)
|> case do
{:ok, _related, notifications} ->
{:ok, record, notifications}
{:error, error} ->
{:error, error}
end
end)
end
defp add_relationship_change_metadata(changeset, relationship_name, data) do
Map.update(
changeset,
:relationships,
%{relationship_name => data},
&Map.put(&1, relationship_name, data)
)
end
defp add_relationship_currently_related_request(
changeset,
%{type: :many_to_many} = relationship
) do
join_through_request = many_to_many_join_resource_request(changeset, relationship)
destination_request = many_to_many_destination_request(changeset, relationship)
requests = [join_through_request, destination_request]
changeset
|> Changeset.add_requests(requests)
|> Changeset.changes_depend_on([:relationships, relationship.name, :current, :data])
end
defp add_relationship_currently_related_request(
changeset,
%{destination: destination} = relationship
) do
value = Changeset.get_attribute(changeset, relationship.source_field)
filter_statement = [{relationship.destination_field, value}]
request =
Request.new(
api: changeset.api,
resource: destination,
action: Ash.Resource.primary_action!(relationship.destination, :read),
path: [:relationships, relationship.name, :current],
query: Ash.Query.filter(destination, ^filter_statement),
data:
Request.resolve([[:relationships, relationship.name, :current, :query]], fn data ->
query = get_in(data, [:relationships, relationship.name, :current, :query])
query =
if changeset.tenant do
Ash.Query.set_tenant(query, changeset.tenant)
else
query
end
Ash.Actions.Read.unpaginated_read(query)
end),
name: "Read related #{relationship.name} before replace"
)
changeset
|> Changeset.add_requests(request)
|> Changeset.changes_depend_on([:relationships, relationship.name, :current, :data])
end
defp many_to_many_join_resource_request(
changeset,
%{through: through} = relationship
) do
value = Changeset.get_attribute(changeset, relationship.source_field)
filter_statement = [{relationship.source_field_on_join_table, value}]
Request.new(
api: changeset.api,
resource: through,
action: Ash.Resource.primary_action!(relationship.destination, :read),
path: [:relationships, relationship.name, :current_join],
query: Ash.Query.filter(through, ^filter_statement),
data:
Request.resolve([[:relationships, relationship.name, :current_join, :query]], fn data ->
query = get_in(data, [:relationships, relationship.name, :current_join, :query])
query =
if changeset.tenant do
Ash.Query.set_tenant(query, changeset.tenant)
else
query
end
Ash.Actions.Read.unpaginated_read(query)
end),
name: "Read related join for #{relationship.name} before replace"
)
end
defp many_to_many_destination_request(
changeset,
%{destination: destination, name: name} = relationship
) do
Request.new(
api: changeset.api,
resource: destination,
action: Ash.Resource.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
|> Ash.Query.new(changeset.api)
|> Ash.Query.filter(^filter_statement)}
end
),
data:
Request.resolve(
[[:relationships, name, :current, :query]],
fn %{
relationships: %{
^name => %{current: %{query: query}}
}
} ->
query =
if changeset.tenant do
Ash.Query.set_tenant(query, changeset.tenant)
else
query
end
Ash.Actions.Read.unpaginated_read(query)
end
),
name: "Read related join for #{name} before replace"
)
end
defp clear_relationships(%resource{} = record) do
resource
|> Ash.Resource.relationships()
|> Enum.reduce(record, fn relationship, record ->
not_loaded = %Ash.NotLoaded{
type: :relationship,
field: relationship.name
}
Map.put(record, relationship.name, not_loaded)
end)
end
end