mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
improvement: add no_fields?
relationships
This commit is contained in:
parent
98a7ac06b8
commit
904968b936
10 changed files with 163 additions and 258 deletions
|
@ -97,6 +97,7 @@ locals_without_parens = [
|
||||||
modify_query: 1,
|
modify_query: 1,
|
||||||
module: 1,
|
module: 1,
|
||||||
name: 1,
|
name: 1,
|
||||||
|
no_fields?: 1,
|
||||||
not_found_message: 1,
|
not_found_message: 1,
|
||||||
on: 1,
|
on: 1,
|
||||||
only_when_valid?: 1,
|
only_when_valid?: 1,
|
||||||
|
|
|
@ -74,8 +74,15 @@ defmodule Ash.Actions.Load do
|
||||||
related_query.tenant
|
related_query.tenant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
query =
|
||||||
|
if Map.get(relationship, :no_fields?) do
|
||||||
|
query
|
||||||
|
else
|
||||||
|
Ash.Query.ensure_selected(query, relationship.source_field)
|
||||||
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
Ash.Query.ensure_selected(query, relationship.source_field),
|
query,
|
||||||
requests ++
|
requests ++
|
||||||
further_requests ++
|
further_requests ++
|
||||||
do_requests(
|
do_requests(
|
||||||
|
@ -132,6 +139,12 @@ defmodule Ash.Actions.Load do
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp attach_to_many_loads(value, %{name: name, no_fields?: true}, data, lead_path) do
|
||||||
|
map_or_update(data, lead_path, fn record ->
|
||||||
|
Map.put(record, name, List.wrap(value))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp attach_to_many_loads(value, last_relationship, data, lead_path) when is_map(value) do
|
defp attach_to_many_loads(value, last_relationship, data, lead_path) when is_map(value) do
|
||||||
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
|
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
|
||||||
|
|
||||||
|
@ -150,6 +163,12 @@ defmodule Ash.Actions.Load do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp attach_to_one_loads(value, %{name: name, no_fields?: true}, data, lead_path) do
|
||||||
|
map_or_update(data, lead_path, fn record ->
|
||||||
|
Map.put(record, name, value |> List.wrap() |> Enum.at(0))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp attach_to_one_loads(value, last_relationship, data, lead_path) when is_map(value) do
|
defp attach_to_one_loads(value, last_relationship, data, lead_path) when is_map(value) do
|
||||||
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
|
primary_key = Ash.Resource.Info.primary_key(last_relationship.source)
|
||||||
|
|
||||||
|
@ -340,10 +359,7 @@ defmodule Ash.Actions.Load do
|
||||||
query:
|
query:
|
||||||
load_query(
|
load_query(
|
||||||
relationship,
|
relationship,
|
||||||
related_query,
|
related_query
|
||||||
path,
|
|
||||||
root_query,
|
|
||||||
request_path
|
|
||||||
),
|
),
|
||||||
data:
|
data:
|
||||||
data(
|
data(
|
||||||
|
@ -601,10 +617,7 @@ defmodule Ash.Actions.Load do
|
||||||
query:
|
query:
|
||||||
load_query(
|
load_query(
|
||||||
join_relationship,
|
join_relationship,
|
||||||
related_query,
|
related_query
|
||||||
Enum.reverse(join_relationship_path),
|
|
||||||
root_query,
|
|
||||||
request_path
|
|
||||||
),
|
),
|
||||||
data:
|
data:
|
||||||
Request.resolve(dependencies, fn
|
Request.resolve(dependencies, fn
|
||||||
|
@ -966,106 +979,25 @@ defmodule Ash.Actions.Load do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_query_with_reverse_path(
|
defp load_query(%{manual: manual}, related_query)
|
||||||
root_query,
|
|
||||||
related_query,
|
|
||||||
reverse_path,
|
|
||||||
root_data_filter
|
|
||||||
) do
|
|
||||||
case Ash.Filter.parse(root_query.resource, root_query.filter, %{}, %{}) do
|
|
||||||
{:ok, nil} ->
|
|
||||||
related_query
|
|
||||||
|> Ash.Query.unset(:load)
|
|
||||||
|> Ash.Query.filter(
|
|
||||||
^put_nested_relationship(
|
|
||||||
[],
|
|
||||||
reverse_path,
|
|
||||||
root_data_filter,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> Ash.Query.filter(^related_query.filter)
|
|
||||||
|> extract_errors()
|
|
||||||
|
|
||||||
{:ok, parsed} ->
|
|
||||||
filter =
|
|
||||||
put_nested_relationship(
|
|
||||||
[],
|
|
||||||
reverse_path,
|
|
||||||
root_data_filter,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
related_query
|
|
||||||
|> Ash.Query.unset(:load)
|
|
||||||
|> Ash.Query.filter(^filter)
|
|
||||||
|> Ash.Query.filter(^put_nested_relationship([], reverse_path, parsed, false))
|
|
||||||
|> Ash.Query.filter(^related_query.filter)
|
|
||||||
|> extract_errors()
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp load_query(%{manual: manual}, related_query, _path, _root_query, _request_path)
|
|
||||||
when not is_nil(manual) do
|
when not is_nil(manual) do
|
||||||
related_query
|
related_query
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_query(
|
defp load_query(
|
||||||
relationship,
|
relationship,
|
||||||
related_query,
|
related_query
|
||||||
path,
|
|
||||||
root_query,
|
|
||||||
request_path
|
|
||||||
) do
|
) do
|
||||||
Request.resolve([request_path ++ [:data]], fn context ->
|
if Map.get(relationship, :no_fields?) do
|
||||||
data =
|
relationship.destination
|
||||||
case get_in(context, request_path ++ [:data, :results]) do
|
|> Ash.Query.new(related_query.api)
|
||||||
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
|
else
|
||||||
results
|
relationship.destination
|
||||||
|
|> Ash.Query.new(related_query.api)
|
||||||
data ->
|
|> Ash.Query.filter(^related_query.filter)
|
||||||
data
|
end
|
||||||
end
|
|
||||||
|
|
||||||
root_data_filter =
|
|
||||||
case data do
|
|
||||||
[] ->
|
|
||||||
false
|
|
||||||
|
|
||||||
[%resource{} | _] = items ->
|
|
||||||
pkey = Ash.Resource.Info.primary_key(resource)
|
|
||||||
[or: Enum.map(items, fn item -> item |> Map.take(pkey) |> Enum.to_list() end)]
|
|
||||||
end
|
|
||||||
|
|
||||||
path = Enum.reverse([relationship.name | Enum.map(path, & &1.name)])
|
|
||||||
|
|
||||||
case Ash.Resource.Info.reverse_relationship(
|
|
||||||
root_query.resource,
|
|
||||||
path
|
|
||||||
) do
|
|
||||||
nil ->
|
|
||||||
relationship.destination
|
|
||||||
|> Ash.Query.new(related_query.api)
|
|
||||||
|> Ash.Query.filter(^related_query.filter)
|
|
||||||
|> extract_errors()
|
|
||||||
|
|
||||||
reverse_path ->
|
|
||||||
load_query_with_reverse_path(
|
|
||||||
root_query,
|
|
||||||
related_query,
|
|
||||||
reverse_path,
|
|
||||||
root_data_filter
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_errors(%{errors: []} = item), do: {:ok, item}
|
|
||||||
defp extract_errors(%{errors: errors}), do: {:error, errors}
|
|
||||||
|
|
||||||
defp true_load_query(relationship, query, data, path, request_path) do
|
defp true_load_query(relationship, query, data, path, request_path) do
|
||||||
{source_field, path} =
|
{source_field, path} =
|
||||||
if relationship.type == :many_to_many do
|
if relationship.type == :many_to_many do
|
||||||
|
@ -1106,47 +1038,52 @@ defmodule Ash.Actions.Load do
|
||||||
defp get_query(query, relationship, source_data, source_field) do
|
defp get_query(query, relationship, source_data, source_field) do
|
||||||
{offset, limit} = offset_and_limit(query)
|
{offset, limit} = offset_and_limit(query)
|
||||||
|
|
||||||
if lateral_join?(query, relationship, source_data) do
|
cond do
|
||||||
{:ok, Ash.Query.unset(query, :load)}
|
lateral_join?(query, relationship, source_data) ->
|
||||||
else
|
{:ok, Ash.Query.unset(query, :load)}
|
||||||
query =
|
|
||||||
if limit || offset do
|
|
||||||
Ash.Query.unset(query, [:limit, :offset])
|
|
||||||
else
|
|
||||||
query
|
|
||||||
end
|
|
||||||
|
|
||||||
related_data =
|
Map.get(relationship, :no_fields?) ->
|
||||||
case source_data do
|
{:ok, query}
|
||||||
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
|
|
||||||
results
|
|
||||||
|
|
||||||
data ->
|
true ->
|
||||||
|
query =
|
||||||
|
if limit || offset do
|
||||||
|
Ash.Query.unset(query, [:limit, :offset])
|
||||||
|
else
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
related_data =
|
||||||
|
case source_data do
|
||||||
|
%page{results: results} when page in [Ash.Page.Keyset, Ash.Page.Offset] ->
|
||||||
|
results
|
||||||
|
|
||||||
|
data ->
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
ids =
|
||||||
|
Enum.flat_map(related_data, fn data ->
|
||||||
data
|
data
|
||||||
end
|
|> Map.get(source_field)
|
||||||
|
|> List.wrap()
|
||||||
|
end)
|
||||||
|
|
||||||
ids =
|
filter_value =
|
||||||
Enum.flat_map(related_data, fn data ->
|
case ids do
|
||||||
data
|
[id] ->
|
||||||
|> Map.get(source_field)
|
id
|
||||||
|> List.wrap()
|
|
||||||
end)
|
|
||||||
|
|
||||||
filter_value =
|
ids ->
|
||||||
case ids do
|
[in: ids]
|
||||||
[id] ->
|
end
|
||||||
id
|
|
||||||
|
|
||||||
ids ->
|
new_query =
|
||||||
[in: ids]
|
query
|
||||||
end
|
|> Ash.Query.filter(^[{relationship.destination_field, filter_value}])
|
||||||
|
|> Ash.Query.unset(:load)
|
||||||
|
|
||||||
new_query =
|
{:ok, new_query}
|
||||||
query
|
|
||||||
|> Ash.Query.filter(^[{relationship.destination_field, filter_value}])
|
|
||||||
|> Ash.Query.unset(:load)
|
|
||||||
|
|
||||||
{:ok, new_query}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1238,30 +1175,4 @@ defmodule Ash.Actions.Load do
|
||||||
is_nil(destination_rel.context) &&
|
is_nil(destination_rel.context) &&
|
||||||
is_nil(rel.context)
|
is_nil(rel.context)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp put_nested_relationship(request_filter, path, value, records?) when not is_list(value) do
|
|
||||||
put_nested_relationship(request_filter, path, [value], records?)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp put_nested_relationship(request_filter, [rel | rest], values, records?) do
|
|
||||||
[
|
|
||||||
{rel, put_nested_relationship(request_filter, rest, values, records?)}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp put_nested_relationship(request_filter, [], [{field, value}], _) do
|
|
||||||
[{field, value} | request_filter]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp put_nested_relationship(request_filter, [], [{field, _} | _] = keys, _) do
|
|
||||||
[{field, [{:in, Enum.map(keys, &elem(&1, 1))}]} | request_filter]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp put_nested_relationship(request_filter, [], [values], _) do
|
|
||||||
List.wrap(request_filter) ++ List.wrap(values)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp put_nested_relationship(request_filter, [], values, _) do
|
|
||||||
Keyword.update(request_filter, :or, values, &Kernel.++(&1, values))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,7 +102,8 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
if input in [nil, []] && opts[:on_missing] != :ignore do
|
if input in [nil, []] && opts[:on_missing] != :ignore do
|
||||||
Ash.Changeset.force_change_attribute(changeset, relationship.source_field, nil)
|
changeset
|
||||||
|
|> maybe_force_change_attribute(relationship, :source_field, nil)
|
||||||
|> Ash.Changeset.after_action(fn _changeset, result ->
|
|> Ash.Changeset.after_action(fn _changeset, result ->
|
||||||
{:ok, Map.put(result, relationship.name, nil)}
|
{:ok, Map.put(result, relationship.name, nil)}
|
||||||
end)
|
end)
|
||||||
|
@ -173,8 +174,9 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
belongs_to_manage_found: %{relationship.name => %{index => input}}
|
belongs_to_manage_found: %{relationship.name => %{index => input}}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.source_field,
|
relationship,
|
||||||
|
:source_field,
|
||||||
Map.get(input, relationship.destination_field)
|
Map.get(input, relationship.destination_field)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -216,8 +218,9 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.source_field,
|
relationship,
|
||||||
|
:source_field,
|
||||||
Map.get(found, relationship.destination_field)
|
Map.get(found, relationship.destination_field)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -247,9 +250,10 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
_value ->
|
_value ->
|
||||||
if opts[:on_match] == :destroy do
|
if opts[:on_match] == :destroy do
|
||||||
changeset =
|
changeset =
|
||||||
Ash.Changeset.force_change_attribute(
|
maybe_force_change_attribute(
|
||||||
changeset,
|
changeset,
|
||||||
relationship.source_field,
|
relationship,
|
||||||
|
:source_field,
|
||||||
nil
|
nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -287,6 +291,16 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
relationship.api || changeset.api
|
relationship.api || changeset.api
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_force_change_attribute(changeset, %{no_fields?: true}, _, _), do: changeset
|
||||||
|
|
||||||
|
defp maybe_force_change_attribute(changeset, relationship, key, value) do
|
||||||
|
Ash.Changeset.force_change_attribute(
|
||||||
|
changeset,
|
||||||
|
Map.get(relationship, key),
|
||||||
|
value
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp validate_required_belongs_to({:error, error}), do: {:error, error}
|
defp validate_required_belongs_to({:error, error}), do: {:error, error}
|
||||||
|
|
||||||
defp validate_required_belongs_to({changeset, instructions}) do
|
defp validate_required_belongs_to({changeset, instructions}) do
|
||||||
|
@ -413,8 +427,9 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
belongs_to_manage_created: %{relationship.name => %{index => created}}
|
belongs_to_manage_created: %{relationship.name => %{index => created}}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.source_field,
|
relationship,
|
||||||
|
:source_field,
|
||||||
Map.get(created, relationship.destination_field)
|
Map.get(created, relationship.destination_field)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -859,12 +874,14 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
relationship.through
|
relationship.through
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.for_create(create_or_update, join_input, actor: actor)
|
|> Ash.Changeset.for_create(create_or_update, join_input, actor: actor)
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.source_field_on_join_table,
|
relationship,
|
||||||
|
:source_field_on_join_table,
|
||||||
Map.get(record, relationship.source_field)
|
Map.get(record, relationship.source_field)
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.destination_field_on_join_table,
|
relationship,
|
||||||
|
:destination_field_on_join_table,
|
||||||
Map.get(found, relationship.destination_field)
|
Map.get(found, relationship.destination_field)
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.set_context(join_relationship.context)
|
|> Ash.Changeset.set_context(join_relationship.context)
|
||||||
|
@ -913,13 +930,13 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
|
|
||||||
found
|
found
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_update(create_or_update, input,
|
|> Ash.Changeset.for_update(create_or_update, input,
|
||||||
relationships: opts[:relationships] || [],
|
relationships: opts[:relationships] || [],
|
||||||
actor: actor
|
actor: actor
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.destination_field,
|
relationship,
|
||||||
|
:destination_field,
|
||||||
Map.get(record, relationship.source_field)
|
Map.get(record, relationship.source_field)
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|
@ -968,14 +985,14 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
else
|
else
|
||||||
relationship.destination
|
relationship.destination
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_create(action_name, input,
|
|> Ash.Changeset.for_create(action_name, input,
|
||||||
require?: false,
|
require?: false,
|
||||||
actor: actor,
|
actor: actor,
|
||||||
relationships: opts[:relationships]
|
relationships: opts[:relationships]
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.destination_field,
|
relationship,
|
||||||
|
:destination_field,
|
||||||
Map.get(record, relationship.source_field)
|
Map.get(record, relationship.source_field)
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|
@ -1017,7 +1034,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
else
|
else
|
||||||
relationship.destination
|
relationship.destination
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_create(action_name, regular_params,
|
|> Ash.Changeset.for_create(action_name, regular_params,
|
||||||
require?: false,
|
require?: false,
|
||||||
relationships: opts[:relationships],
|
relationships: opts[:relationships],
|
||||||
|
@ -1043,12 +1059,14 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
require?: false,
|
require?: false,
|
||||||
actor: actor
|
actor: actor
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.source_field_on_join_table,
|
relationship,
|
||||||
|
:source_field_on_join_table,
|
||||||
Map.get(record, relationship.source_field)
|
Map.get(record, relationship.source_field)
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.force_change_attribute(
|
|> maybe_force_change_attribute(
|
||||||
relationship.destination_field_on_join_table,
|
relationship,
|
||||||
|
:destination_field_on_join_table,
|
||||||
Map.get(created, relationship.destination_field)
|
Map.get(created, relationship.destination_field)
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.set_context(join_relationship.context)
|
|> Ash.Changeset.set_context(join_relationship.context)
|
||||||
|
@ -1114,8 +1132,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
changeset.tenant,
|
changeset.tenant,
|
||||||
relationship,
|
relationship
|
||||||
changeset
|
|
||||||
) do
|
) do
|
||||||
{:ok, notifications} ->
|
{:ok, notifications} ->
|
||||||
{:ok, current_value, notifications, []}
|
{:ok, current_value, notifications, []}
|
||||||
|
@ -1133,8 +1150,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
changeset.tenant,
|
changeset.tenant,
|
||||||
relationship,
|
relationship
|
||||||
changeset
|
|
||||||
) do
|
) do
|
||||||
{:ok, notifications} ->
|
{:ok, notifications} ->
|
||||||
{:ok, current_value, notifications, []}
|
{:ok, current_value, notifications, []}
|
||||||
|
@ -1153,7 +1169,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
|
|
||||||
match
|
match
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_update(action_name, input,
|
|> Ash.Changeset.for_update(action_name, input,
|
||||||
actor: actor,
|
actor: actor,
|
||||||
relationships: opts[:relationships] || []
|
relationships: opts[:relationships] || []
|
||||||
|
@ -1184,7 +1199,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
|
|
||||||
match
|
match
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_update(action_name, regular_params,
|
|> Ash.Changeset.for_update(action_name, regular_params,
|
||||||
actor: actor,
|
actor: actor,
|
||||||
relationships: opts[:relationships]
|
relationships: opts[:relationships]
|
||||||
|
@ -1327,32 +1341,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp set_source_context(changeset, {relationship, original_changeset}) do
|
|
||||||
case changeset.data.__metadata__[:manage_relationship_source] ||
|
|
||||||
original_changeset.context[:manage_relationship_source] do
|
|
||||||
nil ->
|
|
||||||
Ash.Changeset.set_context(changeset, %{
|
|
||||||
manage_relationship_source: [
|
|
||||||
{relationship.source, relationship.name, original_changeset}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
value ->
|
|
||||||
Ash.Changeset.set_context(changeset, %{
|
|
||||||
manage_relationship_source:
|
|
||||||
value ++ [{relationship.source, relationship.name, original_changeset}]
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|> Ash.Changeset.after_action(fn changeset, record ->
|
|
||||||
{:ok,
|
|
||||||
Ash.Resource.Info.put_metadata(
|
|
||||||
record,
|
|
||||||
:manage_relationship_source,
|
|
||||||
changeset.context[:manage_relationship_source]
|
|
||||||
)}
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp delete_unused(
|
defp delete_unused(
|
||||||
source_record,
|
source_record,
|
||||||
original_value,
|
original_value,
|
||||||
|
@ -1424,7 +1412,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
|
|
||||||
record
|
record
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|> Ash.Changeset.set_tenant(changeset.tenant)
|
|> Ash.Changeset.set_tenant(changeset.tenant)
|
||||||
|
@ -1456,7 +1443,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
{:destroy, action_name} ->
|
{:destroy, action_name} ->
|
||||||
record
|
record
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|> Ash.Changeset.set_tenant(changeset.tenant)
|
|> Ash.Changeset.set_tenant(changeset.tenant)
|
||||||
|
@ -1490,8 +1476,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
changeset.tenant,
|
changeset.tenant,
|
||||||
relationship,
|
relationship
|
||||||
changeset
|
|
||||||
) do
|
) do
|
||||||
{:ok, notifications} ->
|
{:ok, notifications} ->
|
||||||
{:cont, {:ok, current_value, notifications}}
|
{:cont, {:ok, current_value, notifications}}
|
||||||
|
@ -1512,8 +1497,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
tenant,
|
tenant,
|
||||||
%{type: :many_to_many} = relationship,
|
%{type: :many_to_many} = relationship
|
||||||
changeset
|
|
||||||
) do
|
) do
|
||||||
action_name =
|
action_name =
|
||||||
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
|
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
|
||||||
|
@ -1531,7 +1515,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
{:ok, result} ->
|
{:ok, result} ->
|
||||||
result
|
result
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|> Ash.Changeset.set_tenant(tenant)
|
|> Ash.Changeset.set_tenant(tenant)
|
||||||
|
@ -1561,8 +1544,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
tenant,
|
tenant,
|
||||||
%{type: type} = relationship,
|
%{type: type} = relationship
|
||||||
changeset
|
|
||||||
)
|
)
|
||||||
when type in [:has_many, :has_one] do
|
when type in [:has_many, :has_one] do
|
||||||
action_name =
|
action_name =
|
||||||
|
@ -1570,12 +1552,11 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
|
|
||||||
record
|
record
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_update(action_name, %{},
|
|> Ash.Changeset.for_update(action_name, %{},
|
||||||
relationships: opts[:relationships] || [],
|
relationships: opts[:relationships] || [],
|
||||||
actor: actor
|
actor: actor
|
||||||
)
|
)
|
||||||
|> Ash.Changeset.force_change_attribute(relationship.destination_field, nil)
|
|> maybe_force_change_attribute(relationship, :destination_field, nil)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|> Ash.Changeset.set_tenant(tenant)
|
|> Ash.Changeset.set_tenant(tenant)
|
||||||
|> api.update(return_notifications?: true, actor: actor, authorize?: opts[:authorize?])
|
|> api.update(return_notifications?: true, actor: actor, authorize?: opts[:authorize?])
|
||||||
|
@ -1596,8 +1577,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
_opts,
|
_opts,
|
||||||
_action_name,
|
_action_name,
|
||||||
_tenant,
|
_tenant,
|
||||||
%{type: :belongs_to},
|
%{type: :belongs_to}
|
||||||
_changeset
|
|
||||||
) do
|
) do
|
||||||
{:ok, []}
|
{:ok, []}
|
||||||
end
|
end
|
||||||
|
@ -1610,8 +1590,7 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
tenant,
|
tenant,
|
||||||
%{type: :many_to_many} = relationship,
|
%{type: :many_to_many} = relationship
|
||||||
changeset
|
|
||||||
) do
|
) do
|
||||||
action_name =
|
action_name =
|
||||||
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
|
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
|
||||||
|
@ -1629,7 +1608,6 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
{:ok, result} ->
|
{:ok, result} ->
|
||||||
result
|
result
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|
||||||
|> Ash.Changeset.set_context(relationship.context)
|
|> Ash.Changeset.set_context(relationship.context)
|
||||||
|> Ash.Changeset.set_tenant(tenant)
|
|> Ash.Changeset.set_tenant(tenant)
|
||||||
|
@ -1659,15 +1637,13 @@ defmodule Ash.Actions.ManagedRelationships do
|
||||||
opts,
|
opts,
|
||||||
action_name,
|
action_name,
|
||||||
tenant,
|
tenant,
|
||||||
relationship,
|
relationship
|
||||||
changeset
|
|
||||||
) do
|
) do
|
||||||
action_name =
|
action_name =
|
||||||
action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name
|
action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name
|
||||||
|
|
||||||
record
|
record
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> set_source_context({relationship, changeset})
|
|
||||||
|> Ash.Changeset.for_destroy(action_name, %{},
|
|> Ash.Changeset.for_destroy(action_name, %{},
|
||||||
relationships: opts[:relationships] || [],
|
relationships: opts[:relationships] || [],
|
||||||
actor: actor
|
actor: actor
|
||||||
|
|
|
@ -74,7 +74,7 @@ defmodule Ash.Changeset do
|
||||||
if context == %{} do
|
if context == %{} do
|
||||||
empty()
|
empty()
|
||||||
else
|
else
|
||||||
concat("context: ", to_doc(sanitize_context(context), opts))
|
concat("context: ", to_doc(context, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
tenant =
|
tenant =
|
||||||
|
@ -112,23 +112,6 @@ defmodule Ash.Changeset do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sanitize_context(%{manage_relationship_source: manage_relationship_source} = context) do
|
|
||||||
sanitized_managed_relationship_source =
|
|
||||||
manage_relationship_source
|
|
||||||
|> Enum.reverse()
|
|
||||||
|> Enum.map_join(" -> ", fn {resource, rel, _} ->
|
|
||||||
"#{inspect(resource)}.#{rel}"
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{
|
|
||||||
context
|
|
||||||
| manage_relationship_source:
|
|
||||||
"#manage_relationship_source<#{sanitized_managed_relationship_source}>"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp sanitize_context(context), do: context
|
|
||||||
|
|
||||||
defp arguments(changeset, opts) do
|
defp arguments(changeset, opts) do
|
||||||
if changeset.action do
|
if changeset.action do
|
||||||
if Enum.empty?(changeset.action.arguments) do
|
if Enum.empty?(changeset.action.arguments) do
|
||||||
|
|
|
@ -153,7 +153,8 @@ defmodule Ash.Engine.Request do
|
||||||
nil
|
nil
|
||||||
|
|
||||||
other ->
|
other ->
|
||||||
raise "Got a weird thing #{inspect(other)}"
|
raise ArgumentError,
|
||||||
|
message: "Unexpected value passed to `Ash.Query.new/1`: #{inspect(other)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
data =
|
data =
|
||||||
|
|
|
@ -17,6 +17,7 @@ defmodule Ash.Resource.Relationships.HasMany do
|
||||||
:violation_message,
|
:violation_message,
|
||||||
:manual,
|
:manual,
|
||||||
:api,
|
:api,
|
||||||
|
no_fields?: false,
|
||||||
could_be_related_at_creation?: false,
|
could_be_related_at_creation?: false,
|
||||||
validate_destination_field?: true,
|
validate_destination_field?: true,
|
||||||
cardinality: :many,
|
cardinality: :many,
|
||||||
|
@ -30,6 +31,7 @@ defmodule Ash.Resource.Relationships.HasMany do
|
||||||
writable?: boolean,
|
writable?: boolean,
|
||||||
read_action: atom,
|
read_action: atom,
|
||||||
filter: Ash.Filter.t() | nil,
|
filter: Ash.Filter.t() | nil,
|
||||||
|
no_fields?: boolean,
|
||||||
name: atom,
|
name: atom,
|
||||||
type: Ash.Type.t(),
|
type: Ash.Type.t(),
|
||||||
destination: Ash.Resource.t(),
|
destination: Ash.Resource.t(),
|
||||||
|
@ -48,7 +50,8 @@ defmodule Ash.Resource.Relationships.HasMany do
|
||||||
|
|
||||||
@opt_schema Ash.OptionsHelpers.merge_schemas(
|
@opt_schema Ash.OptionsHelpers.merge_schemas(
|
||||||
[
|
[
|
||||||
manual()
|
manual(),
|
||||||
|
no_fields()
|
||||||
],
|
],
|
||||||
@global_opts,
|
@global_opts,
|
||||||
"Relationship Options"
|
"Relationship Options"
|
||||||
|
|
|
@ -19,6 +19,7 @@ defmodule Ash.Resource.Relationships.HasOne do
|
||||||
:not_found_message,
|
:not_found_message,
|
||||||
:violation_message,
|
:violation_message,
|
||||||
:manual,
|
:manual,
|
||||||
|
no_fields?: false,
|
||||||
could_be_related_at_creation?: false,
|
could_be_related_at_creation?: false,
|
||||||
validate_destination_field?: true,
|
validate_destination_field?: true,
|
||||||
cardinality: :one,
|
cardinality: :one,
|
||||||
|
@ -33,6 +34,7 @@ defmodule Ash.Resource.Relationships.HasOne do
|
||||||
writable?: boolean,
|
writable?: boolean,
|
||||||
name: atom,
|
name: atom,
|
||||||
read_action: atom,
|
read_action: atom,
|
||||||
|
no_fields?: boolean,
|
||||||
type: Ash.Type.t(),
|
type: Ash.Type.t(),
|
||||||
filter: Ash.Filter.t() | nil,
|
filter: Ash.Filter.t() | nil,
|
||||||
destination: Ash.Resource.t(),
|
destination: Ash.Resource.t(),
|
||||||
|
@ -51,7 +53,7 @@ defmodule Ash.Resource.Relationships.HasOne do
|
||||||
|> OptionsHelpers.set_default!(:source_field, :id)
|
|> OptionsHelpers.set_default!(:source_field, :id)
|
||||||
|
|
||||||
@opt_schema Ash.OptionsHelpers.merge_schemas(
|
@opt_schema Ash.OptionsHelpers.merge_schemas(
|
||||||
[manual()] ++
|
[manual(), no_fields()] ++
|
||||||
[
|
[
|
||||||
required?: [
|
required?: [
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
|
|
|
@ -109,6 +109,32 @@ defmodule Ash.Resource.Relationships.SharedOptions do
|
||||||
@shared_options
|
@shared_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def no_fields do
|
||||||
|
{:no_fields?,
|
||||||
|
[
|
||||||
|
type: :boolean,
|
||||||
|
doc: """
|
||||||
|
If true, all existing entities are considered related, i.e this relationship is not based on any fields, and `source_field` and
|
||||||
|
`destination_field` are ignored.
|
||||||
|
|
||||||
|
This can be very useful when combined with multitenancy. Specifically, if you have a tenant resource like `Organization`,
|
||||||
|
you can use `no_fields?` to do things like `has_many :employees, Employee, no_fields?: true`, which lets you avoid having an
|
||||||
|
unnecessary `organization_id` field on `Employee`. The same works in reverse: `has_one :organization, Organization, no_fields?: true`
|
||||||
|
allows relating the employee to their organization.
|
||||||
|
|
||||||
|
Some important caveats here:
|
||||||
|
|
||||||
|
1. You can still manage relationships from one to the other, but "relate" and "unrelate"
|
||||||
|
will have no effect, because there are no fields to change.
|
||||||
|
|
||||||
|
2. Loading the relationship on a list of resources will not behave as expected in all circumstances involving multitenancy. For example,
|
||||||
|
if you get a list of `Organization` and then try to load `employees`, you would need to set a single tenant on the load query, meaning
|
||||||
|
you'll get all organizations back with the set of employees from one tenant. This could eventually be solved, but for now it is considered an
|
||||||
|
edge case.
|
||||||
|
"""
|
||||||
|
]}
|
||||||
|
end
|
||||||
|
|
||||||
def manual do
|
def manual do
|
||||||
{:manual,
|
{:manual,
|
||||||
type: {:ash_behaviour, Ash.Resource.ManualRelationship},
|
type: {:ash_behaviour, Ash.Resource.ManualRelationship},
|
||||||
|
|
|
@ -16,7 +16,9 @@ defmodule Ash.Resource.Transformers.ValidateRelationshipAttributes do
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.relationships()
|
|> Ash.Resource.Info.relationships()
|
||||||
|> Enum.reject(&Map.get(&1, :manual))
|
|> Enum.reject(fn relationship ->
|
||||||
|
Map.get(relationship, :manual) || Map.get(relationship, :no_fields?)
|
||||||
|
end)
|
||||||
|> Enum.filter(& &1.validate_destination_field?)
|
|> Enum.filter(& &1.validate_destination_field?)
|
||||||
|> Enum.each(&validate_relationship(&1, attribute_names, resource))
|
|> Enum.each(&validate_relationship(&1, attribute_names, resource))
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ defmodule Ash.Test.CalculationTest do
|
||||||
defmodule BestFriendsName do
|
defmodule BestFriendsName do
|
||||||
use Ash.Calculation
|
use Ash.Calculation
|
||||||
|
|
||||||
def load(_query, opts, _) do
|
def load(_query, _opts, _) do
|
||||||
[best_friend: :full_name]
|
[best_friend: :full_name]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue