Skip to content

Commit

Permalink
feat: add node query
Browse files Browse the repository at this point in the history
Allow refetching a resource given it Relay global ID.
Close ash-project#99.
  • Loading branch information
rbino committed Jan 22, 2024
1 parent 8f4604a commit f34ca42
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 3 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
23 changes: 23 additions & 0 deletions lib/graphql/resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions test/relay_ids_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit f34ca42

Please sign in to comment.