Skip to content

Commit

Permalink
feat: add Relay ID translation in mutation and queries (#109)
Browse files Browse the repository at this point in the history
Adds a new option for queries and mutations that defines which arguments or
attributes will use a global Relay ID and their type. This allows automatically
decoding them before hitting their action.

This paves the way to automatic translation derived from the arguments, which
will be implemented subsequently.

---------

Co-authored-by: Zach Daniel <[email protected]>
  • Loading branch information
rbino and zachdaniel authored Feb 6, 2024
1 parent d00ae4e commit 66d2f44
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 3 deletions.
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ spark_locals_without_parens = [
read_one: 3,
relationships: 1,
relay?: 1,
relay_id_translations: 1,
root_level_errors?: 1,
show_metadata: 1,
show_raised_errors?: 1,
Expand Down
14 changes: 14 additions & 0 deletions documentation/dsls/DSL:-AshGraphql.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ get :get_post, :read
| [`metadata_types`](#graphql-queries-get-metadata_types){: #graphql-queries-get-metadata_types } | `keyword` | `[]` | Type overrides for metadata fields on the read action. |
| [`show_metadata`](#graphql-queries-get-show_metadata){: #graphql-queries-get-show_metadata } | `list(atom)` | | The metadata attributes to show. Defaults to all. |
| [`as_mutation?`](#graphql-queries-get-as_mutation?){: #graphql-queries-get-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-get-relay_id_translations){: #graphql-queries-get-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -169,6 +170,7 @@ read_one :current_user, :current_user
| [`metadata_types`](#graphql-queries-read_one-metadata_types){: #graphql-queries-read_one-metadata_types } | `keyword` | `[]` | Type overrides for metadata fields on the read action. |
| [`show_metadata`](#graphql-queries-read_one-show_metadata){: #graphql-queries-read_one-show_metadata } | `list(atom)` | | The metadata attributes to show. Defaults to all. |
| [`as_mutation?`](#graphql-queries-read_one-as_mutation?){: #graphql-queries-read_one-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-read_one-relay_id_translations){: #graphql-queries-read_one-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -215,6 +217,7 @@ list :list_posts_paginated, :read, relay?: true
| [`metadata_types`](#graphql-queries-list-metadata_types){: #graphql-queries-list-metadata_types } | `keyword` | `[]` | Type overrides for metadata fields on the read action. |
| [`show_metadata`](#graphql-queries-list-show_metadata){: #graphql-queries-list-show_metadata } | `list(atom)` | | The metadata attributes to show. Defaults to all. |
| [`as_mutation?`](#graphql-queries-list-as_mutation?){: #graphql-queries-list-as_mutation? } | `boolean` | `false` | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-queries-list-relay_id_translations){: #graphql-queries-list-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -247,7 +250,11 @@ action :check_status, :check_status
|------|------|---------|------|
| [`name`](#graphql-queries-action-name){: #graphql-queries-action-name } | `atom` | `:get` | The name to use for the query. |
| [`action`](#graphql-queries-action-action){: #graphql-queries-action-action .spark-required} | `atom` | | The action to use for the query. |
### Options

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`relay_id_translations`](#graphql-queries-action-relay_id_translations){: #graphql-queries-action-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -312,6 +319,7 @@ create :create_post, :create
| [`upsert?`](#graphql-mutations-create-upsert?){: #graphql-mutations-create-upsert? } | `boolean` | `false` | Whether or not to use the `upsert?: true` option when calling `YourApi.create/2`. |
| [`upsert_identity`](#graphql-mutations-create-upsert_identity){: #graphql-mutations-create-upsert_identity } | `atom` | `false` | Which identity to use for the upsert |
| [`modify_resolution`](#graphql-mutations-create-modify_resolution){: #graphql-mutations-create-modify_resolution } | `mfa` | | An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. |
| [`relay_id_translations`](#graphql-mutations-create-relay_id_translations){: #graphql-mutations-create-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -350,6 +358,7 @@ update :update_post, :update
|------|------|---------|------|
| [`identity`](#graphql-mutations-update-identity){: #graphql-mutations-update-identity } | `atom` | | The identity to use to fetch the record to be updated. Use `false` if no identity is required. |
| [`read_action`](#graphql-mutations-update-read_action){: #graphql-mutations-update-read_action } | `atom` | | The read action to use to fetch the record to be updated. Defaults to the primary read action. |
| [`relay_id_translations`](#graphql-mutations-update-relay_id_translations){: #graphql-mutations-update-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -388,6 +397,7 @@ destroy :destroy_post, :destroy
|------|------|---------|------|
| [`read_action`](#graphql-mutations-destroy-read_action){: #graphql-mutations-destroy-read_action } | `atom` | | The read action to use to fetch the record to be destroyed. Defaults to the primary read action. |
| [`identity`](#graphql-mutations-destroy-identity){: #graphql-mutations-destroy-identity } | `atom` | | The identity to use to fetch the record to be destroyed. Use `false` if no identity is required. |
| [`relay_id_translations`](#graphql-mutations-destroy-relay_id_translations){: #graphql-mutations-destroy-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down Expand Up @@ -420,7 +430,11 @@ action :check_status, :check_status
|------|------|---------|------|
| [`name`](#graphql-mutations-action-name){: #graphql-mutations-action-name } | `atom` | `:get` | The name to use for the query. |
| [`action`](#graphql-mutations-action-action){: #graphql-mutations-action-action .spark-required} | `atom` | | The action to use for the query. |
### Options

| Name | Type | Default | Docs |
|------|------|---------|------|
| [`relay_id_translations`](#graphql-mutations-action-relay_id_translations){: #graphql-mutations-action-relay_id_translations } | `keyword` | `[]` | A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more. |



Expand Down
31 changes: 31 additions & 0 deletions documentation/topics/relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,34 @@ use AshGraphql, relay_ids?: true
```

This allows refetching a node using the `node` query and passing its global ID.

### Translating Relay Global IDs passed as arguments

When `relay_ids?: true` is passed, users of the API will have access only to the global IDs, so they
will also need to use them when an ID is required as argument. You actions, though, internally use the
normal IDs defined by the data layer.

To handle the translation between the two ID domains, you can use the `relay_id_translations`
option. With this, you can define a list of arguments that will be translated from Relay global IDs
to internal IDs.

For example, if you have a `Post` resource with an action to create a post associated with an
author:

```elixir
create :create do
argument :author_id, :uuid

# Do stuff with author_id
end
```

You can add this to the mutation connected to that action:

```elixir
mutations do
create :create_post, :create do
relay_id_translations [input: [author_id: :user]]
end
end
```
67 changes: 67 additions & 0 deletions lib/graphql/id_translator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule AshGraphql.Graphql.IdTranslator do
@moduledoc false

def translate_relay_ids(%{state: :unresolved} = resolution, relay_id_translations) do
arguments =
Enum.reduce(relay_id_translations, resolution.arguments, &process/2)

%{resolution | arguments: arguments}
end

def translate_relay_ids(resolution, _relay_id_translations) do
resolution
end

defp process({field, nested_translations}, args) when is_list(nested_translations) do
case Map.get(args, field) do
subtree when is_map(subtree) ->
new_subtree = Enum.reduce(nested_translations, subtree, &process/2)
Map.put(args, field, new_subtree)

elements when is_list(elements) ->
new_elements =
Enum.map(elements, fn element ->
Enum.reduce(nested_translations, element, &process/2)
end)

Map.put(args, field, new_elements)

_ ->
args
end
end

defp process({field, type}, args) when is_atom(type) do
case Map.get(args, field) do
id when is_binary(id) ->
case AshGraphql.Resource.decode_relay_id(id) do
{:ok, %{type: ^type, id: decoded_id}} ->
Map.put(args, field, decoded_id)

_ ->
# If we fail to decode for the correct type, we just skip translation
# This will be marked as an invalid input down the line
args
end

[id | _] = ids when is_binary(id) ->
decoded_ids =
Enum.map(ids, fn id ->
case AshGraphql.Resource.decode_relay_id(id) do
{:ok, %{type: ^type, id: decoded_id}} ->
decoded_id

_ ->
# If we fail to decode for the correct type, we just skip translation
# This will be marked as an invalid input down the line
id
end
end)

Map.put(args, field, decoded_ids)

_ ->
args
end
end
end
24 changes: 23 additions & 1 deletion lib/resource/mutation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ defmodule AshGraphql.Resource.Mutation do
:read_action,
:upsert?,
:upsert_identity,
:modify_resolution
:modify_resolution,
:relay_id_translations
]

@create_schema [
Expand Down Expand Up @@ -37,6 +38,13 @@ defmodule AshGraphql.Resource.Mutation do
doc: """
An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more.
"""
],
relay_id_translations: [
type: :keyword_list,
doc: """
A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more.
""",
default: []
]
]

Expand All @@ -61,6 +69,13 @@ defmodule AshGraphql.Resource.Mutation do
type: :atom,
doc:
"The read action to use to fetch the record to be updated. Defaults to the primary read action."
],
relay_id_translations: [
type: :keyword_list,
doc: """
A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more.
""",
default: []
]
]

Expand All @@ -85,6 +100,13 @@ defmodule AshGraphql.Resource.Mutation do
doc: """
The identity to use to fetch the record to be destroyed. Use `false` if no identity is required.
"""
],
relay_id_translations: [
type: :keyword_list,
doc: """
A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more.
""",
default: []
]
]

Expand Down
8 changes: 8 additions & 0 deletions lib/resource/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule AshGraphql.Resource.Query do
:identity,
:allow_nil?,
:modify_resolution,
:relay_id_translations,
as_mutation?: false,
metadata_names: [],
metadata_types: [],
Expand Down Expand Up @@ -52,6 +53,13 @@ defmodule AshGraphql.Resource.Query do
doc: """
Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more.
"""
],
relay_id_translations: [
type: :keyword_list,
doc: """
A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more.
""",
default: []
]
]

Expand Down
23 changes: 22 additions & 1 deletion lib/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,19 @@ defmodule AshGraphql.Resource do
type: :atom,
doc: "The action to use for the query.",
required: true
],
relay_id_translations: [
type: :keyword_list,
doc: """
A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more.
""",
default: []
]
]

defmodule Action do
@moduledoc "Represents a configured generic action"
defstruct [:type, :name, :action]
defstruct [:type, :name, :action, :relay_id_translations]
end

@action %Spark.Dsl.Entity{
Expand Down Expand Up @@ -499,6 +506,7 @@ defmodule AshGraphql.Resource do
identifier: name,
middleware:
action_middleware ++
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
[
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, false}}
],
Expand All @@ -520,6 +528,7 @@ defmodule AshGraphql.Resource do
identifier: query.name,
middleware:
action_middleware ++
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
[
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, relay_ids?}}
],
Expand Down Expand Up @@ -569,6 +578,7 @@ defmodule AshGraphql.Resource do
identifier: name,
middleware:
action_middleware ++
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
[
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, true}}
],
Expand All @@ -593,6 +603,7 @@ defmodule AshGraphql.Resource do
identifier: mutation.name,
middleware:
action_middleware ++
id_translation_middleware(mutation.relay_id_translations, relay_ids?) ++
[
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation, relay_ids?}}
],
Expand Down Expand Up @@ -638,6 +649,7 @@ defmodule AshGraphql.Resource do
identifier: mutation.name,
middleware:
action_middleware ++
id_translation_middleware(mutation.relay_id_translations, relay_ids?) ++
[
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation, relay_ids?}}
],
Expand Down Expand Up @@ -691,6 +703,7 @@ defmodule AshGraphql.Resource do
identifier: mutation.name,
middleware:
action_middleware ++
id_translation_middleware(mutation.relay_id_translations, relay_ids?) ++
[
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation, relay_ids?}}
],
Expand Down Expand Up @@ -894,6 +907,14 @@ defmodule AshGraphql.Resource do
end)
end

defp id_translation_middleware(relay_id_translations, true) do
[{{AshGraphql.Graphql.IdTranslator, :translate_relay_ids}, relay_id_translations}]
end

defp id_translation_middleware(_relay_id_translations, _relay_ids?) do
[]
end

# sobelow_skip ["DOS.StringToAtom"]
defp metadata_field(resource, mutation, schema) do
metadata_fields =
Expand Down
Loading

0 comments on commit 66d2f44

Please sign in to comment.