Packages
ash_graphql
1.7.14
1.9.4
1.9.3
1.9.2
1.9.1
1.9.0
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.5
1.0.0-rc.4
retired
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.9
0.26.8
0.26.7
0.26.6
0.26.5
0.26.4
0.26.3
0.26.2
0.26.0
0.25.13
0.25.12
0.25.10
0.25.9
0.25.8
0.25.7
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.13
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
retired
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0-rc.3
0.20.0-rc.2
0.20.0-rc.1
0.20.0-rc.0
0.19.0
0.18.0-rc0
0.17.5
0.17.5-rc0
0.17.4
0.17.2
0.17.1
0.17.0
0.16.28
0.16.27
0.16.26
0.16.25
0.16.24
0.16.23
0.16.22
0.16.21
0.16.20
0.16.18-rc5
0.16.18-rc4
0.16.18-rc3
0.16.18-rc2
0.16.18-rc1
0.16.18-rc0
0.16.17
0.16.16
0.16.15
0.16.14
0.16.13
0.16.12
0.16.11
0.16.10
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.5
0.12.4
0.12.3
0.12.1
0.12.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
The extension for building GraphQL APIs with Ash
Current section
Files
Jump to
Current section
Files
lib/graphql/resolver.ex
defmodule AshGraphql.Graphql.Resolver do
@moduledoc false
require Logger
import Ash.Expr
require Ash.Query
import AshGraphql.TraceHelpers
import AshGraphql.ContextHelpers
def resolve(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve(
%{arguments: arguments, context: context} = resolution,
{domain, resource,
%AshGraphql.Resource.Action{
name: query_name,
action: action,
modify_resolution: modify,
error_location: error_location
}, mutation_args}
) do
action = Ash.Resource.Info.action(resource, action)
arguments_result =
if mutation_args do
handle_mutation_arguments(resource, action, nil, arguments, mutation_args)
else
handle_arguments(resource, action, arguments)
end
case arguments_result do
{:ok, arguments} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
source: :graphql,
query: query_name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
domain: domain,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
input =
%Ash.ActionInput{domain: domain, resource: resource}
|> Ash.ActionInput.set_context(get_context(context))
|> Ash.ActionInput.for_action(action.name, arguments)
result =
input
|> Ash.run_action(opts)
|> case do
:ok ->
{:ok, true}
{:ok, result} ->
load_opts =
[
actor: Map.get(context, :actor),
action: action,
domain: domain,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
if action.returns && Ash.Type.can_load?(action.returns, action.constraints) do
{fields, path} = nested_fields_and_path(resolution, [], [])
loads =
type_loads(
fields,
context,
action.returns,
action.constraints,
load_opts,
resource,
action.name,
resolution,
path,
hd(resolution.path),
nil
)
case loads do
[] ->
{:ok, result}
loads ->
Ash.Type.load(
action.returns,
result,
loads,
action.constraints,
Map.new(load_opts)
)
end
else
{:ok, result}
end
{:error, error} ->
{:error, error}
end
modify_args = [input, result]
result =
if error_location == :in_result do
case result do
{:ok, result} ->
{:ok, %{result: result, errors: []}}
{:error, errors} ->
{:ok,
%{result: nil, errors: to_errors(errors, context, domain, resource, action)}}
end
else
result
end
resolution
|> Absinthe.Resolution.put_result(
to_resolution(
result,
context,
domain
)
)
|> add_root_errors(domain, resource, action, result)
|> modify_resolution(modify, modify_args)
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
def resolve(
%{arguments: arguments, context: context} = resolution,
{domain, resource,
%{
name: query_name,
type: :get,
action: action,
identity: identity,
type_name: type_name,
modify_resolution: modify
} = gql_query, relay_ids?}
) do
case handle_arguments(resource, action, arguments) do
{:ok, arguments} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
source: :graphql,
query: query_name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
filter = identity_filter(identity, resource, arguments, relay_ids?)
query =
resource
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
|> set_query_arguments(action, arguments)
|> select_fields(resource, resolution, type_name)
{result, modify_args} =
case filter do
{:ok, filter} ->
query = Ash.Query.do_filter(query, filter)
result =
query
|> Ash.Query.for_read(action, %{},
actor: opts[:actor],
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant),
tracer: AshGraphql.Domain.Info.tracer(domain),
domain: domain
)
|> load_fields(
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
type_name
)
|> Ash.read_one(opts)
{result, [query, result]}
{:error, error} ->
query =
resource
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
|> set_query_arguments(action, arguments)
|> select_fields(resource, resolution, type_name)
|> load_fields(
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
type_name
)
{{:error, error}, [query, {:error, error}]}
end
case {result, gql_query.allow_nil?} do
{{:ok, nil}, false} ->
{:ok, filter} = filter
error =
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter || []),
resource: resource
)
resolution
|> Absinthe.Resolution.put_result(
{:error, to_errors([error], context, domain, resource, action)}
)
|> add_root_errors(domain, resource, action, result)
{result, _} ->
resolution
|> Absinthe.Resolution.put_result(
to_resolution(
result
|> add_read_metadata(
gql_query,
Ash.Resource.Info.action(query.resource, action)
),
context,
domain
)
)
|> add_root_errors(domain, resource, action, result)
|> modify_resolution(modify, modify_args)
end
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
def resolve(
%{arguments: args, context: context} = resolution,
{domain, resource,
%{
name: query_name,
type: :read_one,
action: action,
modify_resolution: modify,
type_name: type_name
} =
gql_query, _relay_ids?}
) do
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
source: :graphql,
query: query_name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
with {:ok, args} <- handle_arguments(resource, action, args),
{:ok, query} <- read_one_query(resource, args) do
trace domain,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
query =
query
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
|> set_query_arguments(action, args)
|> select_fields(resource, resolution, type_name)
|> Ash.Query.for_read(action, %{},
actor: opts[:actor],
domain: domain,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
)
|> load_fields(
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
type_name
)
result =
Ash.read_one(query, opts)
result =
add_read_metadata(
result,
gql_query,
Ash.Resource.Info.action(query.resource, action)
)
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, domain))
|> add_root_errors(domain, resource, action, result)
|> modify_resolution(modify, [query, args])
end
else
{:error, error} ->
resolution
|> Absinthe.Resolution.put_result(to_resolution({:error, error}, context, domain))
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
def resolve(
%{arguments: args, context: context} = resolution,
{domain, resource,
%{
name: query_name,
type: :list,
relay?: relay?,
action: action,
type_name: type_name,
modify_resolution: modify
} = gql_query, _relay_ids?}
) do
case handle_arguments(resource, action, args) do
{:ok, args} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
source: :graphql,
query: query_name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
pagination = Ash.Resource.Info.action(resource, action).pagination
query = apply_load_arguments(args, Ash.Query.new(resource), true)
{result, modify_args} =
with {:ok, opts} <-
validate_resolve_opts(
resolution,
resource,
pagination,
relay?,
opts,
args,
gql_query,
action
),
result_fields <-
get_result_fields(
AshGraphql.Resource.query_pagination_strategy(
gql_query,
Ash.Resource.Info.action(resource, action)
),
relay?
),
query <-
query
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
|> set_query_arguments(action, args)
|> select_fields(resource, resolution, type_name, result_fields),
query <-
query
|> Ash.Query.for_read(action, %{},
actor: Map.get(context, :actor),
domain: domain,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
),
query <-
load_fields(
query,
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
type_name,
result_fields
),
{:ok, page} <- Ash.read(query, opts) do
result = paginate(resource, gql_query, action, page, relay?)
{result, [query, result]}
else
{:error, error} ->
{{:error, error}, [query, {:error, error}]}
end
result =
add_read_metadata(result, gql_query, Ash.Resource.Info.action(query.resource, action))
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, domain))
|> add_root_errors(domain, resource, action, modify_args)
|> modify_resolution(modify, modify_args)
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
def resolve(
%{root_value: {:pre_resolved, item}} = resolution,
{_, _, %AshGraphql.Resource.Subscription{}, _}
) do
Absinthe.Resolution.put_result(
resolution,
{:ok, item}
)
end
def resolve(
%{arguments: args, context: context, root_value: notifications} = resolution,
{domain, resource,
%AshGraphql.Resource.Subscription{read_action: read_action, name: name}, relay_ids?}
)
when is_list(notifications) do
case handle_arguments(resource, read_action, args) do
{:ok, args} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: read_action,
source: :graphql,
subscription: name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_subscription,
name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: read_action,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
subscription_events =
notifications
|> Enum.group_by(& &1.action_type)
|> Enum.map(fn {type, notifications} ->
subscription_field = subcription_field_from_action_type(type)
key = String.to_existing_atom(subscription_field)
if type in [:create, :update] do
data = Enum.map(notifications, & &1.data)
{filter, args} = Map.pop(args, :filter)
read_action =
read_action || Ash.Resource.Info.primary_action!(resource, :read).name
# read the records that were just created/updated
query =
resource
|> Ash.Query.do_filter(massage_filter(resource, filter))
|> Ash.Query.for_read(read_action, args, opts)
|> AshGraphql.Subscription.query_for_subscription(
domain,
resolution,
subscription_result_type(name),
[subscription_field]
)
query_with_authorization_rules =
Ash.can(
query,
opts[:actor],
tenant: opts[:tenant],
run_queries?: false,
alter_source?: true
)
current_filter = query.filter
{known_results, need_refetch} =
case query_with_authorization_rules do
{:ok, true, %{authorize_results: [], filter: nil} = query} ->
{data, []}
{:ok, true,
%{authorize_results: [], filter: %Ash.Filter{expression: nil}} = query} ->
{data, []}
{:ok, true, %{authorize_results: []} = query} ->
Enum.reduce(data, {[], []}, fn record, {known, refetch} ->
case Ash.Expr.eval(query.filter,
record: data,
unknown_on_unknown_refs?: true
) do
{:ok, true} ->
{[record | known], refetch}
{:ok, false} ->
{known, refetch}
_ ->
{known, [record | refetch]}
end
end)
{:error, false, _} ->
{[], []}
_ ->
{[], data}
end
primary_key = Ash.Resource.Info.primary_key(resource)
primary_key_matches =
Enum.map(need_refetch, fn record ->
Map.take(record, primary_key)
end)
with {:ok, known_results} <- Ash.load(known_results, query),
{:ok, need_refetch} <- do_refetch(query, primary_key_matches) do
known_results
|> Stream.concat(need_refetch)
|> Enum.map(fn record ->
%{key => record}
end)
else
{:error, error} ->
# caught by the batch resolver
raise Ash.Error.to_error_class(error)
end
else
Enum.map(notifications, fn notification ->
%{key => AshGraphql.Resource.encode_id(notification.data, relay_ids?)}
end)
end
end)
case List.flatten(subscription_events) do
[] ->
Absinthe.Resolution.put_result(
resolution,
{:error,
to_errors(
[Ash.Error.Query.NotFound.exception()],
context,
domain,
resource,
read_action
)}
)
[first | rest] ->
Process.put(:batch_resolved, rest)
Absinthe.Resolution.put_result(
resolution,
{:ok, first}
)
end
end
{:error, error} ->
{:error, error}
end
end
def resolve(
%{arguments: args, context: context, root_value: notification} = resolution,
{domain, resource,
%AshGraphql.Resource.Subscription{read_action: read_action, name: name}, relay_ids?}
) do
case handle_arguments(resource, read_action, args) do
{:ok, args} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: read_action,
source: :graphql,
subscription: name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_subscription,
name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: read_action,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant)
]
cond do
notification.action_type in [:create, :update] ->
data = notification.data
{filter, args} = Map.pop(args, :filter)
read_action =
read_action || Ash.Resource.Info.primary_action!(resource, :read).name
query =
Ash.Resource.Info.primary_key(resource)
|> Enum.reduce(resource, fn key, query ->
value = Map.get(data, key)
Ash.Query.filter(query, ^ref(key) == ^value)
end)
query =
Ash.Query.do_filter(
query,
massage_filter(query.resource, filter)
)
query =
AshGraphql.Subscription.query_for_subscription(
query
|> Ash.Query.for_read(read_action, args, opts),
domain,
resolution,
subscription_result_type(name),
[subcription_field_from_action_type(notification.action_type)]
)
result =
with {:ok, true, query} <-
Ash.can(
query,
opts[:actor],
tenant: opts[:tenant],
run_queries?: false,
alter_source?: true
),
[] <- query.authorize_results,
{:ok, true} <-
Ash.Expr.eval(query.filter,
record: data,
unknown_on_unknown_refs?: true
) do
Ash.load(data, query)
else
_ ->
query |> Ash.read_one()
end
case result do
# should only happen if a resource is created/updated and the subscribed user is not allowed to see it
{:ok, nil} ->
resolution
|> Absinthe.Resolution.put_result(
{:error,
to_errors(
[Ash.Error.Query.NotFound.exception()],
context,
domain,
resource,
read_action
)}
)
{:ok, result} ->
resolution
|> Absinthe.Resolution.put_result(
{:ok,
%{
String.to_existing_atom(
subcription_field_from_action_type(notification.action_type)
) => result
}}
)
{:error, error} ->
resolution
|> Absinthe.Resolution.put_result(
{:error, to_errors([error], context, domain, resource, read_action)}
)
end
notification.action_type in [:destroy] ->
resolution
|> Absinthe.Resolution.put_result(
{:ok,
%{
String.to_existing_atom(
subcription_field_from_action_type(notification.action_type)
) => AshGraphql.Resource.encode_id(notification.data, relay_ids?)
}}
)
end
end
{:error, error} ->
{:error, error}
end
end
defp do_refetch(_query, []) do
{:ok, []}
end
defp do_refetch(query, primary_key_matches) do
Ash.read(Ash.Query.do_filter(query, or: primary_key_matches))
end
defp subcription_field_from_action_type(:create), do: "created"
defp subcription_field_from_action_type(:update), do: "updated"
defp subcription_field_from_action_type(:destroy), do: "destroyed"
defp read_one_query(resource, args) do
case Map.fetch(args, :filter) do
{:ok, filter} when filter != %{} ->
case Ash.Filter.parse_input(resource, filter) do
{:ok, parsed} ->
{:ok, Ash.Query.do_filter(resource, parsed)}
{:error, error} ->
{:error, error}
end
_ ->
{:ok, Ash.Query.new(resource)}
end
end
defp handle_mutation_arguments(resource, action, read_action, arguments, mutation_args) do
{input_argument, arguments} = Map.pop(arguments, :input, %{})
{mutation_inputs, read_inputs} = Map.split(arguments, mutation_args)
mutation_inputs = Map.merge(mutation_inputs, input_argument)
case handle_arguments(resource, action, mutation_inputs) do
{:ok, mutation_inputs} ->
if read_action do
case handle_arguments(resource, read_action, read_inputs) do
{:ok, read_inputs} ->
{:ok, mutation_inputs, read_inputs}
error ->
error
end
else
{:ok, mutation_inputs}
end
error ->
error
end
end
defp handle_arguments(_resource, nil, argument_values) do
{:ok, argument_values}
end
defp handle_arguments(resource, action, argument_values) when is_atom(action) do
action = Ash.Resource.Info.action(resource, action)
handle_arguments(resource, action, argument_values)
end
defp handle_arguments(resource, action, argument_values) do
action_arguments = action.arguments
attributes =
resource
|> Ash.Resource.Info.attributes()
argument_values
|> Enum.reduce_while({:ok, %{}}, fn {key, value}, {:ok, arguments} ->
argument =
Enum.find(action_arguments, &(&1.name == key)) || Enum.find(attributes, &(&1.name == key))
if argument do
%{type: type, name: name, constraints: constraints} = argument
case handle_argument(resource, action, type, constraints, value, name) do
{:ok, value} ->
{:cont, {:ok, Map.put(arguments, name, value)}}
{:error, error} ->
{:halt, {:error, error}}
end
else
{:cont, {:ok, Map.put(arguments, key, value)}}
end
end)
end
defp handle_argument(resource, action, {:array, type}, constraints, value, name)
when is_list(value) do
value
|> Enum.reduce_while({:ok, []}, fn value, {:ok, acc} ->
case handle_argument(resource, action, type, constraints[:items], value, name) do
{:ok, value} ->
{:cont, {:ok, [value | acc]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, value} -> {:ok, Enum.reverse(value)}
{:error, error} -> {:error, error}
end
end
defp handle_argument(_resource, _action, Ash.Type.Union, constraints, value, name) do
handle_union_type(value, constraints, name)
end
defp handle_argument(resource, action, type, constraints, value, name) do
cond do
AshGraphql.Resource.Info.managed_relationship(resource, action, %{name: name, type: type}) &&
is_map(value) ->
managed_relationship =
AshGraphql.Resource.Info.managed_relationship(resource, action, %{
name: name,
type: type
})
opts = AshGraphql.Resource.find_manage_change(%{name: name, type: type}, action, resource)
relationship =
Ash.Resource.Info.relationship(resource, opts[:relationship]) ||
raise """
No relationship found when building managed relationship input: #{opts[:relationship]}
"""
manage_opts_schema =
if opts[:opts][:type] do
defaults = Ash.Changeset.manage_relationship_opts(opts[:opts][:type])
Enum.reduce(defaults, Ash.Changeset.manage_relationship_schema(), fn {key, value},
manage_opts ->
Spark.Options.Helpers.set_default!(manage_opts, key, value)
end)
else
Ash.Changeset.manage_relationship_schema()
end
manage_opts = Spark.Options.validate!(opts[:opts], manage_opts_schema)
fields =
resource
|> AshGraphql.Resource.manage_fields(
manage_opts,
managed_relationship,
relationship,
__MODULE__
)
|> Enum.reject(fn
{_, :__primary_key, _} ->
true
{_, {:identity, _}, _} ->
true
_ ->
false
end)
|> Map.new(fn {_, _, %{identifier: identifier}} = field ->
{identifier, field}
end)
Enum.reduce_while(value, {:ok, %{}}, fn {key, value}, {:ok, acc} ->
field_name =
resource
|> AshGraphql.Resource.Info.field_names()
|> Enum.map(fn {l, r} -> {r, l} end)
|> Keyword.get(key, key)
case Map.get(fields, field_name) do
nil ->
{:cont, {:ok, Map.put(acc, key, value)}}
{resource, action, _} ->
action = Ash.Resource.Info.action(resource, action)
attributes = Ash.Resource.Info.public_attributes(resource)
argument =
Enum.find(action.arguments, &(&1.name == field_name)) ||
Enum.find(attributes, &(&1.name == field_name))
if argument do
%{type: type, name: name, constraints: constraints} = argument
case handle_argument(resource, action, type, constraints, value, name) do
{:ok, value} ->
{:cont, {:ok, Map.put(acc, key, value)}}
{:error, error} ->
{:halt, {:error, error}}
end
else
{:cont, {:ok, Map.put(acc, key, value)}}
end
end
end)
Ash.Type.NewType.new_type?(type) ->
handle_argument(
resource,
action,
Ash.Type.NewType.subtype_of(type),
Ash.Type.NewType.constraints(type, constraints),
value,
name
)
AshGraphql.Resource.embedded?(type) and is_map(value) ->
create_action =
if constraints[:create_action] do
Ash.Resource.Info.action(type, constraints[:create_action]) ||
Ash.Resource.Info.primary_action(type, :create)
else
Ash.Resource.Info.primary_action(type, :create)
end
update_action =
if constraints[:update_action] do
Ash.Resource.Info.action(type, constraints[:update_action]) ||
Ash.Resource.Info.primary_action(type, :update)
else
Ash.Resource.Info.primary_action(type, :update)
end
attributes = Ash.Resource.Info.public_attributes(type)
fields =
cond do
create_action && update_action ->
create_action.arguments ++ update_action.arguments ++ attributes
update_action ->
update_action.arguments ++ attributes
create_action ->
create_action.arguments ++ attributes
true ->
attributes
end
value
|> Enum.reduce_while({:ok, %{}}, fn {key, value}, {:ok, acc} ->
field =
Enum.find(fields, fn field ->
field.name == key
end)
if field do
case handle_argument(
resource,
action,
field.type,
field.constraints,
value,
"#{name}.#{key}"
) do
{:ok, value} ->
{:cont, {:ok, Map.put(acc, key, value)}}
{:error, error} ->
{:halt, {:error, error}}
end
else
{:cont, {:ok, Map.put(acc, key, value)}}
end
end)
true ->
{:ok, value}
end
end
defp handle_union_type(value, constraints, name) do
value
|> Enum.reject(fn {_key, value} ->
is_nil(value)
end)
|> case do
[] ->
{:ok, nil}
[{key, value}] ->
config = constraints[:types][key]
if config[:tag] && is_map(value) do
{:ok,
%Ash.Union{type: key, value: Map.put_new(value, config[:tag], config[:tag_value])}}
else
{:ok, %Ash.Union{type: key, value: value}}
end
key_vals ->
keys = Enum.map_join(key_vals, ", ", fn {key, _} -> to_string(key) end)
{:error,
%{message: "Only one key can be specified, but got #{keys}", fields: ["#{name}"]}}
end
end
def validate_resolve_opts(resolution, resource, pagination, relay?, opts, args, query, action) do
action = Ash.Resource.Info.action(resource, action)
case AshGraphql.Resource.query_pagination_strategy(query, action) do
nil ->
{:ok, opts}
strategy ->
with {:ok, page} <- page_opts(resolution, resource, pagination, relay?, args, strategy) do
{:ok, Keyword.put(opts, :page, page)}
end
end
end
defp page_opts(resolution, resource, pagination, relay?, args, strategy, nested \\ []) do
page_opts =
args
|> Map.take([:limit, :offset, :first, :after, :before, :last])
|> Enum.reject(fn {_, val} -> is_nil(val) end)
with {:ok, page_opts} <- validate_offset_opts(page_opts, strategy, pagination),
{:ok, page_opts} <- validate_keyset_opts(page_opts, strategy, pagination) do
type = page_type(resource, strategy, relay?)
field_names = resolution |> fields(nested, type) |> names_only()
page_opts =
if Enum.any?(field_names, &(&1 == :count)) do
Keyword.put(page_opts, :count, true)
else
page_opts
end
{:ok, page_opts}
end
end
defp validate_offset_opts(opts, :offset, %{max_page_size: max_page_size}) do
limit =
case opts |> Keyword.take([:limit]) |> Enum.into(%{}) do
%{limit: limit} ->
min(limit, max_page_size)
_ ->
max_page_size
end
{:ok, Keyword.put(opts, :limit, limit)}
end
defp validate_offset_opts(opts, _, _) do
{:ok, opts}
end
defp validate_keyset_opts(opts, strategy, %{max_page_size: max_page_size})
when strategy in [:keyset, :relay] do
case opts |> Keyword.take([:first, :last, :after, :before]) |> Enum.into(%{}) do
%{first: _first, last: _last} ->
{:error,
%Ash.Error.Query.InvalidQuery{
message: "You can pass either `first` or `last`, not both",
field: :first
}}
%{first: _first, before: _before} ->
{:error,
%Ash.Error.Query.InvalidQuery{
message:
"You can pass either `first` and `after` cursor, or `last` and `before` cursor",
field: :first
}}
%{last: _last, after: _after} ->
{:error,
%Ash.Error.Query.InvalidQuery{
message:
"You can pass either `first` and `after` cursor, or `last` and `before` cursor",
field: :last
}}
%{first: first} ->
{:ok, opts |> Keyword.delete(:first) |> Keyword.put(:limit, min(first, max_page_size))}
%{last: last, before: before} when not is_nil(before) ->
{:ok, opts |> Keyword.delete(:last) |> Keyword.put(:limit, min(last, max_page_size))}
%{last: _last} ->
{:error,
%Ash.Error.Query.InvalidQuery{
message: "You can pass `last` only with `before` cursor",
field: :last
}}
_ ->
{:ok, Keyword.put(opts, :limit, max_page_size)}
end
end
defp validate_keyset_opts(opts, _, _) do
{:ok, opts}
end
defp get_result_fields(:keyset, true) do
["edges", "node"]
end
defp get_result_fields(:keyset, false) do
["results"]
end
defp get_result_fields(:offset, _) do
["results"]
end
defp get_result_fields(:relay, _) do
["edges", "node"]
end
defp get_result_fields(_pagination, _) do
[]
end
defp paginate_with_keyset(
%Ash.Page.Keyset{
results: results,
more?: more,
after: after_cursor,
before: before_cursor,
count: count
},
relay?
) do
{start_cursor, end_cursor} =
case results do
[] ->
{nil, nil}
[first] ->
{first.__metadata__.keyset, first.__metadata__.keyset}
[first | rest] ->
last = List.last(rest)
{first.__metadata__.keyset, last.__metadata__.keyset}
end
{has_previous_page, has_next_page} =
case {after_cursor, before_cursor} do
{nil, nil} ->
{false, more}
{_, nil} ->
{true, more}
{nil, _} ->
# https://github.com/ash-project/ash_graphql/pull/36#issuecomment-1243892511
{more, not Enum.empty?(results)}
end
if relay? do
{
:ok,
%{
page_info: %{
start_cursor: start_cursor,
end_cursor: end_cursor,
has_next_page: has_next_page,
has_previous_page: has_previous_page
},
count: count,
edges:
Enum.map(results, fn result ->
%{
cursor: result.__metadata__.keyset,
node: result
}
end)
}
}
else
{:ok, %{results: results, count: count, start_keyset: start_cursor, end_keyset: end_cursor}}
end
end
defp paginate_with_offset(%Ash.Page.Offset{
results: results,
count: count,
more?: more?,
offset: offset,
limit: limit
}) do
total_pages = get_total_pages(count, limit)
has_next_page = more?
has_previous_page = offset > 0
page_number = get_current_page(%Ash.Page.Offset{limit: limit, offset: offset}, total_pages)
last_page = total_pages
{:ok,
%{
results: results,
count: count,
more?: more?,
limit: limit,
has_next_page: has_next_page,
has_previous_page: has_previous_page,
page_number: page_number,
last_page: last_page
}}
end
defp get_total_pages(count, _) when count in [0, nil], do: 1
defp get_total_pages(_, nil), do: 1
defp get_total_pages(total_count, limit), do: ceil(total_count / limit)
defp get_current_page(%Ash.Page.Offset{limit: limit, offset: offset}, total),
do: min(ceil(offset / limit) + 1, total)
defp paginate(_resource, _gql_query, _action, %Ash.Page.Keyset{} = keyset, relay?) do
paginate_with_keyset(keyset, relay?)
end
defp paginate(resource, query, action, %Ash.Page.Offset{} = offset, relay?) do
# If a read action supports both offset and keyset, it will return an offset page by default
# Check what strategy we're using and convert the page accordingly
pagination_strategy = query_pagination_strategy(query, resource, action)
if relay? or pagination_strategy == :keyset do
offset
|> offset_to_keyset()
|> paginate_with_keyset(relay?)
else
paginate_with_offset(offset)
end
end
defp paginate(resource, query, action, page, relay?) do
case query_pagination_strategy(query, resource, action) do
nil ->
{:ok, page}
:offset ->
paginate(
resource,
query,
action,
%Ash.Page.Offset{results: page, count: Enum.count(page), more?: false},
relay?
)
:keyset ->
paginate(
resource,
query,
action,
%Ash.Page.Keyset{
results: page,
more?: false,
after: nil,
before: nil
},
relay?
)
_ ->
{:ok, page}
end
end
defp query_pagination_strategy(query, resource, action) when is_atom(action) do
action = Ash.Resource.Info.action(resource, action)
query_pagination_strategy(query, resource, action)
end
defp query_pagination_strategy(query, _resource, action) do
AshGraphql.Resource.query_pagination_strategy(query, action)
end
defp offset_to_keyset(%Ash.Page.Offset{} = offset) do
%Ash.Page.Keyset{
results: offset.results,
limit: offset.limit,
more?: offset.more?,
count: offset.count,
after: nil,
before: nil
}
end
defp paginate_relationship(%Ash.Page.Keyset{} = keyset, strategy) do
relay? = strategy == :relay
paginate_with_keyset(keyset, relay?)
end
defp paginate_relationship(%Ash.Page.Offset{} = offset, strategy)
when strategy in [:relay, :keyset] do
# If a read action supports both offset and keyset, it will return an offset page by default,
# so we might end up here even with relay or keyset strategy
relay? = strategy == :relay
offset
|> offset_to_keyset()
|> paginate_with_keyset(relay?)
end
defp paginate_relationship(%Ash.Page.Offset{} = offset, :offset) do
paginate_with_offset(offset)
end
defp paginate_relationship(page, _) do
{:ok, page}
end
def mutate(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def mutate(
%{arguments: arguments, context: context} = resolution,
{domain, resource,
%{
type: :create,
name: mutation_name,
action: action,
upsert?: upsert?,
upsert_identity: upsert_identity,
args: args,
modify_resolution: modify
}, _relay_ids?}
) do
case handle_mutation_arguments(resource, action, nil, arguments, args) do
{:ok, input} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
source: :graphql,
mutation_name: mutation_name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_mutation,
mutation_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tenant: Map.get(context, :tenant),
upsert?: upsert?
]
opts =
if upsert? && upsert_identity do
Keyword.put(opts, :upsert_identity, upsert_identity)
else
opts
end
type_name = mutation_result_type(mutation_name)
changeset =
resource
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.set_context(get_context(context))
|> Ash.Changeset.for_create(action, input,
actor: Map.get(context, :actor),
authorize?: AshGraphql.Domain.Info.authorize?(domain)
)
|> select_fields(resource, resolution, type_name, ["result"])
|> load_fields(
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
type_name,
["result"]
)
{result, modify_args} =
changeset
|> Ash.create(opts)
|> case do
{:ok, value} ->
{{:ok, add_metadata(%{result: value, errors: []}, value, changeset.action)},
[changeset, {:ok, value}]}
{:error, %{changeset: changeset} = error} ->
{{:ok,
%{
result: nil,
errors: to_errors(changeset.errors, context, domain, resource, action)
}}, [changeset, {:error, error}]}
end
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, domain))
|> add_root_errors(domain, resource, action, modify_args)
|> modify_resolution(modify, modify_args)
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
if AshGraphql.Domain.Info.root_level_errors?(domain) do
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:ok, %{result: nil, errors: to_errors(error, context, domain, resource, action)}},
context,
domain
)
)
end
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
def mutate(
%{arguments: arguments, context: context} = resolution,
{domain, resource,
%{
name: mutation_name,
type: :update,
action: action,
identity: identity,
read_action: read_action,
args: args,
modify_resolution: modify
}, relay_ids?}
) do
read_action = read_action || Ash.Resource.Info.primary_action!(resource, :read).name
case handle_mutation_arguments(resource, action, read_action, arguments, args) do
{:ok, input, read_action_input} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
mutation: mutation_name,
source: :graphql,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_mutation,
mutation_name,
metadata do
filter = identity_filter(identity, resource, arguments, relay_ids?)
case filter do
{:ok, filter} ->
query =
resource
|> Ash.Query.do_filter(filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
|> set_query_arguments(read_action, read_action_input)
|> Ash.Query.limit(1)
{result, modify_args} =
query
|> Ash.bulk_update(action, input,
return_errors?: true,
notify?: true,
strategy: [:atomic, :stream, :atomic_batches],
allow_stream_with: :full_read,
authorize_changeset_with: authorize_bulk_with(query.resource),
return_records?: true,
tenant: Map.get(context, :tenant),
context: get_context(context) || %{},
authorize?: AshGraphql.Domain.Info.authorize?(domain),
read_action: read_action,
domain: domain,
actor: Map.get(context, :actor),
select:
get_select(resource, resolution, mutation_result_type(mutation_name), [
"result"
]),
load:
get_loads(
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
mutation_result_type(mutation_name),
["result"]
)
)
|> case do
%Ash.BulkResult{status: :success, records: [value]} ->
action = Ash.Resource.Info.action(resource, action)
{{:ok, add_metadata(%{result: value, errors: []}, value, action)},
[query, {:ok, value}]}
%Ash.BulkResult{status: :success, records: []} ->
{{:ok,
%{
result: nil,
errors:
to_errors(
[
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter || []),
resource: resource
)
],
context,
domain,
resource,
action
)
}},
[
query,
{:error,
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter || []),
resource: resource
)}
]}
%Ash.BulkResult{status: :error, errors: errors} ->
{{:ok,
%{result: nil, errors: to_errors(errors, context, domain, resource, action)}},
[query, {:error, errors}]}
end
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, domain))
|> add_root_errors(domain, resource, action, modify_args)
|> modify_resolution(modify, modify_args)
{:error, error} ->
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
end
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
if AshGraphql.Domain.Info.root_level_errors?(domain) do
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:ok, %{result: nil, errors: to_errors(error, context, domain, resource, action)}},
context,
domain
)
)
end
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
def mutate(
%{arguments: arguments, context: context} = resolution,
{domain, resource,
%{
name: mutation_name,
type: :destroy,
action: action,
identity: identity,
read_action: read_action,
args: args,
modify_resolution: modify
}, relay_ids?}
) do
read_action = read_action || Ash.Resource.Info.primary_action!(resource, :read).name
case handle_mutation_arguments(resource, action, read_action, arguments, args) do
{:ok, input, read_action_input} ->
metadata = %{
domain: domain,
resource: resource,
resource_short_name: Ash.Resource.Info.short_name(resource),
actor: Map.get(context, :actor),
tenant: Map.get(context, :tenant),
action: action,
source: :graphql,
mutation: mutation_name,
authorize?: AshGraphql.Domain.Info.authorize?(domain)
}
trace domain,
resource,
:gql_mutation,
mutation_name,
metadata do
filter = identity_filter(identity, resource, arguments, relay_ids?)
case filter do
{:ok, filter} ->
query =
resource
|> Ash.Query.do_filter(filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
|> set_query_arguments(read_action, read_action_input)
|> Ash.Query.limit(1)
{result, modify_args} =
query
|> Ash.bulk_destroy(action, input,
return_errors?: true,
notify?: true,
authorize_changeset_with: authorize_bulk_with(query.resource),
strategy: [:atomic, :stream, :atomic_batches],
allow_stream_with: :full_read,
return_records?: true,
read_action: read_action,
tenant: Map.get(context, :tenant),
context: get_context(context) || %{},
authorize?: AshGraphql.Domain.Info.authorize?(domain),
actor: Map.get(context, :actor),
domain: domain,
select:
get_select(resource, resolution, mutation_result_type(mutation_name), [
"result"
]),
load:
get_loads(
[
domain: domain,
tenant: Map.get(context, :tenant),
authorize?: AshGraphql.Domain.Info.authorize?(domain),
tracer: AshGraphql.Domain.Info.tracer(domain),
actor: Map.get(context, :actor)
],
resource,
resolution,
resolution.path,
context,
mutation_result_type(mutation_name),
["result"]
)
)
|> case do
%Ash.BulkResult{status: :success, records: [value]} ->
action = Ash.Resource.Info.action(resource, action)
{{:ok, add_metadata(%{result: value, errors: []}, value, action)},
[query, {:ok, value}]}
%Ash.BulkResult{status: :success, records: []} ->
{{:ok,
%{
result: nil,
errors:
to_errors(
[
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter || []),
resource: resource
)
],
context,
domain,
resource,
action
)
}},
[
query,
{:error,
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter || []),
resource: resource
)}
]}
%Ash.BulkResult{status: :error, errors: errors} ->
{{:ok,
%{result: nil, errors: to_errors(errors, context, domain, resource, action)}},
[query, {:error, errors}]}
end
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, domain))
|> add_root_errors(domain, resource, action, modify_args)
|> modify_resolution(modify, modify_args)
{:error, error} ->
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
end
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
if AshGraphql.Domain.Info.root_level_errors?(domain) do
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, domain)
)
else
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:ok, %{result: nil, errors: to_errors(error, context, domain, resource, action)}},
context,
domain
)
)
end
else
something_went_wrong(resolution, e, domain, __STACKTRACE__)
end
end
if Application.compile_env(:ash_graphql, :authorize_update_destroy_with_error?) do
def authorize_bulk_with(resource) do
if Ash.DataLayer.data_layer_can?(resource, :expr_error) do
:error
else
:filter
end
end
else
def authorize_bulk_with(_resource) do
:filter
end
end
defp log_exception(e, stacktrace) do
uuid = Ash.UUID.generate()
Logger.error(
"""
#{uuid}: Exception raised while resolving query.
#{String.slice(Exception.format(:error, e), 0, 2000)}
#{Exception.format_stacktrace(stacktrace)}
""",
crash_reason: {e, stacktrace}
)
uuid
end
defp something_went_wrong(resolution, e, domain, stacktrace) do
tracer = AshGraphql.Domain.Info.tracer(domain)
Ash.Tracer.set_error(tracer, e)
uuid = log_exception(e, stacktrace)
Absinthe.Resolution.put_result(
resolution,
{:error,
[
%{
message: "Something went wrong. Unique error id: `#{uuid}`",
code: "something_went_wrong",
vars: %{},
fields: [],
short_message: "Something went wrong."
}
]}
)
end
defp modify_resolution(resolution, nil, _), do: resolution
defp modify_resolution(resolution, {m, f, a}, args) do
apply(m, f, [resolution | args] ++ a)
end
def identity_filter(false, _resource, _arguments, _relay_ids?) do
{:ok, nil}
end
def identity_filter(nil, resource, arguments, relay_ids?) do
if relay_ids? or AshGraphql.Resource.Info.encode_primary_key?(resource) do
case AshGraphql.Resource.decode_id(
resource,
Map.get(arguments, :id) || "",
relay_ids?
) do
{:ok, value} ->
{:ok, value}
{:error, error} ->
{:error, error}
end
else
resource
|> Ash.Resource.Info.primary_key()
|> Enum.reduce_while({:ok, nil}, fn key, {:ok, expr} ->
value = Map.get(arguments, key)
if value do
if expr do
{:cont, {:ok, Ash.Expr.expr(^expr and ^ref(key) == ^value)}}
else
{:cont, {:ok, Ash.Expr.expr(^ref(key) == ^value)}}
end
else
{:halt, {:error, "Required key not present"}}
end
end)
end
end
def identity_filter(identity, resource, arguments, _relay_ids?) do
{:ok,
resource
|> Ash.Resource.Info.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)}
end
def massage_filter(_resource, nil), do: nil
def massage_filter(resource, filter) when is_map(filter) do
Enum.map(filter, fn {key, value} ->
cond do
rel = Ash.Resource.Info.relationship(resource, key) ->
{key, massage_filter(rel.destination, value)}
Ash.Resource.Info.calculation(resource, key) ->
calc_input(key, value)
true ->
{key, value}
end
end)
end
def massage_filter(_resource, other), do: other
defp calc_input(key, value) do
case Map.fetch(value, :input) do
{:ok, input} ->
{key, {input, Map.delete(value, :input)}}
:error ->
{key, value}
end
end
@doc false
def load_fields(
query_or_changeset,
load_opts,
resource,
resolution,
path,
context,
type_override,
nested \\ []
) do
load =
get_loads(load_opts, resource, resolution, path, context, type_override, nested)
case query_or_changeset do
%Ash.Query{} = query ->
Ash.Query.load(query, load)
%Ash.Changeset{} = changeset ->
Ash.Changeset.load(changeset, load)
end
end
@doc false
def get_loads(
load_opts,
resource,
resolution,
path,
context,
type_override,
nested \\ []
) do
type_override = type_override || AshGraphql.Resource.Info.type(resource)
{fields, path} = nested_fields_and_path(resolution, path, nested, type_override)
resource_loads(fields, resource, resolution, load_opts, path, context)
end
defp nested_fields_and_path(resolution, path, nested, type \\ nil)
defp nested_fields_and_path(resolution, path, [], type) do
base = Enum.at(path, 0) || resolution
selections =
case base do
%Absinthe.Resolution{} ->
if type do
Absinthe.Resolution.project(resolution, type)
else
Absinthe.Resolution.project(resolution)
end
%Absinthe.Blueprint.Document.Field{selections: selections} ->
projection_type = type || base.schema_node.type
{fields, _} =
selections
|> Absinthe.Resolution.Projector.project(
Absinthe.Schema.lookup_type(resolution.schema, projection_type),
path,
%{},
resolution
)
fields
end
{selections, path}
end
defp nested_fields_and_path(resolution, path, [nested | rest], _type) do
base = Enum.at(path, 0) || resolution
selections =
case base do
%Absinthe.Resolution{} ->
Absinthe.Resolution.project(resolution)
%Absinthe.Blueprint.Document.Field{selections: selections} ->
{fields, _} =
selections
|> Absinthe.Resolution.Projector.project(
Absinthe.Schema.lookup_type(resolution.schema, base.schema_node.type),
path,
%{},
resolution
)
fields
end
selection = Enum.find(selections, &(&1.name == nested))
if selection do
nested_fields_and_path(resolution, [selection | path], rest)
else
{[], path}
end
end
defp resource_loads(fields, resource, resolution, load_opts, path, context) do
Enum.flat_map(fields, fn selection ->
cond do
aggregate = Ash.Resource.Info.aggregate(resource, selection.schema_node.identifier) ->
[aggregate.name]
calculation = Ash.Resource.Info.calculation(resource, selection.schema_node.identifier) ->
arguments =
selection.arguments
|> Map.new(fn argument ->
{argument.schema_node.identifier, argument.input_value.data}
end)
|> then(fn args ->
if selection.alias do
Map.put(args, :as, {:__ash_graphql_calculation__, selection.alias})
else
args
end
end)
if Ash.Type.can_load?(calculation.type, calculation.constraints) do
loads =
type_loads(
selection.selections,
context,
calculation.type,
calculation.constraints,
load_opts,
resource,
calculation.name,
resolution,
[selection | path],
selection,
AshGraphql.Resource.Info.type(resource)
)
case loads do
[] ->
[{calculation.name, arguments}]
loads ->
[{calculation.name, {arguments, loads}}]
end
else
[{calculation.name, arguments}]
end
attribute = Ash.Resource.Info.attribute(resource, selection.schema_node.identifier) ->
if Ash.Type.can_load?(attribute.type, attribute.constraints) do
loads =
type_loads(
selection.selections,
context,
attribute.type,
attribute.constraints,
load_opts,
resource,
attribute.name,
resolution,
[selection | path],
selection,
AshGraphql.Resource.Info.type(resource)
)
case loads do
[] ->
if selection.alias do
{:ok, calc} =
Ash.Query.Calculation.new(
{:__ash_graphql_attribute__, selection.alias},
Ash.Resource.Calculation.LoadAttribute,
Keyword.put(load_opts, :attribute, attribute.name),
attribute.type,
attribute.constraints
)
[
calc
]
else
[attribute.name]
end
loads ->
if selection.alias do
{:ok, calc} =
Ash.Query.Calculation.new(
{:__ash_graphql_attribute__, selection.alias},
Ash.Resource.Calculation.LoadAttribute,
Keyword.merge(load_opts, load: loads, attribute: attribute.name),
attribute.type,
attribute.constraints
)
[
calc
]
else
[{attribute.name, loads}]
end
end
else
[attribute.name]
end
relationship = Ash.Resource.Info.relationship(resource, selection.schema_node.identifier) ->
read_action =
case relationship.read_action do
nil ->
Ash.Resource.Info.primary_action!(relationship.destination, :read)
read_action ->
Ash.Resource.Info.action(relationship.destination, read_action)
end
args =
Map.new(selection.arguments, fn argument ->
{argument.schema_node.identifier, argument.input_value.data}
end)
related_query =
relationship.destination
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(get_context(context))
pagination_strategy =
AshGraphql.Resource.relationship_pagination_strategy(
resource,
relationship.name,
read_action
)
will_paginate? = pagination_strategy != nil
relay? = pagination_strategy == :relay
result_fields = get_result_fields(pagination_strategy, relay?)
nested = Enum.map(Enum.reverse([selection | path]), & &1.name)
related_query =
if pagination_strategy do
case page_opts(
resolution,
relationship.destination,
read_action.pagination,
relay?,
args,
pagination_strategy,
nested
) do
{:ok, page_opts} ->
Ash.Query.page(related_query, page_opts)
{:error, error} ->
Ash.Query.add_error(related_query, error)
end
else
related_query
end
related_query =
args
|> apply_load_arguments(related_query, will_paginate?)
|> set_query_arguments(read_action, args)
|> select_fields(
relationship.destination,
resolution,
nil,
nested
)
|> load_fields(
load_opts,
relationship.destination,
resolution,
[
selection | path
],
context,
nil,
result_fields
)
if selection.alias do
{type, constraints} =
case relationship.cardinality do
:many ->
{{:array, :struct}, items: [instance_of: relationship.destination]}
:one ->
{:struct, instance_of: relationship.destination}
end
{:ok, calc} =
Ash.Query.Calculation.new(
{:__ash_graphql_relationship__, selection.alias},
Ash.Resource.Calculation.LoadRelationship,
Keyword.merge(load_opts, relationship: relationship.name, query: related_query),
type,
constraints
)
[
calc
]
else
[{relationship.name, related_query}]
end
true ->
[]
end
end)
end
defp type_loads(
selections,
context,
type,
constraints,
load_opts,
resource,
field_name,
resolution,
path,
selection,
parent_type_name,
original_type \\ nil,
already_expanded? \\ false
)
defp type_loads(
selections,
context,
{:array, type},
constraints,
load_opts,
resource,
field_name,
resolution,
path,
selection,
parent_type_name,
original_type,
already_expanded?
) do
type_loads(
selections,
context,
type,
constraints[:items] || [],
load_opts,
resource,
field_name,
resolution,
path,
selection,
parent_type_name,
original_type,
already_expanded?
)
end
defp type_loads(
selections,
context,
type,
constraints,
load_opts,
resource,
field_name,
resolution,
path,
selection,
parent_type_name,
original_type,
already_expanded?
) do
cond do
Ash.Type.NewType.new_type?(type) ->
subtype_constraints = Ash.Type.NewType.constraints(type, constraints)
subtype_of = Ash.Type.NewType.subtype_of(type)
type_loads(
selections,
context,
subtype_of,
subtype_constraints,
load_opts,
resource,
field_name,
resolution,
path,
selection,
parent_type_name,
{type, constraints},
already_expanded?
)
AshGraphql.Resource.embedded?(type) || Ash.Resource.Info.resource?(type) ||
(type in [Ash.Type.Struct, :struct] && constraints[:instance_of] &&
(AshGraphql.Resource.embedded?(constraints[:instance_of]) ||
Ash.Resource.Info.resource?(constraints[:instance_of]))) ->
type =
if type in [:struct, Ash.Type.Struct] do
constraints[:instance_of]
else
type
end
fields =
if already_expanded? do
selections
else
value_type =
Absinthe.Schema.lookup_type(resolution.schema, selection.schema_node.type)
{fields, _} =
Absinthe.Resolution.Projector.project(
selections,
value_type,
path,
%{},
resolution
)
fields
end
resource_loads(fields, type, resolution, load_opts, path, context)
type == Ash.Type.Union ->
{global_selections, fragments} =
Enum.split_with(selections, fn
%Absinthe.Blueprint.Document.Field{} ->
true
_ ->
false
end)
loads =
case global_selections do
[] ->
[]
global_selections ->
first_type_config =
constraints[:types]
|> Enum.at(0)
|> elem(1)
first_type = first_type_config[:type]
first_constraints = first_type_config[:constraints]
type_loads(
global_selections,
context,
first_type,
first_constraints,
load_opts,
resource,
field_name,
resolution,
path,
selection,
parent_type_name,
original_type
)
end
{graphql_unnested_unions, configured_type_name} =
case original_type do
{type, constraints} ->
configured_type_name =
if function_exported?(type, :graphql_type, 1) do
type.graphql_type(constraints)
end
unnested_unions =
if function_exported?(type, :graphql_unnested_unions, 1) do
type.graphql_unnested_unions(constraints)
else
[]
end
{unnested_unions, configured_type_name}
_ ->
{[], nil}
end
constraints[:types]
|> Enum.filter(fn {_, config} ->
Ash.Type.can_load?(config[:type], config[:constraints])
end)
|> Enum.reduce(loads, fn {type_name, config}, acc ->
{gql_type_name, nested?} =
if type_name in graphql_unnested_unions do
{AshGraphql.Resource.field_type(
config[:type],
%Ash.Resource.Attribute{
name: configured_type_name,
type: config[:type],
constraints: config[:constraints]
},
resource
), false}
else
{AshGraphql.Resource.nested_union_type_name(
%{name: configured_type_name || "#{parent_type_name}_#{field_name}"},
type_name,
true
), true}
end
gql_type = Absinthe.Schema.lookup_type(resolution.schema, gql_type_name)
if !gql_type do
raise Ash.Error.Framework.AssumptionFailed,
message: "Could not find a corresponding graphql type for #{inspect(gql_type_name)}"
end
if nested? do
{fields, _} =
Absinthe.Resolution.Projector.project(
fragments,
gql_type,
path,
%{},
resolution
)
if selection = Enum.find(fields, &(&1.schema_node.identifier == :value)) do
Keyword.put(
acc,
type_name,
type_loads(
selection.selections,
context,
config[:type],
config[:constraints],
load_opts,
resource,
gql_type_name,
resolution,
[selection | path],
selection,
gql_type_name,
original_type
)
)
else
acc
end
else
{fields, _} =
Absinthe.Resolution.Projector.project(
fragments,
gql_type,
path,
%{},
resolution
)
Keyword.put(
acc,
type_name,
type_loads(
fields,
context,
config[:type],
config[:constraints],
load_opts,
resource,
gql_type_name,
resolution,
path,
selection,
gql_type_name,
original_type,
true
)
)
end
end)
true ->
[]
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp mutation_result_type(mutation_name) do
String.to_atom("#{mutation_name}_result")
end
# sobelow_skip ["DOS.StringToAtom"]
defp subscription_result_type(subscription_name) do
String.to_atom("#{subscription_name}_result")
end
# sobelow_skip ["DOS.StringToAtom"]
defp page_type(resource, strategy, relay?) do
type = AshGraphql.Resource.Info.type(resource)
cond do
relay? ->
String.to_atom("#{type}_connection")
strategy == :keyset ->
String.to_atom("keyset_page_of_#{type}")
strategy == :offset ->
String.to_atom("page_of_#{type}")
end
end
@doc false
def select_fields(
query_or_changeset,
resource,
resolution,
type_override,
nested \\ []
) do
subfields = get_select(resource, resolution, type_override, nested)
case query_or_changeset do
%Ash.Query{} = query ->
query |> Ash.Query.select(subfields)
%Ash.Changeset{} = changeset ->
changeset |> Ash.Changeset.select(subfields)
end
end
defp get_select(resource, resolution, type_override, nested) do
type = type_override || AshGraphql.Resource.Info.type(resource)
resolution
|> fields(nested, type)
|> names_only()
|> Enum.map(&field_or_relationship(resource, &1))
|> Enum.filter(& &1)
|> Enum.map(& &1.name)
end
defp field_or_relationship(resource, identifier) do
case Ash.Resource.Info.attribute(resource, identifier) do
nil ->
case Ash.Resource.Info.relationship(resource, identifier) do
nil ->
nil
rel ->
Ash.Resource.Info.attribute(resource, rel.source_attribute)
end
attr ->
attr
end
end
defp fields(%Absinthe.Resolution{} = resolution, [], type) do
resolution
|> Absinthe.Resolution.project(type)
end
defp fields(%Absinthe.Resolution{} = resolution, names, _type) do
# Here we don't pass the type to project because the Enum.reduce below already
# takes care of projecting the nested fields using the correct type
project =
resolution
|> Absinthe.Resolution.project()
cache = resolution.fields_cache
Enum.reduce(names, {project, cache}, fn name, {fields, cache} ->
case fields |> Enum.find(&(&1.name == name)) do
nil ->
{fields, cache}
selection ->
type = Absinthe.Schema.lookup_type(resolution.schema, selection.schema_node.type)
selection
|> Map.get(:selections)
|> Absinthe.Resolution.Projector.project(
type,
[selection | resolution.path],
cache,
resolution
)
end
end)
|> elem(0)
end
defp names_only(fields) do
Enum.map(fields, & &1.schema_node.identifier)
end
@doc false
def set_query_arguments(query, action, arg_values) do
action =
if is_atom(action) do
Ash.Resource.Info.action(query.resource, action)
else
action
end
action.arguments
|> Enum.filter(& &1.public?)
|> Enum.reduce(query, fn argument, query ->
case Map.fetch(arg_values, argument.name) do
{:ok, value} ->
Ash.Query.set_argument(query, argument.name, value)
_ ->
query
end
end)
end
defp add_root_errors(resolution, domain, resource, action, {:error, error_or_errors}) do
do_root_errors(domain, resource, action, resolution, error_or_errors)
end
defp add_root_errors(resolution, domain, resource, action, [_, {:error, error_or_errors}]) do
do_root_errors(domain, resource, action, resolution, error_or_errors)
end
defp add_root_errors(resolution, domain, resource, action, [_, {:ok, %{errors: errors}}])
when errors not in [nil, []] do
do_root_errors(domain, resource, action, resolution, errors, false)
end
defp add_root_errors(resolution, domain, resource, action, {:ok, %{errors: errors}})
when errors not in [nil, []] do
do_root_errors(domain, resource, action, resolution, errors, false)
end
defp add_root_errors(resolution, _domain, _resource, _action, _other_thing) do
resolution
end
defp do_root_errors(domain, resource, action, resolution, error_or_errors, to_errors? \\ true) do
if AshGraphql.Domain.Info.root_level_errors?(domain) do
Map.update!(resolution, :errors, fn current_errors ->
if to_errors? do
Enum.concat(
current_errors || [],
List.wrap(to_errors(error_or_errors, resolution.context, domain, resource, action))
)
else
Enum.concat(current_errors || [], List.wrap(error_or_errors))
end
end)
else
resolution
end
end
defp add_read_metadata({:error, error}, _, _) do
{:error, error}
end
defp add_read_metadata({:ok, result}, query, action) do
{:ok, add_read_metadata(result, query, action)}
end
defp add_read_metadata(nil, _, _), do: nil
defp add_read_metadata(result, query, action) when is_list(result) do
show_metadata = query.show_metadata || Enum.map(Map.get(action, :metadata, []), & &1.name)
Enum.map(result, fn record ->
do_add_read_metadata(record, show_metadata)
end)
end
defp add_read_metadata(result, query, action) do
show_metadata = query.show_metadata || Enum.map(Map.get(action, :metadata, []), & &1.name)
do_add_read_metadata(result, show_metadata)
end
defp do_add_read_metadata(record, show_metadata) do
Enum.reduce(show_metadata, record, fn key, record ->
Map.put(record, key, Map.get(record.__metadata__ || %{}, key))
end)
end
defp add_metadata(result, action_result, action) do
metadata = Map.get(action, :metadata, [])
if Enum.empty?(metadata) do
result
else
metadata =
Map.new(action.metadata, fn metadata ->
{metadata.name, Map.get(action_result.__metadata__ || %{}, metadata.name)}
end)
Map.put(result, :metadata, metadata)
end
end
@doc false
def unwrap_errors([]), do: []
def unwrap_errors(errors) do
errors
|> List.wrap()
|> Enum.flat_map(fn
%class{errors: errors} when class in [Ash.Error.Invalid, Ash.Error.Forbidden] ->
unwrap_errors(List.wrap(errors))
errors ->
List.wrap(errors)
end)
end
defp to_errors(errors, context, domain, resource, action) do
AshGraphql.Errors.to_errors(errors, context, domain, resource, action)
end
def resolve_calculation(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_calculation(
%Absinthe.Resolution{
source: parent,
context: context
} = resolution,
{domain, resource, calculation}
) do
domain = domain || context[:domain]
result =
if resolution.definition.alias do
Map.get(parent.calculations, {:__ash_graphql_calculation__, resolution.definition.alias})
else
Map.get(parent, calculation.name)
end
case result do
%struct{} when struct == Ash.ForbiddenField ->
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:error,
Ash.Error.Forbidden.ForbiddenField.exception(
resource: resource,
field: resolution.definition.name
)},
context,
domain
)
)
result ->
unwrapped_type =
unwrap_type(calculation.type)
result =
if Ash.Type.NewType.new_type?(unwrapped_type) &&
Ash.Type.NewType.subtype_of(unwrapped_type) == Ash.Type.Union &&
function_exported?(unwrapped_type, :graphql_unnested_unions, 1) do
unnested_types = unwrapped_type.graphql_unnested_unions(calculation.constraints)
resolve_union_result(
result,
{calculation.name, calculation.type, calculation, resource, unnested_types, domain}
)
else
result
end
Absinthe.Resolution.put_result(resolution, to_resolution({:ok, result}, context, domain))
end
end
defp unwrap_type({:array, type}),
do: unwrap_type(type)
defp unwrap_type(other), do: other
def resolve_assoc_one(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_assoc_one(
%{source: parent} = resolution,
{_domain, relationship}
) do
value =
if resolution.definition.alias do
Map.get(parent.calculations, {:__ash_graphql_relationship__, resolution.definition.alias})
else
Map.get(parent, relationship.name)
end
Absinthe.Resolution.put_result(resolution, {:ok, value})
end
def resolve_assoc_many(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_assoc_many(
%{source: parent} = resolution,
{_domain, relationship, pagination_strategy}
) do
page =
if resolution.definition.alias do
Map.get(parent.calculations, {:__ash_graphql_relationship__, resolution.definition.alias})
else
Map.get(parent, relationship.name)
end
result = paginate_relationship(page, pagination_strategy)
Absinthe.Resolution.put_result(resolution, result)
end
def resolve_id(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_id(
%{source: parent} = resolution,
{_resource, _field, relay_ids?}
) do
Absinthe.Resolution.put_result(
resolution,
{:ok, AshGraphql.Resource.encode_id(parent, relay_ids?)}
)
end
def resolve_union(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_union(
%{source: parent, context: context} = resolution,
{name, _field_type, _field, resource, _unnested_types, domain} = data
) do
domain = domain || context[:domain]
value =
if resolution.definition.alias do
Map.get(parent.calculations, {:__ash_graphql_attribute__, resolution.definition.alias})
else
Map.get(parent, name)
end
case value do
%struct{} when struct == Ash.ForbiddenField ->
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:error,
Ash.Error.Forbidden.ForbiddenField.exception(
resource: resource,
field: resolution.definition.name
)},
context,
domain
)
)
value ->
result = resolve_union_result(value, data)
Absinthe.Resolution.put_result(resolution, {:ok, result})
end
end
def resolve_attribute(
%{source: %resource{} = parent, context: context} = resolution,
{name, type, constraints, domain}
) do
domain = domain || context[:domain]
value =
if resolution.definition.alias && Ash.Type.can_load?(type, constraints) do
Map.get(parent.calculations, {:__ash_graphql_attribute__, resolution.definition.alias})
else
Map.get(parent, name)
end
case value do
%struct{} when struct == Ash.ForbiddenField ->
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:error,
Ash.Error.Forbidden.ForbiddenField.exception(
resource: resource,
field: resolution.definition.name
)},
context,
domain
)
)
value ->
Absinthe.Resolution.put_result(resolution, {:ok, value})
end
end
def resolve_attribute(
%{source: nil} = resolution,
_
) do
Absinthe.Resolution.put_result(resolution, {:ok, nil})
end
def resolve_attribute(
%{source: parent} = resolution,
{name, type, constraints, _domain}
)
when is_map(parent) do
value =
if resolution.definition.alias && Ash.Type.can_load?(type, constraints) do
Map.get(parent.calculations, {:__ash_graphql_attribute__, resolution.definition.alias})
else
Map.get(parent, name)
end
Absinthe.Resolution.put_result(resolution, {:ok, value})
end
def resolve_attribute(
%{source: source},
_
) do
raise "unknown source #{inspect(source)}"
end
defp resolve_union_result(
value,
{name, {:array, field_type}, field, resource, unnested_types, domain}
) do
if value do
Enum.map(
value,
&resolve_union_result(
&1,
{name, field_type, %{field | type: field_type, constraints: field.constraints[:items]},
resource, unnested_types, domain}
)
)
end
end
defp resolve_union_result(
value,
{_name, field_type, field, resource, unnested_types, _domain}
) do
case value do
%Ash.Union{type: type, value: value} = union ->
constraints = Ash.Type.NewType.constraints(field_type, field.constraints)
if type in unnested_types and not is_nil(value) do
type =
AshGraphql.Resource.field_type(
constraints[:types][type][:type],
%{field | constraints: constraints[:types][type][:constraints]},
resource
)
Map.put(value, :__union_type__, type)
else
union
end
other ->
other
end
end
def resolve_keyset(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_keyset(
%{source: parent} = resolution,
_field
) do
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent.__metadata__, :keyset)})
end
def resolve_composite_id(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_composite_id(
%{source: parent} = resolution,
{_resource, _fields, relay_ids?}
) do
Absinthe.Resolution.put_result(
resolution,
{:ok, AshGraphql.Resource.encode_id(parent, relay_ids?)}
)
end
def query_complexity(
%{limit: limit},
child_complexity,
_
) do
if child_complexity == 0 do
1
else
limit * child_complexity
end
end
def query_complexity(
_,
child_complexity,
_
) do
child_complexity + 1
end
def resolve_node(
%{arguments: %{id: id}} = resolution,
{type_to_domain_and_resource_map, all_domains}
) do
case AshGraphql.Resource.decode_relay_id(id) do
{:ok, %{type: type, id: primary_key}} ->
{domain, resource} = Map.fetch!(type_to_domain_and_resource_map, type)
# We can be sure this returns something since we check this at compile time
query = AshGraphql.Resource.primary_key_get_query(resource, all_domains)
# We pass relay_ids? as false since we pass the already decoded primary key
put_in(resolution.arguments.id, primary_key)
|> resolve({domain, resource, query, false})
{:error, _reason} = error ->
Absinthe.Resolution.put_result(resolution, error)
end
end
def resolve_node_type(%resource{}, _) do
AshGraphql.Resource.Info.type(resource)
end
defp apply_load_arguments(arguments, query, will_paginate?) do
Enum.reduce(arguments, query, fn
{:limit, limit}, query when not will_paginate? ->
Ash.Query.limit(query, limit)
{:offset, offset}, query when not will_paginate? ->
Ash.Query.offset(query, offset)
{:filter, value}, query ->
Ash.Query.filter_input(query, massage_filter(query.resource, value))
{:sort, value}, query ->
keyword_sort =
Enum.map(value, fn %{order: order, field: field} = input ->
case Ash.Resource.Info.calculation(query.resource, field) do
%{arguments: [_ | _]} ->
input_name = String.to_existing_atom("#{field}_input")
{field, {input[input_name] || %{}, order}}
_ ->
{field, order}
end
end)
Ash.Query.sort_input(query, keyword_sort)
_, query ->
query
end)
end
@doc false
def to_resolution({:ok, value}, _context, _domain), do: {:ok, value}
def to_resolution({:error, error}, context, domain) do
{:error,
error
|> unwrap_errors()
|> Enum.map(fn error ->
if AshGraphql.Error.impl_for(error) do
error = AshGraphql.Error.to_error(error)
case AshGraphql.Domain.Info.error_handler(domain) do
nil ->
error
{m, f, a} ->
apply(m, f, [error, context | a])
end
else
uuid = Ash.UUID.generate()
stacktrace =
case error do
%{stacktrace: %{stacktrace: v}} ->
v
_ ->
nil
end
Logger.warning(
"`#{uuid}`: AshGraphql.Error not implemented for error:\n\n#{Exception.format(:error, error, stacktrace)}"
)
if AshGraphql.Domain.Info.show_raised_errors?(domain) do
%{
message: """
Raised error: #{uuid}
#{Exception.format(:error, error, stacktrace)}"
"""
}
else
%{
message: "Something went wrong. Unique error id: `#{uuid}`"
}
end
end
end)}
end
end