diff --git a/.formatter.exs b/.formatter.exs
index dfbae687..4e192992 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -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,
diff --git a/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd b/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd
index af262ca7..7c2c4f2a 100644
--- a/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd
+++ b/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd
@@ -639,6 +639,29 @@ get :get_post, :read
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+
+ |
+
+
@@ -856,6 +879,29 @@ read_one :current_user, :current_user
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+
+ |
+
+
@@ -1078,6 +1124,29 @@ list :list_posts_paginated, :read, relay?: true
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+
+ |
+
+
@@ -1161,7 +1230,43 @@ action :check_status, :check_status
+### Options
+
+
+
+
+ Name |
+ Type |
+ Default |
+ Docs |
+
+
+
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ |
+
+
+
+
@@ -1341,6 +1446,29 @@ create :create_post, :create
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+
+ |
+
+
@@ -1477,6 +1605,29 @@ update :update_post, :update
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+
+ |
+
+
@@ -1613,6 +1764,29 @@ destroy :destroy_post, :destroy
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+
+ |
+
+
@@ -1696,7 +1870,43 @@ action :check_status, :check_status
+### Options
+
+
+
+
+ Name |
+ Type |
+ Default |
+ Docs |
+
+
+
+
+
+
+
+ relay_id_translations
+
+
+
+ |
+
+ Keyword.t
+ |
+
+ []
+ |
+
+ A keyword list indicating which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ |
+
+
+
+
diff --git a/lib/graphql/id_translator.ex b/lib/graphql/id_translator.ex
new file mode 100644
index 00000000..9bda4c5a
--- /dev/null
+++ b/lib/graphql/id_translator.ex
@@ -0,0 +1,43 @@
+defmodule AshGraphql.Graphql.IdTranslator do
+ 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)
+
+ _ ->
+ args
+ end
+ end
+
+ defp process({field, type}, args) when is_atom(type) do
+ case Map.get(args, field) do
+ id when is_binary(id) ->
+ {:ok, %{type: ^type, id: decoded_id}} = AshGraphql.Resource.decode_relay_id(id)
+ Map.put(args, field, decoded_id)
+
+ [id | _] = ids when is_binary(id) ->
+ decoded_ids =
+ Enum.map(ids, fn id ->
+ {:ok, %{type: ^type, id: decoded_id}} = AshGraphql.Resource.decode_relay_id(id)
+ decoded_id
+ end)
+
+ Map.put(args, field, decoded_ids)
+
+ _ ->
+ args
+ end
+ end
+end
diff --git a/lib/resource/mutation.ex b/lib/resource/mutation.ex
index d8503ca6..75d6a181 100644
--- a/lib/resource/mutation.ex
+++ b/lib/resource/mutation.ex
@@ -8,7 +8,8 @@ defmodule AshGraphql.Resource.Mutation do
:read_action,
:upsert?,
:upsert_identity,
- :modify_resolution
+ :modify_resolution,
+ :relay_id_translations
]
@create_schema [
@@ -37,6 +38,15 @@ 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 which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+ The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ """,
+ default: []
]
]
@@ -61,6 +71,15 @@ 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 which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+ The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ """,
+ default: []
]
]
@@ -85,6 +104,15 @@ 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 which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+ The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ """,
+ default: []
]
]
diff --git a/lib/resource/query.ex b/lib/resource/query.ex
index 9a597da6..ff18ef8a 100644
--- a/lib/resource/query.ex
+++ b/lib/resource/query.ex
@@ -7,6 +7,7 @@ defmodule AshGraphql.Resource.Query do
:identity,
:allow_nil?,
:modify_resolution,
+ :relay_id_translations,
as_mutation?: false,
metadata_names: [],
metadata_types: [],
@@ -52,6 +53,15 @@ 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 which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+ The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ """,
+ default: []
]
]
diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex
index 10f6b65a..b31ec78a 100644
--- a/lib/resource/resource.ex
+++ b/lib/resource/resource.ex
@@ -57,12 +57,21 @@ 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 which arguments or attributes will be automatically translated from a global Relay ID to a normal ID.
+
+ The keyword list can be arbitrarily nested to match the argument structure. The leaves of the keyword list must have the attribute or argument name as key and its GraphQL type as value.
+ """,
+ 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{
@@ -499,6 +508,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}}
],
@@ -520,6 +530,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?}}
],
@@ -569,6 +580,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}}
],
@@ -593,6 +605,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?}}
],
@@ -636,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?}}
],
@@ -687,6 +701,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?}}
],
@@ -862,6 +877,20 @@ defmodule AshGraphql.Resource do
end)
end
+ defp id_translation_middleware(relay_id_translations, true) do
+ # TODO: compile time check that all keys in relay_id_translations actually map
+ # to an argument or attribute in the query/mutation
+ # Is it possible to also check that the value is an existing GraphQL type?
+ # Probably not since we're in the process of constructing the schema
+ [
+ {{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 =
diff --git a/test/relay_ids_test.exs b/test/relay_ids_test.exs
index 757fe14c..38716679 100644
--- a/test/relay_ids_test.exs
+++ b/test/relay_ids_test.exs
@@ -257,4 +257,168 @@ defmodule AshGraphql.RelayIdsTest do
|> AshGraphql.Resource.decode_relay_id()
end
end
+
+ describe "relay ID translation" do
+ test "works with create mutations" do
+ author_id =
+ User
+ |> Ash.Changeset.for_create(:create, %{name: "Fred"})
+ |> Api.create!()
+ |> AshGraphql.Resource.encode_relay_id()
+
+ resp =
+ """
+ mutation SimpleCreatePost($input: SimpleCreatePostInput) {
+ simpleCreatePost(input: $input) {
+ result {
+ text
+ author {
+ id
+ }
+ }
+ errors {
+ message
+ }
+ }
+ }
+ """
+ |> Absinthe.run(Schema,
+ variables: %{
+ "input" => %{
+ "text" => "foo",
+ "author_id" => author_id
+ }
+ }
+ )
+
+ assert {:ok, result} = resp
+
+ refute Map.has_key?(result, :errors)
+
+ assert %{
+ data: %{
+ "simpleCreatePost" => %{
+ "result" => %{
+ "text" => "foo",
+ "author" => %{
+ "id" => ^author_id
+ }
+ }
+ }
+ }
+ } = result
+ end
+
+ test "works in update mutations" do
+ author_id =
+ User
+ |> Ash.Changeset.for_create(:create, %{name: "Fred"})
+ |> Api.create!()
+ |> AshGraphql.Resource.encode_relay_id()
+
+ post_id =
+ Post
+ |> Ash.Changeset.for_create(:create, %{text: "foo"})
+ |> Api.create!()
+ |> AshGraphql.Resource.encode_relay_id()
+
+ resp =
+ """
+ mutation AssignAuthor($id: ID!, $input: AssignAuthorInput) {
+ assignAuthor(id: $id, input: $input) {
+ result {
+ text
+ author {
+ id
+ }
+ }
+ errors {
+ message
+ }
+ }
+ }
+ """
+ |> Absinthe.run(Schema,
+ variables: %{
+ "id" => post_id,
+ "input" => %{
+ "author_id" => author_id
+ }
+ }
+ )
+
+ assert {:ok, result} = resp
+
+ refute Map.has_key?(result, :errors)
+
+ assert %{
+ data: %{
+ "assignAuthor" => %{
+ "result" => %{
+ "author" => %{
+ "id" => ^author_id
+ }
+ }
+ }
+ }
+ } = result
+ end
+
+ test "works with lists" do
+ author_id =
+ User
+ |> Ash.Changeset.for_create(:create, %{name: "Fred"})
+ |> Api.create!()
+ |> AshGraphql.Resource.encode_relay_id()
+
+ post_ids =
+ Enum.map(1..5, fn i ->
+ Post
+ |> Ash.Changeset.for_create(:create, %{text: "foo #{i}"})
+ |> Api.create!()
+ |> AshGraphql.Resource.encode_relay_id()
+ end)
+
+ resp =
+ """
+ mutation AssignPosts($id: ID!, $input: AssignPostsInput) {
+ assignPosts(id: $id, input: $input) {
+ result {
+ posts {
+ id
+ }
+ }
+ errors {
+ message
+ }
+ }
+ }
+ """
+ |> Absinthe.run(Schema,
+ variables: %{
+ "id" => author_id,
+ "input" => %{
+ "post_ids" => post_ids
+ }
+ }
+ )
+
+ assert {:ok, result} = resp
+
+ refute Map.has_key?(result, :errors)
+
+ assert %{
+ data: %{
+ "assignPosts" => %{
+ "result" => %{
+ "posts" => posts
+ }
+ }
+ }
+ } = result
+
+ assert length(posts) == 5
+ Enum.each(posts, fn post -> assert post["id"] in post_ids end)
+ end
+ end
end
diff --git a/test/support/relay_ids/resources/post.ex b/test/support/relay_ids/resources/post.ex
index fb950e19..e623caf3 100644
--- a/test/support/relay_ids/resources/post.ex
+++ b/test/support/relay_ids/resources/post.ex
@@ -16,8 +16,9 @@ defmodule AshGraphql.Test.RelayIds.Post do
end
mutations do
- create :simple_create_post, :create
+ create :simple_create_post, :create, relay_id_translations: [input: [author_id: :user]]
update :update_post, :update
+ update :assign_author, :assign_author, relay_id_translations: [input: [author_id: :user]]
destroy :delete_post, :destroy
end
end
@@ -31,6 +32,12 @@ defmodule AshGraphql.Test.RelayIds.Post do
change(set_attribute(:author_id, arg(:author_id)))
end
+
+ update :assign_author do
+ argument(:author_id, :uuid)
+
+ change(set_attribute(:author_id, arg(:author_id)))
+ end
end
attributes do
diff --git a/test/support/relay_ids/resources/user.ex b/test/support/relay_ids/resources/user.ex
index 577ff111..5323f41c 100644
--- a/test/support/relay_ids/resources/user.ex
+++ b/test/support/relay_ids/resources/user.ex
@@ -14,11 +14,18 @@ defmodule AshGraphql.Test.RelayIds.User do
mutations do
create :create_user, :create
+ update :assign_posts, :assign_posts, relay_id_translations: [input: [post_ids: :post]]
end
end
actions do
defaults([:create, :update, :destroy, :read])
+
+ update :assign_posts do
+ argument(:post_ids, {:array, :uuid})
+
+ change(manage_relationship(:post_ids, :posts, value_is_key: :id, type: :append_and_remove))
+ end
end
attributes do