Skip to content

Commit

Permalink
feat: add node query
Browse files Browse the repository at this point in the history
Allow retrieving a resource implementing the Node interface given its Relay
global id.

Close #99
  • Loading branch information
rbino committed Jan 24, 2024
1 parent 2886e0c commit ce4c4d8
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 4 deletions.
61 changes: 58 additions & 3 deletions lib/ash_graphql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions lib/graphql/resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2586,6 +2586,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
Expand Down
119 changes: 118 additions & 1 deletion test/relay_ids_test.exs
Original file line number Diff line number Diff line change
@@ -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 ->
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions test/support/relay_ids/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions test/support/relay_ids/resources/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ce4c4d8

Please sign in to comment.