ash_graphql/test/relay_ids_test.exs
Riccardo Binetti 66d2f44443
feat: add Relay ID translation in mutation and queries (#109)
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 <zachary.s.daniel@gmail.com>
2024-02-06 09:46:09 -05:00

534 lines
12 KiB
Elixir

defmodule AshGraphql.RelayIdsTest do
use ExUnit.Case, async: false
alias AshGraphql.Test.RelayIds.{Api, Post, ResourceWithNoPrimaryKeyGet, Schema, User}
setup do
on_exit(fn ->
AshGraphql.TestHelpers.stop_ets()
end)
end
describe "relay global ID" do
test "can be used in get queries and is exposed correctly in relationships" 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)
resp =
"""
query GetPost($id: ID!) {
getPost(id: $id) {
text
author {
id
name
}
}
}
"""
|> Absinthe.run(Schema,
variables: %{
"id" => post_relay_id
}
)
assert {:ok, result} = resp
refute Map.has_key?(result, :errors)
assert %{
data: %{
"getPost" => %{
"text" => "foo",
"author" => %{"id" => ^user_relay_id, "name" => "fred"}
}
}
} = result
end
test "returns error on invalid ID" do
resp =
"""
query GetPost($id: ID!) {
getPost(id: $id) {
text
}
}
"""
|> Absinthe.run(Schema,
variables: %{
"id" => "invalid"
}
)
assert {:ok, result} = resp
assert [%{code: "invalid_primary_key"}] = result[:errors]
end
test "returns error on ID for wrong resource" do
user =
User
|> Ash.Changeset.for_create(:create, %{name: "fred"})
|> Api.create!()
user_relay_id = AshGraphql.Resource.encode_relay_id(user)
resp =
"""
query GetPost($id: ID!) {
getPost(id: $id) {
text
}
}
"""
|> Absinthe.run(Schema,
variables: %{
"id" => user_relay_id
}
)
assert {:ok, result} = resp
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
describe "relay ID decoding" do
test "round trips" do
user =
User
|> Ash.Changeset.for_create(:create, %{name: "Fred"})
|> Api.create!()
user_id = user.id
user_type = AshGraphql.Resource.Info.type(User)
user_relay_id = AshGraphql.Resource.encode_relay_id(user)
assert {:ok, %{type: ^user_type, id: ^user_id}} =
AshGraphql.Resource.decode_relay_id(user_relay_id)
end
test "fails for invalid ids" do
assert {:error, %Ash.Error.Invalid.InvalidPrimaryKey{}} =
AshGraphql.Resource.decode_relay_id("notbase64")
assert {:error, %Ash.Error.Invalid.InvalidPrimaryKey{}} =
"non-existing-type:1234"
|> Base.encode64()
|> AshGraphql.Resource.decode_relay_id()
assert {:error, %Ash.Error.Invalid.InvalidPrimaryKey{}} =
"user"
|> Base.encode64()
|> 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
test "rejects invalid IDs" 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)
post_ids = ["invalid_id" | post_ids]
resp =
"""
mutation AssignPosts($id: ID!, $input: AssignPostsInput) {
assignPosts(id: $id, input: $input) {
result {
posts {
id
}
}
errors {
fields
message
}
}
}
"""
|> Absinthe.run(Schema,
variables: %{
"id" => author_id,
"input" => %{
"post_ids" => post_ids
}
}
)
assert {:ok, result} = resp
assert %{
data: %{
"assignPosts" => %{
"result" => nil,
"errors" => [
%{
"fields" => ["post_ids"],
"message" => "is invalid"
}
]
}
}
} = result
end
test "rejects IDs for another type" do
author_id =
User
|> Ash.Changeset.for_create(:create, %{name: "Fred"})
|> Api.create!()
|> AshGraphql.Resource.encode_relay_id()
post_ids = [author_id]
resp =
"""
mutation AssignPosts($id: ID!, $input: AssignPostsInput) {
assignPosts(id: $id, input: $input) {
result {
posts {
id
}
}
errors {
fields
message
}
}
}
"""
|> Absinthe.run(Schema,
variables: %{
"id" => author_id,
"input" => %{
"post_ids" => post_ids
}
}
)
assert {:ok, result} = resp
assert %{
data: %{
"assignPosts" => %{
"result" => nil,
"errors" => [
%{
"fields" => ["post_ids"],
"message" => "is invalid"
}
]
}
}
} = result
end
end
end