ash_graphql/lib/graphql/resolver.ex

541 lines
15 KiB
Elixir
Raw Normal View History

2020-05-02 10:35:12 +12:00
defmodule AshGraphql.Graphql.Resolver do
2020-08-14 10:55:34 +12:00
@moduledoc false
require Ash.Query
2020-10-10 14:17:34 +13:00
2020-05-02 10:35:12 +12:00
def resolve(
%{arguments: arguments, context: context} = resolution,
{api, resource, %{type: :get, action: action, identity: identity}}
2020-05-02 10:35:12 +12:00
) do
2020-09-29 02:42:36 +13:00
opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.authorize?(api),
action: action,
verbose?: AshGraphql.Api.debug?(api)
2020-09-29 02:42:36 +13:00
]
2020-06-06 06:41:00 +12:00
filter =
if identity do
2021-01-13 09:14:35 +13:00
{:ok,
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.identities()
2021-01-13 09:14:35 +13:00
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)}
else
2021-01-13 09:14:35 +13:00
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
{:ok, value} -> {:ok, [id: value]}
{:error, error} -> {:error, error}
end
end
2020-10-28 19:16:16 +13:00
result =
2021-01-13 09:14:35 +13:00
case filter do
{:ok, filter} ->
resource
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.filter(^filter)
|> set_query_arguments(action, arguments)
|> select_fields(resource, resolution)
|> load_fields(resource, api, resolution)
|> case do
{:ok, query} ->
api.read_one(query, opts)
{:error, error} ->
{:error, error}
end
2021-01-13 09:14:35 +13:00
{:error, error} ->
{:error, error}
end
2020-05-02 10:35:12 +12:00
Absinthe.Resolution.put_result(resolution, to_resolution(result))
2021-04-04 19:10:50 +12:00
end
def resolve(
%{arguments: args, context: context} = resolution,
{api, resource, %{type: :read_one, action: action}}
) do
opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.authorize?(api),
action: action,
verbose?: AshGraphql.Api.debug?(api)
]
query =
case Map.fetch(args, :filter) do
{:ok, filter} ->
Ash.Query.filter(resource, ^filter)
_ ->
Ash.Query.new(resource)
end
result =
query
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> set_query_arguments(action, args)
|> select_fields(resource, resolution)
|> load_fields(resource, api, resolution)
|> case do
{:ok, query} ->
api.read_one(query, opts)
{:error, error} ->
{:error, error}
end
Absinthe.Resolution.put_result(resolution, to_resolution(result))
2020-05-02 10:35:12 +12:00
end
def resolve(
%{arguments: args, context: context, definition: %{selections: selections}} = resolution,
{api, resource, %{type: :list, action: action}}
2020-05-02 10:35:12 +12:00
) do
2020-09-29 02:42:36 +13:00
opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.authorize?(api),
action: action,
verbose?: AshGraphql.Api.debug?(api)
2020-09-29 02:42:36 +13:00
]
2020-06-06 06:41:00 +12:00
page_opts =
args
|> Map.take([:limit, :offset, :after, :before])
|> Enum.reject(fn {_, val} -> is_nil(val) end)
opts =
case page_opts do
[] ->
opts
page_opts ->
if Enum.any?(selections, &(&1.name == :count)) do
page_opts = Keyword.put(page_opts, :count, true)
Keyword.put(opts, :page, page_opts)
else
Keyword.put(opts, :page, page_opts)
end
end
2020-08-14 09:39:59 +12:00
query =
case Map.fetch(args, :filter) do
{:ok, filter} ->
2020-12-31 07:10:10 +13:00
Ash.Query.filter(resource, ^filter)
2020-08-14 09:39:59 +12:00
_ ->
Ash.Query.new(resource)
2020-08-14 09:39:59 +12:00
end
2020-11-12 16:41:54 +13:00
query =
case Map.fetch(args, :sort) do
{:ok, sort} ->
keyword_sort =
Enum.map(sort, fn %{order: order, field: field} ->
{field, order}
end)
Ash.Query.sort(query, keyword_sort)
_ ->
query
end
nested =
if Ash.Resource.Info.action(resource, action, :read).pagination do
"results"
else
nil
end
2020-08-14 09:39:59 +12:00
result =
query
2020-10-28 19:16:16 +13:00
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> set_query_arguments(action, args)
|> select_fields(resource, resolution, nested)
|> load_fields(resource, api, resolution, nested)
|> case do
{:ok, query} ->
query
|> api.read(opts)
|> case do
{:ok, %{results: results, count: count}} ->
{:ok, %{results: results, count: count}}
{:ok, results} ->
if Ash.Resource.Info.action(resource, action, :read).pagination do
{:ok, %{results: results, count: Enum.count(results)}}
else
{:ok, results}
end
error ->
error
end
{:error, error} ->
{:error, error}
2020-05-02 10:35:12 +12:00
end
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
2020-08-14 09:39:59 +12:00
def mutate(
%{arguments: %{input: input}, context: context} = resolution,
{api, resource, %{type: :create, action: action, upsert?: upsert?}}
2020-08-14 09:39:59 +12:00
) do
2020-09-29 02:42:36 +13:00
opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.authorize?(api),
action: action,
verbose?: AshGraphql.Api.debug?(api),
upsert?: upsert?
2020-09-29 02:42:36 +13:00
]
2020-08-14 09:39:59 +12:00
result =
resource
|> Ash.Changeset.new()
2020-11-03 15:15:30 +13:00
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.for_create(action, input)
|> select_fields(resource, resolution, "result")
2020-10-28 19:16:16 +13:00
|> api.create(opts)
|> case do
2020-09-24 12:54:57 +12:00
{:ok, value} ->
case load_fields(value, resource, api, resolution, "result") do
{:ok, result} ->
{:ok, %{result: result, errors: []}}
{:error, error} ->
{:ok, %{result: nil, errors: to_errors(List.wrap(error))}}
end
2020-09-24 12:54:57 +12:00
{:error, %{changeset: changeset}} ->
{:ok, %{result: nil, errors: to_errors(changeset.errors)}}
2020-08-14 09:39:59 +12:00
end
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
def mutate(
%{arguments: %{input: input} = arguments, context: context} = resolution,
{api, resource, %{type: :update, action: action, identity: identity}}
2020-08-14 09:39:59 +12:00
) do
filter =
if identity do
2021-01-13 09:14:35 +13:00
{:ok,
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.identities()
2021-01-13 09:14:35 +13:00
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)}
else
2021-01-13 09:14:35 +13:00
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
{:ok, value} -> {:ok, [id: value]}
{:error, error} -> {:error, error}
end
end
2021-01-13 09:14:35 +13:00
case filter do
{:ok, filter} ->
resource
|> Ash.Query.filter(^filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> set_query_arguments(action, arguments)
|> api.read_one!(verbose?: AshGraphql.Api.debug?(api))
2021-01-13 09:14:35 +13:00
|> case do
nil ->
not_found(filter, resource)
2021-01-13 09:14:35 +13:00
initial ->
opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.authorize?(api),
action: action,
verbose?: AshGraphql.Api.debug?(api)
2021-01-13 09:14:35 +13:00
]
result =
initial
|> Ash.Changeset.new()
2021-01-13 09:14:35 +13:00
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.for_update(action, input)
2021-01-13 09:14:35 +13:00
|> Ash.Changeset.set_arguments(arguments)
|> select_fields(resource, resolution, "result")
2021-01-13 09:14:35 +13:00
|> api.update(opts)
|> update_result(resource, api, resolution)
2021-01-13 09:14:35 +13:00
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
{:error, error} ->
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}))
2020-08-14 09:39:59 +12:00
end
end
2020-10-28 19:16:16 +13:00
def mutate(
%{arguments: arguments, context: context} = resolution,
{api, resource, %{type: :destroy, action: action, identity: identity}}
2020-10-28 19:16:16 +13:00
) do
filter =
if identity do
2021-01-13 09:14:35 +13:00
{:ok,
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.identities()
2021-01-13 09:14:35 +13:00
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)}
else
2021-01-13 09:14:35 +13:00
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
{:ok, value} -> {:ok, [id: value]}
{:error, error} -> {:error, error}
end
end
2021-01-13 09:14:35 +13:00
case filter do
{:ok, filter} ->
resource
|> Ash.Query.filter(^filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> set_query_arguments(action, arguments)
|> api.read_one!(verbose?: AshGraphql.Api.debug?(api))
2021-01-13 09:14:35 +13:00
|> case do
nil ->
not_found(filter, resource)
2021-01-13 09:14:35 +13:00
initial ->
opts = destroy_opts(api, context, action)
result =
initial
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> select_fields(resource, resolution, "result")
2021-01-13 09:14:35 +13:00
|> api.destroy(opts)
|> destroy_result(initial, resource, resolution)
2021-01-13 09:14:35 +13:00
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
{:error, error} ->
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}))
end
end
2020-08-14 09:39:59 +12:00
defp not_found(filter, resource) do
{:ok,
%{
result: nil,
errors: [
to_errors(
Ash.Error.Query.NotFound.exception(
primary_key: Map.new(filter),
resource: resource
)
)
]
}}
end
defp clear_fields(result, resource, resolution) do
resolution
|> fields(true)
|> Enum.map(fn field ->
Ash.Resource.Info.aggregate(resource, field.schema_node.identifier) ||
Ash.Resource.Info.calculation(resource, field.schema_node.identifier) ||
Ash.Resource.Info.attribute(resource, field.schema_node.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 \\ nil) do
loading =
resolution
|> fields(nested)
|> Enum.map(fn field ->
Ash.Resource.Info.aggregate(resource, field.schema_node.identifier) ||
Ash.Resource.Info.calculation(resource, field.schema_node.identifier)
end)
|> Enum.filter(& &1)
|> Enum.map(& &1.name)
case query_or_record do
%Ash.Query{} = query ->
{:ok, Ash.Query.load(query, loading)}
record ->
api.load(record, loading)
end
end
defp select_fields(query_or_changeset, resource, resolution, nested \\ nil) do
subfields =
resolution
|> fields(nested)
|> Enum.map(&Ash.Resource.Info.attribute(resource, &1.schema_node.identifier))
|> Enum.filter(& &1)
|> Enum.map(& &1.name)
case query_or_changeset do
%Ash.Query{} = query ->
Ash.Query.select(query, subfields)
%Ash.Changeset{} = changeset ->
Ash.Changeset.select(changeset, subfields)
end
end
defp fields(resolution, nested) do
if nested do
resolution
|> Absinthe.Resolution.project()
|> Enum.find(&(&1.name == nested))
|> Map.get(:selections)
else
Absinthe.Resolution.project(resolution)
end
end
defp set_query_arguments(query, action, arg_values) do
2021-02-19 04:16:00 +13:00
action = Ash.Resource.Info.action(query.resource, action, :read)
action.arguments
|> Enum.reject(& &1.private?)
|> Enum.reduce(query, fn argument, query ->
Ash.Query.set_argument(query, argument.name, Map.get(arg_values, argument.name))
end)
end
2021-01-13 09:14:35 +13:00
defp destroy_opts(api, context, action) do
if AshGraphql.Api.authorize?(api) do
[actor: Map.get(context, :actor), action: action, verbose?: AshGraphql.Api.debug?(api)]
2021-01-13 09:14:35 +13:00
else
[action: action, verbose?: AshGraphql.Api.debug?(api)]
2021-01-13 09:14:35 +13:00
end
end
2020-08-14 09:39:59 +12:00
defp update_result(result, resource, api, resolution) do
2021-01-13 09:14:35 +13:00
case result do
{:ok, value} ->
case load_fields(value, resource, api, resolution, "result") do
{:ok, result} ->
{:ok, %{result: result, errors: []}}
{:error, error} ->
{:ok, %{result: nil, errors: to_errors(List.wrap(error))}}
end
2021-01-13 09:14:35 +13:00
{:error, error} ->
{:ok, %{result: nil, errors: to_errors(List.wrap(error))}}
2021-01-13 09:14:35 +13:00
end
end
defp destroy_result(result, initial, resource, resolution) do
2021-01-13 09:14:35 +13:00
case result do
:ok ->
{:ok, %{result: clear_fields(initial, resource, resolution), errors: []}}
{:error, %{changeset: changeset}} ->
{:ok, %{result: nil, errors: to_errors(changeset.error)}}
2021-01-13 09:14:35 +13:00
end
end
2020-08-14 09:39:59 +12:00
defp to_errors(errors) do
errors
|> List.wrap()
|> Enum.map(fn error ->
if AshGraphql.Error.impl_for(error) do
AshGraphql.Error.to_error(error)
else
%{
message: "something went wrong."
}
2020-08-14 09:39:59 +12:00
end
end)
end
2020-09-24 12:54:57 +12:00
def resolve_assoc(
2020-10-28 19:16:16 +13:00
%{source: parent, arguments: args, context: %{loader: loader} = context} = resolution,
2020-09-24 12:54:57 +12:00
{api, relationship}
) do
api_opts = [
actor: Map.get(context, :actor),
authorize?: AshGraphql.Api.authorize?(api),
verbose?: AshGraphql.Api.debug?(api)
]
2020-09-29 02:42:36 +13:00
opts = [
query: apply_load_arguments(args, Ash.Query.new(relationship.destination)),
2020-10-28 19:16:16 +13:00
api_opts: api_opts,
2020-11-12 16:41:54 +13:00
args: args,
2020-10-28 19:16:16 +13:00
tenant: Map.get(context, :tenant)
2020-09-29 02:42:36 +13:00
]
2020-09-24 12:54:57 +12:00
{batch_key, parent} = {{relationship.name, opts}, parent}
2020-08-14 09:39:59 +12:00
2020-09-29 02:42:36 +13:00
do_dataloader(resolution, loader, api, batch_key, args, parent)
2020-08-14 09:39:59 +12:00
end
2020-09-24 12:54:57 +12:00
defp do_dataloader(
resolution,
loader,
api,
batch_key,
2020-10-28 19:16:16 +13:00
_args,
2020-09-29 02:42:36 +13:00
parent
2020-09-24 12:54:57 +12:00
) do
loader = Dataloader.load(loader, api, batch_key, parent)
fun = fn loader ->
2020-10-28 19:16:16 +13:00
{:ok, Dataloader.get(loader, api, batch_key, parent)}
2020-09-24 12:54:57 +12:00
end
2020-08-14 09:39:59 +12:00
2020-09-24 12:54:57 +12:00
Absinthe.Resolution.put_result(
resolution,
{:middleware, Absinthe.Middleware.Dataloader, {loader, fun}}
)
end
2020-08-14 09:39:59 +12:00
2020-09-24 12:54:57 +12:00
defp apply_load_arguments(arguments, query) do
Enum.reduce(arguments, query, fn
{:limit, limit}, query ->
Ash.Query.limit(query, limit)
2020-08-15 02:20:47 +12:00
2020-09-24 12:54:57 +12:00
{:offset, offset}, query ->
Ash.Query.offset(query, offset)
2020-08-15 02:20:47 +12:00
2020-09-24 12:54:57 +12:00
{:filter, value}, query ->
2020-08-15 02:20:47 +12:00
decode_and_filter(query, value)
2020-11-12 16:41:54 +13:00
{:sort, value}, query ->
keyword_sort =
Enum.map(value, fn %{order: order, field: field} ->
{field, order}
end)
Ash.Query.sort(query, keyword_sort)
2020-08-15 02:20:47 +12:00
end)
end
defp decode_and_filter(query, value) do
2020-12-31 07:10:10 +13:00
Ash.Query.filter(query, ^value)
2020-08-15 02:20:47 +12:00
end
2020-05-02 10:35:12 +12:00
defp to_resolution({:ok, value}), do: {:ok, value}
2020-08-14 09:39:59 +12:00
defp to_resolution({:error, error}),
do: {:error, error |> List.wrap() |> Enum.map(&Exception.message(&1))}
2020-05-02 10:35:12 +12:00
end