defmodule AshGraphql.Graphql.Resolver do @moduledoc false require Ash.Query require Logger def resolve( %{arguments: arguments, context: context} = resolution, {api, resource, %{type: :get, action: action, identity: identity, modify_resolution: modify} = gql_query} ) 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, context, api)) |> add_root_errors(api, result) |> modify_resolution(modify, modify_args) 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) end end def resolve( %{arguments: args, context: context} = resolution, {api, resource, %{type: :read_one, action: action, modify_resolution: modify}} ) 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 resolution |> Absinthe.Resolution.put_result(to_resolution(result, context, api)) |> add_root_errors(api, result) |> modify_resolution(modify, modify_args) 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) end end def resolve( %{arguments: args, context: context} = resolution, {api, resource, %{type: :list, relay?: relay?, action: action, modify_resolution: modify}} ) 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 resolution |> Absinthe.Resolution.put_result(to_resolution(result, context, api)) |> add_root_errors(api, modify_args) |> modify_resolution(modify, modify_args) 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) end end def validate_resolve_opts(resolution, pagination, opts, args) do case args |> Map.take([:limit, :offset, :first, :after, :before, :last]) |> Enum.reject(fn {_, val} -> is_nil(val) end) |> validate_pagination_opts(pagination) do {:ok, []} -> {:ok, opts} {:ok, page_opts} -> page_fields = get_page_fields(pagination) field_names = resolution |> fields(page_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)} error -> error end end defp validate_pagination_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_pagination_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_pagination_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 get_page_fields(%{keyset?: true}) do ["pageInfo"] end defp get_page_fields(_pagination) do [] end defp paginate( _resource, _action, %Ash.Page.Keyset{ results: results, more?: more, after: after_cursor, before: before_cursor }, 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, 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 { :ok, %{ page_info: %{ start_cursor: start_cursor, end_cursor: end_cursor, has_next_page: has_next_page, has_previous_page: has_previous_page }, edges: Enum.map(results, fn result -> %{ cursor: result.__metadata__.keyset, node: result } end) } } end defp paginate( _resource, _action, %Ash.Page.Keyset{ results: results, count: count }, false ) do {:ok, %{results: results, count: count}} end defp paginate(_resource, _action, %Ash.Page.Offset{results: results, count: count}, _) do {:ok, %{results: results, count: count}} 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)}, relay? ) %{keyset?: true} -> paginate( resource, action, %Ash.Page.Keyset{ results: page, more?: false, after: nil, before: nil }, relay? ) _ -> {:ok, page} end end def mutate( %{arguments: arguments, context: context} = resolution, {api, resource, %{ type: :create, action: action, upsert?: upsert?, upsert_identity: upsert_identity, modify_resolution: modify }} ) do input = arguments[:input] || %{} 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) 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) end end def mutate( %{arguments: arguments, context: context} = resolution, {api, resource, %{ type: :update, action: action, identity: identity, read_action: read_action, modify_resolution: modify }} ) do input = arguments[:input] || %{} 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, arguments) |> 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 nil -> result = not_found(filter, resource, context, api) resolution |> Absinthe.Resolution.put_result(result) |> add_root_errors(api, result) 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) ) |> Ash.Changeset.set_arguments(arguments) |> 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) end {:error, error} -> Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api)) 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) end end def mutate( %{arguments: arguments, context: context} = resolution, {api, resource, %{ type: :destroy, action: action, identity: identity, read_action: read_action, modify_resolution: modify }} ) do filter = identity_filter(identity, resource, arguments) input = arguments[:input] || %{} 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, arguments) |> 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 nil -> result = not_found(filter, resource, context, api) resolution |> Absinthe.Resolution.put_result(result) |> add_root_errors(api, result) 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) ) |> Ash.Changeset.set_arguments(arguments) |> 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) end {:error, error} -> Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api)) 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) end end defp log_exception(e) do uuid = Ash.UUID.generate() Logger.error(""" #{uuid}: Exception raised while resolving query. #{Exception.message(e)} """) uuid end defp something_went_wrong(resolution, e) do uuid = log_exception(e) 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} -> {field, order} end) fields = keyword_sort |> Keyword.keys() |> Enum.filter(&Ash.Resource.Info.public_aggregate(resource, &1)) query |> Ash.Query.load(fields) |> Ash.Query.sort(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_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 defp unwrap_errors([]), do: [] defp unwrap_errors(errors) do errors |> List.wrap() |> Enum.flat_map(fn %Ash.Error.Invalid{errors: errors} -> unwrap_errors(List.wrap(errors)) errors -> List.wrap(errors) end) end defp to_errors(errors, context, api) do errors |> 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() if is_exception(error) do case error do %{stacktrace: %{stacktrace: stacktrace}} -> Logger.warn( "`#{uuid}`: AshGraphql.Error not implemented for error:\n\n#{Exception.format(:error, error, stacktrace)}" ) error -> Logger.warn( "`#{uuid}`: AshGraphql.Error not implemented for error:\n\n#{Exception.format(:error, error)}" ) end else Logger.warn( "`#{uuid}`: AshGraphql.Error not implemented for error:\n\n#{inspect(error)}" ) end %{ message: "something went wrong. Unique error id: `#{uuid}`" } end end) end 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) ] opts = [ api_opts: api_opts, type: :calculation, args: args, tenant: Map.get(context, :tenant) ] batch_key = {calculation.name, opts} do_dataloader(resolution, loader, api, batch_key, opts, parent) end 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} -> opts = [ query: related_query, api_opts: api_opts, type: :relationship, args: args, resource: relationship.source, tenant: Map.get(context, :tenant) ] 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( %{source: parent} = resolution, {_resource, field} ) do Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, field)}) end 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)}" ) %{ message: "Something went wrong. Unique error id: `#{uuid}`" } end end)} end end