From f34ca424c685a7a869defda086c74cd6a7c3c296 Mon Sep 17 00:00:00 2001 From: Riccardo Binetti Date: Mon, 22 Jan 2024 19:06:54 +0100 Subject: [PATCH] feat: add node query Allow refetching a resource given it Relay global ID. Close #99. --- lib/ash_graphql.ex | 61 +++++++++++++++++++++++++++++-- lib/graphql/resolver.ex | 23 ++++++++++++ test/relay_ids_test.exs | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 3 deletions(-) diff --git a/lib/ash_graphql.ex b/lib/ash_graphql.ex index 9f83c329..8b4476f4 100644 --- a/lib/ash_graphql.ex +++ b/lib/ash_graphql.ex @@ -137,14 +137,25 @@ defmodule AshGraphql do api = unquote(api) action_middleware = unquote(action_middleware) - blueprint_with_queries = - api - |> AshGraphql.Api.queries( + api_queries = + AshGraphql.Api.queries( + api, unquote(resources), action_middleware, __MODULE__, unquote(relay_ids?) ) + + relay_queries = + if unquote(first?) and unquote(define_relay_types?) and unquote(relay_ids?) do + apis_with_resources = unquote(Enum.map(apis, &{elem(&1, 0), elem(&1, 1)})) + AshGraphql.relay_queries(apis_with_resources, unquote(schema), __ENV__) + else + [] + end + + blueprint_with_queries = + (relay_queries ++ api_queries) |> Enum.reduce(blueprint, fn query, blueprint -> Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query) end) @@ -351,6 +362,50 @@ defmodule AshGraphql do end end + def relay_queries(apis_with_resources, schema, env) do + type_to_api_and_resource_map = + apis_with_resources + |> Enum.flat_map(fn {api, resources} -> + resources + |> Enum.flat_map(fn resource -> + type = AshGraphql.Resource.Info.type(resource) + + if type do + [{type, {api, resource}}] + else + [] + end + end) + end) + |> Enum.into(%{}) + + [ + %Absinthe.Blueprint.Schema.FieldDefinition{ + name: "node", + identifier: :node, + arguments: [ + %Absinthe.Blueprint.Schema.InputValueDefinition{ + name: "id", + identifier: :id, + type: %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: :id + }, + description: "The Node unique identifier", + __reference__: AshGraphql.Resource.ref(env) + } + ], + middleware: [ + {{AshGraphql.Graphql.Resolver, :resolve_node}, type_to_api_and_resource_map} + ], + complexity: {AshGraphql.Graphql.Resolver, :query_complexity}, + module: schema, + description: "Retrieves a Node from its global id", + type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :node}, + __reference__: AshGraphql.Resource.ref(__ENV__) + } + ] + end + defp nested_attrs({:array, type}, constraints, already_checked) do nested_attrs(type, constraints[:items] || [], already_checked) end diff --git a/lib/graphql/resolver.ex b/lib/graphql/resolver.ex index f336bdd1..c498cfb7 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -2543,6 +2543,29 @@ defmodule AshGraphql.Graphql.Resolver do child_complexity + 1 end + def resolve_node(%{arguments: %{id: id}} = resolution, type_to_api_and_resource_map) do + with {:ok, {type, primary_key}} <- AshGraphql.Resource.decode_relay_id(id), + {:ok, {api, resource}} <- Map.fetch(type_to_api_and_resource_map, type) do + # TODO: what if there's no get query? It should probably not implement the node interface + query = get_query(resource) + + # We pass relay_ids? as false since we pass the already decoded primary key + put_in(resolution.arguments.id, primary_key) + |> resolve({api, resource, query, false}) + else + _ -> + # TODO: is this return value ok? + Absinthe.Resolution.put_result(resolution, {:error, "Invalid primary key"}) + end + end + + defp get_query(resource) do + # Find the get query with no identities, i.e. the one that uses the primary key + resource + |> AshGraphql.Resource.Info.queries() + |> Enum.find(&(&1.type == :get and not (&1.identity || false))) + end + def resolve_node_type(%resource{}, _) do AshGraphql.Resource.Info.type(resource) end diff --git a/test/relay_ids_test.exs b/test/relay_ids_test.exs index 30ed80ad..8e16990c 100644 --- a/test/relay_ids_test.exs +++ b/test/relay_ids_test.exs @@ -107,5 +107,86 @@ defmodule AshGraphql.RelayIdsTest do assert {:ok, result} = resp assert result[:errors] != nil end + + test "allows retrieving resources with the node query" do + user = + User + |> Ash.Changeset.for_create(:create, %{name: "fred"}) + |> Api.create!() + + post = + Post + |> Ash.Changeset.for_create( + :create, + %{ + author_id: user.id, + text: "foo", + published: true + } + ) + |> Api.create!() + + user_relay_id = AshGraphql.Resource.encode_relay_id(user) + post_relay_id = AshGraphql.Resource.encode_relay_id(post) + + document = + """ + query Node($id: ID!) { + node(id: $id) { + __typename + + ... on User { + name + } + + ... on Post { + text + } + } + } + """ + + resp = + document + |> Absinthe.run(Schema, + variables: %{ + "id" => post_relay_id + } + ) + + assert {:ok, result} = resp + + refute Map.has_key?(result, :errors) + + assert %{ + data: %{ + "node" => %{ + "__typename" => "Post", + "text" => "foo" + } + } + } = result + + resp = + document + |> Absinthe.run(Schema, + variables: %{ + "id" => user_relay_id + } + ) + + assert {:ok, result} = resp + + refute Map.has_key?(result, :errors) + + assert %{ + data: %{ + "node" => %{ + "__typename" => "User", + "name" => "fred" + } + } + } = result + end end end