From d5d66253f122104b490d2ee068da34458808a186 Mon Sep 17 00:00:00 2001 From: Riccardo Binetti Date: Wed, 24 Jan 2024 19:10:38 +0100 Subject: [PATCH] feat: add node query Allow retrieving a resource implementing the Node interface given its Relay global id. Close #99 --- lib/ash_graphql.ex | 61 ++++++++- lib/graphql/resolver.ex | 16 +++ test/relay_ids_test.exs | 119 +++++++++++++++++- test/support/relay_ids/registry.ex | 1 + .../resource_with_no_primary_key_get.ex | 38 ++++++ test/support/relay_ids/resources/user.ex | 4 + 6 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 test/support/relay_ids/resources/resource_with_no_primary_key_get.ex 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 c545c078..d988a2fd 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -2588,6 +2588,22 @@ defmodule AshGraphql.Graphql.Resolver do child_complexity + 1 end + def resolve_node(%{arguments: %{id: id}} = resolution, type_to_api_and_resource_map) do + case AshGraphql.Resource.decode_relay_id(id) do + {:ok, {type, primary_key}} -> + {api, resource} = Map.fetch!(type_to_api_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) + + # 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}) + + {:error, _reason} = error -> + Absinthe.Resolution.put_result(resolution, error) + end + 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 e4635c9a..c6650228 100644 --- a/test/relay_ids_test.exs +++ b/test/relay_ids_test.exs @@ -1,7 +1,7 @@ defmodule AshGraphql.RelayIdsTest do use ExUnit.Case, async: false - alias AshGraphql.Test.RelayIds.{Api, Post, Schema, User} + alias AshGraphql.Test.RelayIds.{Api, Post, ResourceWithNoPrimaryKeyGet, Schema, User} setup do on_exit(fn -> @@ -108,4 +108,121 @@ defmodule AshGraphql.RelayIdsTest do assert [%{code: "invalid_primary_key"}] = result[:errors] end end + + describe "node interface and query" do + test "allows retrieving resources" 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 + + test "return an error for resources without a primary key get" do + resource = + ResourceWithNoPrimaryKeyGet + |> Ash.Changeset.for_create(:create, %{name: "foo"}) + |> Api.create!() + + document = + """ + query Node($id: ID!) { + node(id: $id) { + __typename + + ... on ResourceWithNoPrimaryKeyGet{ + name + } + } + } + """ + + resource_relay_id = AshGraphql.Resource.encode_relay_id(resource) + + resp = + document + |> Absinthe.run(Schema, + variables: %{ + "id" => resource_relay_id + } + ) + + assert {:ok, result} = resp + + assert result[:errors] != nil + end + end end diff --git a/test/support/relay_ids/registry.ex b/test/support/relay_ids/registry.ex index 4ed2182d..6a9f9108 100644 --- a/test/support/relay_ids/registry.ex +++ b/test/support/relay_ids/registry.ex @@ -4,6 +4,7 @@ defmodule AshGraphql.Test.RelayIds.Registry do entries do entry(AshGraphql.Test.RelayIds.Post) + entry(AshGraphql.Test.RelayIds.ResourceWithNoPrimaryKeyGet) entry(AshGraphql.Test.RelayIds.User) end end diff --git a/test/support/relay_ids/resources/resource_with_no_primary_key_get.ex b/test/support/relay_ids/resources/resource_with_no_primary_key_get.ex new file mode 100644 index 00000000..1c84f09b --- /dev/null +++ b/test/support/relay_ids/resources/resource_with_no_primary_key_get.ex @@ -0,0 +1,38 @@ +defmodule AshGraphql.Test.RelayIds.ResourceWithNoPrimaryKeyGet do + @moduledoc false + + use Ash.Resource, + data_layer: Ash.DataLayer.Ets, + extensions: [AshGraphql.Resource] + + graphql do + type :resource_with_no_primary_key_get + + queries do + get :get_resource_by_name, :get_by_name + end + + mutations do + create :create_resource, :create + end + end + + actions do + defaults([:create, :update, :destroy, :read]) + + read(:get_by_name, get_by: :name) + end + + attributes do + uuid_primary_key(:id) + attribute(:name, :string, allow_nil?: false) + end + + identities do + identity(:name, [:name], pre_check_with: AshGraphql.Test.RelayIds.Api) + end + + relationships do + has_many(:posts, AshGraphql.Test.RelayIds.Post, destination_attribute: :author_id) + end +end diff --git a/test/support/relay_ids/resources/user.ex b/test/support/relay_ids/resources/user.ex index a46b56f6..577ff111 100644 --- a/test/support/relay_ids/resources/user.ex +++ b/test/support/relay_ids/resources/user.ex @@ -8,6 +8,10 @@ defmodule AshGraphql.Test.RelayIds.User do graphql do type :user + queries do + get :get_user, :read + end + mutations do create :create_user, :create end