Packages
ash_graphql
0.22.12
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 Ash.Query
require Logger
import AshGraphql.TraceHelpers
def resolve(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve(
%{arguments: arguments, context: context} = resolution,
{api, resource,
%{
name: query_name,
type: :get,
action: action,
identity: identity,
modify_resolution: modify
} = gql_query}
) do
case handle_arguments(resource, action, arguments) do
{:ok, arguments} ->
metadata = %{
api: api,
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.Api.Info.authorize?(api)
}
trace api,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
verbose?: AshGraphql.Api.Info.debug?(api)
]
filter = identity_filter(identity, resource, arguments)
query =
resource
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(Map.get(context, :ash_context) || %{})
|> set_query_arguments(action, arguments)
|> select_fields(resource, resolution)
{result, modify_args} =
case filter do
{:ok, filter} ->
query
|> Ash.Query.do_filter(filter)
|> load_fields(resource, api, resolution)
|> case do
{:ok, query} ->
result =
query
|> Ash.Query.for_read(action, %{},
actor: opts[:actor],
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> api.read_one(opts)
{result, [query, result]}
{:error, error} ->
{{:error, error}, [query, {:error, error}]}
end
{:error, error} ->
query =
resource
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(Map.get(context, :ash_context) || %{})
|> set_query_arguments(action, arguments)
|> select_fields(resource, resolution)
|> load_fields(resource, api, resolution)
{{:error, error}, [query, {:error, error}]}
end
case {result, gql_query.allow_nil?} do
{{:ok, nil}, false} ->
{:ok, filter} = filter
result = not_found(filter, resource, context, api)
resolution
|> Absinthe.Resolution.put_result(result)
|> add_root_errors(api, result)
{result, _} ->
resolution
|> Absinthe.Resolution.put_result(
to_resolution(
result
|> add_read_metadata(
gql_query,
Ash.Resource.Info.action(query.resource, action)
),
context,
api
)
)
|> add_root_errors(api, result)
|> modify_resolution(modify, modify_args)
end
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Api.Info.show_raised_errors?(api) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api))
else
something_went_wrong(resolution, e, api, __STACKTRACE__)
end
end
def resolve(
%{arguments: args, context: context} = resolution,
{api, resource,
%{name: query_name, type: :read_one, action: action, modify_resolution: modify} =
gql_query}
) do
metadata = %{
api: api,
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.Api.Info.authorize?(api)
}
case handle_arguments(resource, action, args) do
{:ok, args} ->
trace api,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
verbose?: AshGraphql.Api.Info.debug?(api)
]
query =
case Map.fetch(args, :filter) do
{:ok, filter} when filter != %{} ->
Ash.Query.do_filter(resource, filter)
_ ->
Ash.Query.new(resource)
end
query =
query
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(Map.get(context, :ash_context) || %{})
|> set_query_arguments(action, args)
|> select_fields(resource, resolution)
{result, modify_args} =
case load_fields(query, resource, api, resolution) do
{:ok, query} ->
result =
query
|> Ash.Query.for_read(action, %{},
actor: opts[:actor],
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> api.read_one(opts)
{result, [query, result]}
{: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, api))
|> add_root_errors(api, result)
|> modify_resolution(modify, modify_args)
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Api.Info.show_raised_errors?(api) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api))
else
something_went_wrong(resolution, e, api, __STACKTRACE__)
end
end
def resolve(
%{arguments: args, context: context} = resolution,
{api, resource,
%{
name: query_name,
type: :list,
relay?: relay?,
action: action,
modify_resolution: modify
} = gql_query}
) do
case handle_arguments(resource, action, args) do
{:ok, args} ->
metadata = %{
api: api,
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.Api.Info.authorize?(api)
}
trace api,
resource,
:gql_query,
query_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
verbose?: AshGraphql.Api.Info.debug?(api)
]
pagination = Ash.Resource.Info.action(resource, action).pagination
query = load_filter_and_sort_requirements(resource, args)
{result, modify_args} =
with {:ok, opts} <- validate_resolve_opts(resolution, pagination, opts, args),
result_fields <- get_result_fields(pagination, relay?),
initial_query <-
query
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(Map.get(context, :ash_context) || %{})
|> set_query_arguments(action, args)
|> select_fields(resource, resolution, result_fields),
{:ok, query} <-
load_fields(initial_query, resource, api, resolution, result_fields),
{:ok, page} <-
query
|> Ash.Query.for_read(action, %{},
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> api.read(opts) do
result = paginate(resource, 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, api))
|> add_root_errors(api, modify_args)
|> modify_resolution(modify, modify_args)
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Api.Info.show_raised_errors?(api) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api))
else
something_went_wrong(resolution, e, api, __STACKTRACE__)
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(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({:array, type}, constraints, value, name) when is_list(value) do
value
|> Enum.reduce_while({:ok, []}, fn value, {:ok, acc} ->
case handle_argument(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(Ash.Type.Union, constraints, value, name) do
handle_union_type(value, constraints, name)
end
defp handle_argument(type, constraints, value, name) do
cond do
Ash.Type.NewType.new_type?(type) ->
handle_argument(
Ash.Type.NewType.subtype_of(type),
Ash.Type.NewType.constraints(type, constraints),
value,
name
)
Ash.Type.embedded_type?(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
{:ok,
Map.new(value, fn {key, value} ->
field =
Enum.find(fields, fn field ->
field.name == key
end)
if field do
{key, handle_argument(field.type, field.constraints, value, "#{name}.#{key}")}
else
{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, Map.put_new(value, config[:tag], config[:tag_value])}
else
{:ok, 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, pagination, opts, args) do
with page_opts <-
args
|> Map.take([:limit, :offset, :first, :after, :before, :last])
|> Enum.reject(fn {_, val} -> is_nil(val) end),
{:ok, page_opts} <- validate_offset_opts(page_opts, pagination),
{:ok, page_opts} <- validate_keyset_opts(page_opts, pagination) do
field_names = resolution |> fields([]) |> names_only()
page =
if Enum.any?(field_names, &(&1 == :count)) do
Keyword.put(page_opts, :count, true)
else
page_opts
end
{:ok, Keyword.put(opts, :page, page)}
else
error ->
error
end
end
defp validate_offset_opts(opts, %{offset?: true, 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, %{keyset?: true, max_page_size: max_page_size}) 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}, true) do
["edges", "node"]
end
defp get_result_fields(%{keyset?: true}, false) do
["results"]
end
defp get_result_fields(%{offset?: true}, _) do
["results"]
end
defp get_result_fields(_pagination, _) do
[]
end
defp paginate(
_resource,
_action,
%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(
_resource,
_action,
%Ash.Page.Offset{results: results, count: count, more?: more},
true
) 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 = false
has_next_page = more
{
: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)
}
}
end
defp paginate(
_resource,
_action,
%Ash.Page.Offset{results: results, count: count, more?: more?},
_
) do
{:ok, %{results: results, count: count, more?: more?}}
end
defp paginate(resource, action, page, relay?) do
case Ash.Resource.Info.action(resource, action).pagination do
%{offset?: true} ->
paginate(
resource,
action,
%Ash.Page.Offset{results: page, count: Enum.count(page), more?: false},
relay?
)
%{keyset?: true} ->
paginate(
resource,
action,
%Ash.Page.Keyset{
results: page,
more?: false,
after: nil,
before: nil
},
relay?
)
_ ->
{:ok, page}
end
end
def mutate(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def mutate(
%{arguments: arguments, context: context} = resolution,
{api, resource,
%{
type: :create,
name: mutation_name,
action: action,
upsert?: upsert?,
upsert_identity: upsert_identity,
modify_resolution: modify
}}
) do
input = arguments[:input] || %{}
case handle_arguments(resource, action, input) do
{:ok, input} ->
metadata = %{
api: api,
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.Api.Info.authorize?(api)
}
trace api,
resource,
:gql_mutation,
mutation_name,
metadata do
opts = [
actor: Map.get(context, :actor),
action: action,
verbose?: AshGraphql.Api.Info.debug?(api),
upsert?: upsert?,
after_action: fn _changeset, result ->
load_fields(result, resource, api, resolution, ["result"])
end
]
opts =
if upsert? && upsert_identity do
Keyword.put(opts, :upsert_identity, upsert_identity)
else
opts
end
changeset =
resource
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.set_context(Map.get(context, :ash_context) || %{})
|> Ash.Changeset.for_create(action, input,
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> select_fields(resource, resolution, ["result"])
{result, modify_args} =
changeset
|> api.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, api)}},
[changeset, {:error, error}]}
end
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, api))
|> add_root_errors(api, modify_args)
|> modify_resolution(modify, modify_args)
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Api.Info.show_raised_errors?(api) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
if AshGraphql.Api.Info.root_level_errors?(api) do
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
else
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:ok, %{result: nil, errors: to_errors(error, context, api)}},
context,
api
)
)
end
else
something_went_wrong(resolution, e, api, __STACKTRACE__)
end
end
def mutate(
%{arguments: arguments, context: context} = resolution,
{api, resource,
%{
name: mutation_name,
type: :update,
action: action,
identity: identity,
read_action: read_action,
modify_resolution: modify
}}
) do
input = arguments[:input] || %{}
args_result =
with {:ok, input} <- handle_arguments(resource, action, input),
{:ok, read_action_input} <-
handle_arguments(resource, read_action, Map.delete(arguments, :input)) do
{:ok, input, read_action_input}
end
case args_result do
{:ok, input, read_action_input} ->
metadata = %{
api: api,
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.Api.Info.authorize?(api)
}
trace api,
resource,
:gql_mutation,
mutation_name,
metadata do
filter = identity_filter(identity, resource, arguments)
case filter do
{:ok, filter} ->
resource
|> Ash.Query.do_filter(filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(Map.get(context, :ash_context) || %{})
|> set_query_arguments(action, read_action_input)
|> api.read_one(
action: read_action,
verbose?: AshGraphql.Api.Info.debug?(api),
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> case do
{:ok, nil} ->
result = not_found(filter, resource, context, api)
resolution
|> Absinthe.Resolution.put_result(result)
|> add_root_errors(api, result)
{:ok, initial} ->
opts = [
actor: Map.get(context, :actor),
action: action,
verbose?: AshGraphql.Api.Info.debug?(api),
after_action: fn _changeset, result ->
load_fields(result, resource, api, resolution, ["result"])
end
]
changeset =
initial
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.set_context(Map.get(context, :ash_context) || %{})
|> Ash.Changeset.for_update(action, input,
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> select_fields(resource, resolution, ["result"])
{result, modify_args} =
changeset
|> api.update(opts)
|> case do
{:ok, value} ->
{{:ok,
add_metadata(%{result: value, errors: []}, value, changeset.action)},
[changeset, {:ok, value}]}
{:error, error} ->
{{:ok, %{result: nil, errors: to_errors(List.wrap(error), context, api)}},
[changeset, {:error, error}]}
end
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, api))
|> add_root_errors(api, modify_args)
|> modify_resolution(modify, modify_args)
{:error, error} ->
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
end
{:error, error} ->
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
end
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Api.Info.show_raised_errors?(api) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
if AshGraphql.Api.Info.root_level_errors?(api) do
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
else
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:ok, %{result: nil, errors: to_errors(error, context, api)}},
context,
api
)
)
end
else
something_went_wrong(resolution, e, api, __STACKTRACE__)
end
end
def mutate(
%{arguments: arguments, context: context} = resolution,
{api, resource,
%{
name: mutation_name,
type: :destroy,
action: action,
identity: identity,
read_action: read_action,
modify_resolution: modify
}}
) do
input = arguments[:input] || %{}
args_result =
with {:ok, input} <- handle_arguments(resource, action, input),
{:ok, read_action_input} <-
handle_arguments(resource, read_action, Map.delete(arguments, :input)) do
{:ok, input, read_action_input}
end
case args_result do
{:ok, input, read_action_input} ->
metadata = %{
api: api,
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.Api.Info.authorize?(api)
}
trace api,
resource,
:gql_mutation,
mutation_name,
metadata do
filter = identity_filter(identity, resource, arguments)
case filter do
{:ok, filter} ->
resource
|> Ash.Query.do_filter(filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.set_context(Map.get(context, :ash_context) || %{})
|> set_query_arguments(action, read_action_input)
|> api.read_one(
action: read_action,
verbose?: AshGraphql.Api.Info.debug?(api),
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> case do
{:ok, nil} ->
result = not_found(filter, resource, context, api)
resolution
|> Absinthe.Resolution.put_result(result)
|> add_root_errors(api, result)
{:ok, initial} ->
opts = destroy_opts(api, context, action)
changeset =
initial
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.set_context(Map.get(context, :ash_context) || %{})
|> Ash.Changeset.for_destroy(action, input,
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api)
)
|> select_fields(resource, resolution, ["result"])
{result, modify_args} =
changeset
|> api.destroy(opts)
|> destroy_result(initial, resource, changeset, api, resolution)
resolution
|> Absinthe.Resolution.put_result(to_resolution(result, context, api))
|> add_root_errors(api, result)
|> modify_resolution(modify, modify_args)
{:error, error} ->
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
end
{:error, error} ->
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
end
end
{:error, error} ->
{:error, error}
end
rescue
e ->
if AshGraphql.Api.Info.show_raised_errors?(api) do
error = Ash.Error.to_ash_error([e], __STACKTRACE__)
if AshGraphql.Api.Info.root_level_errors?(api) do
Absinthe.Resolution.put_result(
resolution,
to_resolution({:error, error}, context, api)
)
else
Absinthe.Resolution.put_result(
resolution,
to_resolution(
{:ok, %{result: nil, errors: to_errors(error, context, api)}},
context,
api
)
)
end
else
something_went_wrong(resolution, e, api, __STACKTRACE__)
end
end
defp log_exception(e, stacktrace) do
uuid = Ash.UUID.generate()
Logger.error("""
#{uuid}: Exception raised while resolving query.
#{Exception.format(:error, e, stacktrace)}
""")
uuid
end
defp something_went_wrong(resolution, e, api, stacktrace) do
tracer = AshGraphql.Api.Info.tracer(api)
if tracer do
tracer.set_error(Ash.Error.to_ash_error(e))
end
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) do
{:ok, nil}
end
def identity_filter(nil, resource, arguments) do
if AshGraphql.Resource.Info.encode_primary_key?(resource) do
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") 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.Query.expr(^expr and ref(^key) == ^value)}}
else
{:cont, {:ok, Ash.Query.expr(ref(^key) == ^value)}}
end
else
{:halt, {:error, "Required key not present"}}
end
end)
end
end
def identity_filter(identity, resource, arguments) 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
defp not_found(filter, resource, context, api) do
{:ok,
%{
result: nil,
errors:
to_errors(
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter || []),
resource: resource
),
context,
api
)
}}
end
defp load_filter_and_sort_requirements(resource, args) do
query =
case Map.fetch(args, :filter) do
{:ok, filter} ->
Ash.Query.do_filter(resource, massage_filter(resource, filter))
_ ->
Ash.Query.new(resource)
end
case Map.fetch(args, :sort) do
{:ok, sort} ->
keyword_sort =
Enum.map(sort, fn %{order: order, field: field} = input ->
case Ash.Resource.Info.calculation(resource, field) do
%{arguments: [_ | _]} ->
input_name = String.to_existing_atom("#{field}_input")
{field, {order, input[input_name] || %{}}}
_ ->
{field, order}
end
end)
Ash.Query.sort(query, keyword_sort)
_ ->
query
end
end
defp massage_filter(_resource, nil), do: nil
defp massage_filter(resource, filter) when is_map(filter) do
Map.new(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
defp 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
defp clear_fields(nil, _, _), do: nil
defp clear_fields(result, resource, resolution) do
resolution
|> fields(["result"])
|> names_only()
|> Enum.map(fn identifier ->
Ash.Resource.Info.aggregate(resource, identifier)
end)
|> Enum.filter(& &1)
|> Enum.map(& &1.name)
|> Enum.reduce(result, fn field, result ->
Map.put(result, field, nil)
end)
end
defp load_fields(query_or_record, resource, api, resolution, nested \\ []) do
fields = fields(resolution, nested)
fields
|> Enum.map(fn selection ->
aggregate = Ash.Resource.Info.aggregate(resource, selection.schema_node.identifier)
if aggregate do
aggregate.name
end
end)
|> Enum.filter(& &1)
|> case do
[] ->
{:ok, query_or_record}
loading ->
case query_or_record do
%Ash.Query{} = query ->
{:ok, Ash.Query.load(query, loading)}
record ->
api.load(record, loading)
end
end
end
defp select_fields(query_or_changeset, resource, resolution, nested \\ []) do
subfields =
resolution
|> fields(nested)
|> names_only()
|> Enum.map(&field_or_relationship(resource, &1))
|> Enum.filter(& &1)
|> names_only()
case query_or_changeset do
%Ash.Query{} = query ->
query |> Ash.Query.select(subfields)
%Ash.Changeset{} = changeset ->
changeset |> Ash.Changeset.select(subfields)
end
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(resolution, []) do
resolution
|> Absinthe.Resolution.project()
end
defp fields(resolution, names) do
project =
resolution
|> Absinthe.Resolution.project()
Enum.reduce(names, {project, resolution.fields_cache}, fn name, {fields, cache} ->
case fields |> Enum.find(&(&1.name == name)) do
nil ->
{fields, cache}
path ->
type = Absinthe.Schema.lookup_type(resolution.schema, path.schema_node.type)
path
|> Map.get(:selections)
|> Absinthe.Resolution.Projector.project(
type,
resolution.path ++ [path],
cache,
resolution
)
end
end)
|> elem(0)
end
defp names_only(fields) do
Enum.map(fields, fn
%{schema_node: %{identifier: identifier}} ->
identifier
%{name: name} ->
name
end)
end
defp set_query_arguments(query, action, arg_values) do
action = Ash.Resource.Info.action(query.resource, action)
action.arguments
|> Enum.reject(& &1.private?)
|> 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 destroy_opts(api, context, action) do
if AshGraphql.Api.Info.authorize?(api) do
[
actor: Map.get(context, :actor),
action: action,
verbose?: AshGraphql.Api.Info.debug?(api)
]
else
[
action: action,
verbose?: AshGraphql.Api.Info.debug?(api)
]
end
end
defp add_root_errors(resolution, api, {:error, error_or_errors}) do
do_root_errors(api, resolution, error_or_errors)
end
defp add_root_errors(resolution, api, [_, {:error, error_or_errors}]) do
do_root_errors(api, resolution, error_or_errors)
end
defp add_root_errors(resolution, api, [_, {:ok, %{errors: errors}}])
when errors not in [nil, []] do
do_root_errors(api, resolution, errors, false)
end
defp add_root_errors(resolution, api, {:ok, %{errors: errors}})
when errors not in [nil, []] do
do_root_errors(api, resolution, errors, false)
end
defp add_root_errors(resolution, _api, _other_thing) do
resolution
end
defp do_root_errors(api, resolution, error_or_errors, to_errors? \\ true) do
if AshGraphql.Api.Info.root_level_errors?(api) 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, api))
)
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
defp destroy_result(result, initial, resource, changeset, api, resolution) do
case result do
:ok ->
{{:ok, %{result: clear_fields(initial, resource, resolution), errors: []}},
[changeset, :ok]}
{:error, %{changeset: changeset} = error} ->
{{:ok, %{result: nil, errors: to_errors(changeset.errors, resolution.context, api)}},
{:error, error}}
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, api) do
AshGraphql.Errors.to_errors(errors, context, api)
end
def resolve_calculation(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_calculation(
%{source: parent, arguments: args, context: %{loader: loader} = context} = resolution,
{api, resource, calculation}
) do
api_opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api),
verbose?: AshGraphql.Api.Info.debug?(api)
]
tracer = AshGraphql.Api.Info.tracer(api)
opts = [
api_opts: api_opts,
type: :calculation,
resource: resource,
args: args,
tenant: Map.get(context, :tenant),
span_context: tracer && tracer.get_span_context()
]
batch_key = {calculation.name, opts}
do_dataloader(resolution, loader, api, batch_key, opts, parent)
end
def resolve_assoc(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_assoc(
%{source: parent, arguments: args, context: %{loader: loader} = context} = resolution,
{api, relationship}
) do
api_opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.Info.authorize?(api),
verbose?: AshGraphql.Api.Info.debug?(api)
]
query = load_filter_and_sort_requirements(relationship.destination, args)
args
|> apply_load_arguments(query)
|> select_fields(relationship.destination, resolution)
|> load_fields(relationship.destination, api, resolution)
|> case do
{:ok, related_query} ->
tracer = AshGraphql.Api.Info.tracer(api)
opts = [
query: related_query,
api_opts: api_opts,
type: :relationship,
args: args,
resource: relationship.source,
tenant: Map.get(context, :tenant),
span_context: tracer && tracer.get_span_context()
]
batch_key = {relationship.name, opts}
do_dataloader(resolution, loader, api, batch_key, args, parent)
{:error, error} ->
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api))
end
end
def resolve_id(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_id(
%{source: parent} = resolution,
{_resource, field}
) do
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, field)})
end
def resolve_keyset(%Absinthe.Resolution{state: :resolved} = resolution, _),
do: resolution
def resolve_keyset(
%{source: parent} = resolution,
_field
) do
parent.__metadata__
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}
) do
Absinthe.Resolution.put_result(
resolution,
{:ok, AshGraphql.Resource.encode_primary_key(parent)}
)
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 fetch_dataloader(loader, api, batch_key, context, parent) do
to_resolution(Dataloader.get(loader, api, batch_key, parent), context, api)
end
defp do_dataloader(
resolution,
loader,
api,
batch_key,
_args,
parent
) do
loader = Dataloader.load(loader, api, batch_key, parent)
fun = fn loader ->
fetch_dataloader(loader, api, batch_key, resolution.context, parent)
end
Absinthe.Resolution.put_result(
resolution,
{:middleware, Absinthe.Middleware.Dataloader, {loader, fun}}
)
end
defp apply_load_arguments(arguments, query) do
Enum.reduce(arguments, query, fn
{:limit, limit}, query ->
Ash.Query.limit(query, limit)
{:offset, offset}, query ->
Ash.Query.offset(query, offset)
{:filter, value}, query ->
decode_and_filter(query, value)
{:sort, value}, query ->
keyword_sort =
Enum.map(value, fn %{order: order, field: field} ->
{field, order}
end)
Ash.Query.sort(query, keyword_sort)
end)
end
defp decode_and_filter(query, value) do
Ash.Query.do_filter(query, value)
end
defp to_resolution({:ok, value}, _context, _api), do: {:ok, value}
defp to_resolution({:error, error}, context, api) do
{:error,
error
|> unwrap_errors()
|> Enum.map(fn error ->
if AshGraphql.Error.impl_for(error) do
error = AshGraphql.Error.to_error(error)
case AshGraphql.Api.Info.error_handler(api) 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.warn(
"`#{uuid}`: AshGraphql.Error not implemented for error:\n\n#{Exception.format(:error, error, stacktrace)}"
)
if AshGraphql.Api.Info.show_raised_errors?(api) do
%{
message: """
Raised error: #{uuid}
#{Exception.format(:error, error, stacktrace)}"
"""
}
else
%{
message: "Something went wrong. Unique error id: `#{uuid}`"
}
end
end
end)}
end
end