Skip to content

Commit

Permalink
feat: Relay refetching support (#106)
Browse files Browse the repository at this point in the history
* feat: add support for relay global IDs

* improvement: use the GraphQL type when projecting fields

This allows picking also up fields coming from fragments in queries returning an
interface

* feat: add relay node query

Allow retrieving a resource implementing the Node interface given its Relay
global id.
  • Loading branch information
rbino authored Jan 24, 2024
1 parent 7225d5f commit 365b3ae
Show file tree
Hide file tree
Showing 14 changed files with 708 additions and 96 deletions.
11 changes: 11 additions & 0 deletions documentation/topics/relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ Use the following option when calling `use AshGraphql`
```elixir
use AshGraphql, define_relay_types?: false
```

## Relay Global IDs

Use the following option to generate Relay Global IDs (see
[here](https://relay.dev/graphql/objectidentification.htm)).

```elixir
use AshGraphql, relay_ids?: true
```

This allows refetching a node using the `node` query and passing its global ID.
18 changes: 12 additions & 6 deletions lib/api/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,33 @@ defmodule AshGraphql.Api do
defdelegate debug?(api), to: AshGraphql.Api.Info

@doc false
def queries(api, resources, action_middleware, schema) do
Enum.flat_map(resources, &AshGraphql.Resource.queries(api, &1, action_middleware, schema))
def queries(api, resources, action_middleware, schema, relay_ids?) do
Enum.flat_map(
resources,
&AshGraphql.Resource.queries(api, &1, action_middleware, schema, relay_ids?)
)
end

@doc false
def mutations(api, resources, action_middleware, schema) do
def mutations(api, resources, action_middleware, schema, relay_ids?) do
resources
|> Enum.filter(fn resource ->
AshGraphql.Resource in Spark.extensions(resource)
end)
|> Enum.flat_map(&AshGraphql.Resource.mutations(api, &1, action_middleware, schema))
|> Enum.flat_map(
&AshGraphql.Resource.mutations(api, &1, action_middleware, schema, relay_ids?)
)
end

@doc false
def type_definitions(api, resources, schema, env, first?, define_relay_types?) do
def type_definitions(api, resources, schema, env, first?, define_relay_types?, relay_ids?) do
resource_types =
resources
|> Enum.reject(&Ash.Resource.Info.embedded?/1)
|> Enum.flat_map(fn resource ->
if AshGraphql.Resource in Spark.extensions(resource) &&
AshGraphql.Resource.Info.type(resource) do
AshGraphql.Resource.type_definitions(resource, api, schema) ++
AshGraphql.Resource.type_definitions(resource, api, schema, relay_ids?) ++
AshGraphql.Resource.mutation_types(resource, schema)
else
AshGraphql.Resource.no_graphql_types(resource, schema)
Expand All @@ -118,6 +123,7 @@ defmodule AshGraphql.Api do
}
],
identifier: :node,
resolve_type: &AshGraphql.Graphql.Resolver.resolve_node_type/2,
__reference__: AshGraphql.Resource.ref(env),
module: schema
},
Expand Down
91 changes: 82 additions & 9 deletions lib/ash_graphql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ defmodule AshGraphql do
apis: opts[:apis],
api: opts[:api],
action_middleware: opts[:action_middleware] || [],
define_relay_types?: Keyword.get(opts, :define_relay_types?, true)
define_relay_types?: Keyword.get(opts, :define_relay_types?, true),
relay_ids?: Keyword.get(opts, :relay_ids?, false)
],
generated: true do
require Ash.Api.Info
Expand Down Expand Up @@ -136,16 +137,37 @@ defmodule AshGraphql do
api = unquote(api)
action_middleware = unquote(action_middleware)

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 =
api
|> AshGraphql.Api.queries(unquote(resources), action_middleware, __MODULE__)
(relay_queries ++ api_queries)
|> Enum.reduce(blueprint, fn query, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
end)

blueprint_with_mutations =
api
|> AshGraphql.Api.mutations(unquote(resources), action_middleware, __MODULE__)
|> AshGraphql.Api.mutations(
unquote(resources),
action_middleware,
__MODULE__,
unquote(relay_ids?)
)
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
end)
Expand All @@ -155,7 +177,11 @@ defmodule AshGraphql do
apis = unquote(Enum.map(apis, &elem(&1, 0)))

embedded_types =
AshGraphql.get_embedded_types(unquote(ash_resources), unquote(schema))
AshGraphql.get_embedded_types(
unquote(ash_resources),
unquote(schema),
unquote(relay_ids?)
)

global_enums =
AshGraphql.global_enums(unquote(ash_resources), unquote(schema), __ENV__)
Expand All @@ -171,7 +197,8 @@ defmodule AshGraphql do
unquote(schema),
__ENV__,
true,
unquote(define_relay_types?)
unquote(define_relay_types?),
unquote(relay_ids?)
) ++
global_enums ++
global_unions ++
Expand All @@ -185,7 +212,8 @@ defmodule AshGraphql do
unquote(schema),
__ENV__,
false,
false
false,
unquote(relay_ids?)
)
end

Expand Down Expand Up @@ -334,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 Expand Up @@ -491,7 +563,7 @@ defmodule AshGraphql do
end

# sobelow_skip ["DOS.BinToAtom"]
def get_embedded_types(all_resources, schema) do
def get_embedded_types(all_resources, schema, relay_ids?) do
all_resources
|> Enum.flat_map(fn resource ->
resource
Expand Down Expand Up @@ -566,7 +638,8 @@ defmodule AshGraphql do
AshGraphql.Resource.type_definition(
embedded_type,
Module.concat(embedded_type, ShadowApi),
schema
schema,
relay_ids?
),
AshGraphql.Resource.embedded_type_input(
source_resource,
Expand Down
12 changes: 12 additions & 0 deletions lib/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,15 @@ defimpl AshGraphql.Error, for: Ash.Error.Forbidden.ForbiddenField do
}
end
end

defimpl AshGraphql.Error, for: Ash.Error.Invalid.InvalidPrimaryKey do
def to_error(error) do
%{
message: "invalid primary key provided",
short_message: "invalid primary key provided",
fields: [],
vars: Map.new(error.vars),
code: Ash.ErrorKind.code(error)
}
end
end
Loading

0 comments on commit 365b3ae

Please sign in to comment.