-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement overridable global ID translator #93
Changes from 9 commits
785352b
19cedce
b7d9888
137fd6f
ab5421a
0cf86f2
f1e08e4
dc7d3f5
7ee9b4e
b642193
d155cf8
9f0bef7
d7a0a93
1169387
ab0ca11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,9 +83,9 @@ defmodule Absinthe.Relay.Node do | |
|
||
This will create an object type, `:person`, as you might expect. An `:id` | ||
field is created for you automatically, and this field generates a global ID; | ||
a Base64 string that's built using the object type name and the raw, internal | ||
identifier. All of this is handled for you automatically by prefixing your | ||
object type definition with `"node "`. | ||
an opaque string that's built using a global ID translator (by default a | ||
Base64 implementation). All of this is handled for you automatically by | ||
prefixing your object type definition with `"node "`. | ||
|
||
The raw, internal value is retrieved using `default_id_fetcher/2` which just | ||
pattern matches an `:id` field from the resolved object. If you need to | ||
|
@@ -101,6 +101,9 @@ defmodule Absinthe.Relay.Node do | |
end | ||
``` | ||
|
||
For instructions on how to change the underlying method of decoding/encoding | ||
a global ID, see `Absinthe.Relay.Node.IDTranslator`. | ||
|
||
## Macros | ||
|
||
For more details on node-related macros, see | ||
|
@@ -110,6 +113,8 @@ defmodule Absinthe.Relay.Node do | |
|
||
require Logger | ||
|
||
@type global_id_t :: binary | ||
|
||
# Middleware to handle a global id | ||
# parses the global ID before invoking it | ||
@doc false | ||
|
@@ -128,6 +133,9 @@ defmodule Absinthe.Relay.Node do | |
@doc """ | ||
Parse a global ID, given a schema. | ||
|
||
To change the underlying method of decoding a global ID, | ||
see `Absinthe.Relay.Node.IDTranslator`. | ||
|
||
## Examples | ||
|
||
For `nil`, pass-through: | ||
|
@@ -165,22 +173,21 @@ defmodule Absinthe.Relay.Node do | |
{:error, "Type `Item' is not a valid node type"} | ||
``` | ||
""" | ||
@spec from_global_id(nil, atom) :: {:ok, nil} | ||
@spec from_global_id(binary, atom) :: {:ok, %{type: atom, id: binary}} | {:error, binary} | ||
@spec from_global_id(nil, Absinthe.Schema.t) :: {:ok, nil} | ||
@spec from_global_id(global_id_t, Absinthe.Schema.t) :: {:ok, %{type: atom, id: binary}} | {:error, binary} | ||
def from_global_id(nil, _schema) do | ||
{:ok, nil} | ||
end | ||
def from_global_id(global_id, schema) do | ||
case Base.decode64(global_id) do | ||
{:ok, decoded} -> | ||
String.split(decoded, ":", parts: 2) | ||
|> do_from_global_id(decoded, schema) | ||
:error -> | ||
{:error, "Could not decode ID value `#{global_id}'"} | ||
case translate_global_id(schema, :from_global_id, [global_id]) do | ||
{:ok, type_name, id} -> | ||
do_from_global_id({type_name, id}, schema) | ||
{:error, err} -> | ||
{:error, err} | ||
end | ||
end | ||
|
||
defp do_from_global_id([type_name, id], _, schema) when byte_size(id) > 0 and byte_size(type_name) > 0 do | ||
defp do_from_global_id({type_name, id}, schema) do | ||
case schema.__absinthe_type__(type_name) do | ||
nil -> | ||
{:error, "Unknown type `#{type_name}'"} | ||
|
@@ -192,12 +199,25 @@ defmodule Absinthe.Relay.Node do | |
end | ||
end | ||
end | ||
defp do_from_global_id(_, decoded, _schema) do | ||
{:error, "Could not extract value from decoded ID `#{inspect decoded}'"} | ||
|
||
@doc """ | ||
Similar to `from_global_id/2` but raises `RuntimeError` if fails to decode global ID. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bruce Not sure if |
||
""" | ||
@spec from_global_id!(global_id_t | nil, Absinthe.Schema.t) :: %{type: atom, id: binary} | nil | ||
def from_global_id!(global_id, schema) do | ||
case from_global_id(global_id, schema) do | ||
{:ok, result} -> | ||
result | ||
{:error, err} -> | ||
raise RuntimeError, message: err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make a dedicated, specific error type for this in case anyone wants to catch it. |
||
end | ||
end | ||
|
||
@doc """ | ||
Generate a global ID given a node type name and an internal (non-global) ID | ||
Generate a global ID given a node type name and an internal (non-global) ID given a schema. | ||
|
||
To change the underlying method of encoding a global ID, | ||
see `Absinthe.Relay.Node.IDTranslator`. | ||
|
||
## Examples | ||
|
||
|
@@ -210,19 +230,57 @@ defmodule Absinthe.Relay.Node do | |
"No source non-global ID value given" | ||
``` | ||
""" | ||
@spec to_global_id(atom | binary, integer | binary | nil) :: binary | nil | ||
def to_global_id(_node_type, nil) do | ||
nil | ||
@spec to_global_id(atom | binary, integer | binary | nil, Absinthe.Schema.t | nil) :: {:ok, global_id_t | nil} | {:error, binary} | ||
def to_global_id(node_type, source_id, schema \\ nil) | ||
def to_global_id(_node_type, nil, _schema) do | ||
{:ok, nil} | ||
end | ||
def to_global_id(node_type, source_id) when is_binary(node_type) do | ||
"#{node_type}:#{source_id}" |> Base.encode64 | ||
def to_global_id(node_type, source_id, schema) when is_binary(node_type) do | ||
translate_global_id(schema, :to_global_id, [node_type, source_id]) | ||
end | ||
def to_global_id(node_type, source_id, schema) when is_atom(node_type) do | ||
def to_global_id(node_type, source_id, schema) when is_atom(node_type) and not is_nil(schema) do | ||
case Absinthe.Schema.lookup_type(schema, node_type) do | ||
nil -> | ||
nil | ||
{:ok, nil} | ||
type -> | ||
to_global_id(type.name, source_id) | ||
to_global_id(type.name, source_id, schema) | ||
end | ||
end | ||
|
||
@doc """ | ||
Similar to `to_global_id/3` but raises `RuntimeError` if fails to encode global ID. | ||
""" | ||
@spec to_global_id!(atom | binary, integer | binary | nil, Absinthe.Schema.t | nil) :: global_id_t | nil | ||
def to_global_id!(node_type, source_id, schema \\ nil) do | ||
case to_global_id(node_type, source_id, schema) do | ||
{:ok, global_id} -> | ||
global_id | ||
{:error, err} -> | ||
raise RuntimeError, message: err | ||
end | ||
end | ||
|
||
defp translate_global_id(nil, direction, args) do | ||
apply(Absinthe.Relay.Node.IDTranslator.Default, direction, args ++ [nil]) | ||
end | ||
defp translate_global_id(schema, direction, args) do | ||
(global_id_translator(:env, schema) || global_id_translator(:schema, schema) || Absinthe.Relay.Node.IDTranslator.Default) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure that the environment setting should be the fallback for the explicit schema ( |
||
|> apply(direction, args ++ [schema]) | ||
end | ||
|
||
@non_relay_schema_error "Non relay schema provided" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Capitalize "Relay" |
||
|
||
defp global_id_translator(:env, schema) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be nice to have this just be With this in place, |
||
Absinthe.Relay | ||
|> Application.get_env(schema, []) | ||
|> Keyword.get(:global_id_translator, nil) | ||
end | ||
defp global_id_translator(:schema, schema) do | ||
case Keyword.get(schema.__info__(:functions), :__absinthe_relay_global_id_translator__) do | ||
0 -> | ||
apply(schema, :__absinthe_relay_global_id_translator__, []) | ||
nil -> | ||
raise ArgumentError, message: @non_relay_schema_error | ||
end | ||
end | ||
|
||
|
@@ -241,7 +299,7 @@ defmodule Absinthe.Relay.Node do | |
nil -> | ||
report_fetch_id_error(type.name, info.source) | ||
internal_id -> | ||
{:ok, to_global_id(type.name, internal_id)} | ||
to_global_id(type.name, internal_id, info.schema) | ||
end | ||
end | ||
end | ||
|
@@ -251,7 +309,7 @@ defmodule Absinthe.Relay.Node do | |
nil -> | ||
report_fetch_id_error(type_name, info.source) | ||
internal_id -> | ||
{:ok, to_global_id(type_name, internal_id)} | ||
to_global_id(type_name, internal_id, info.schema) | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
defmodule Absinthe.Relay.Node.IDTranslator do | ||
@moduledoc """ | ||
An ID translator handles encoding and decoding a global ID | ||
used in a relay node. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Capitalize "Relay" |
||
|
||
This module provides the behaviour for implementing an ID Translator. | ||
Example use cases of this module would be a translator that encypts the | ||
global ID or perhaps use a different base encoding. | ||
|
||
## Example | ||
|
||
A basic example that encodes the global ID by joining the `type_name` and | ||
`source_id` with `":"`. | ||
|
||
``` | ||
defmodule MyApp.Absinthe.IDTranslator do | ||
@behaviour Absinthe.Relay.Node.IDTranslator | ||
|
||
def to_global_id(type_name, source_id, _schema) do | ||
{:ok, "\#{type_name}:\#{source_id}"} | ||
end | ||
|
||
def from_global_id(global_id, _schema) do | ||
case String.split(global_id, ":", parts: 2) do | ||
[type_name, source_id] -> | ||
{:ok, type_name, source_id} | ||
_ -> | ||
{:error, "Could not extract value from ID `\#{inspect global_id}`"} | ||
end | ||
end | ||
end | ||
``` | ||
""" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Callback docs? |
||
@callback to_global_id(binary, binary | integer, Absinthe.Schema.t) :: {:ok, Absinthe.Relay.Node.global_id_t} | {:error, binary} | ||
@callback from_global_id(Absinthe.Relay.Node.global_id_t, Absinthe.Schema.t | nil) :: {:ok, binary, binary} | {:error, binary} | ||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
defmodule Absinthe.Relay.Node.IDTranslator.Default do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's be more explicit with this and call it |
||
@behaviour Absinthe.Relay.Node.IDTranslator | ||
|
||
@moduledoc """ | ||
A basic implementation of `Absinthe.Relay.Node.IDTranslator` using Base64 encoding. | ||
""" | ||
|
||
def to_global_id(type_name, source_id, _schema) do | ||
{:ok, Base.encode64("#{type_name}:#{source_id}")} | ||
end | ||
|
||
def from_global_id(global_id, _schema) do | ||
case Base.decode64(global_id) do | ||
{:ok, decoded} -> | ||
case String.split(decoded, ":", parts: 2) do | ||
[type_name, source_id] when byte_size(type_name) > 0 and byte_size(source_id) > 0 -> | ||
{:ok, type_name, source_id} | ||
_ -> | ||
{:error, "Could not extract value from decoded ID `#{inspect decoded}`"} | ||
end | ||
:error -> | ||
{:error, "Could not decode ID value `#{global_id}'"} | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,10 +5,26 @@ defmodule Absinthe.Relay.Schema do | |
See `Absinthe.Relay`. | ||
""" | ||
|
||
defmacro __using__(opts) do | ||
defmacro __using__ do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed.
Reference: https://hexdocs.pm/elixir/Kernel.html#use/2 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Learn something new every day, never knew this. |
||
do_using([], []) | ||
end | ||
defmacro __using__(flavor) when is_atom(flavor) do | ||
do_using(flavor, []) | ||
end | ||
defmacro __using__(opts) when is_list(opts) do | ||
opts | ||
|> Keyword.get(:flavor, []) | ||
|> do_using(opts) | ||
end | ||
|
||
defp do_using(flavor, opts) do | ||
quote do | ||
use Absinthe.Relay.Schema.Notation, unquote(opts) | ||
use Absinthe.Relay.Schema.Notation, unquote(flavor) | ||
import_types Absinthe.Relay.Connection.Types | ||
|
||
def __absinthe_relay_global_id_translator__ do | ||
Keyword.get(unquote(opts), :global_id_translator, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Third argument not necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this is an old habit I have in being a bit too explicit |
||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,9 +15,29 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
defstruct [:id, :name] | ||
end | ||
|
||
defmodule CustomIDTranslator do | ||
@behaviour Absinthe.Relay.Node.IDTranslator | ||
|
||
def to_global_id(type_name, source_id, _schema) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we set |
||
{:ok, "#{type_name}:#{source_id}"} | ||
end | ||
|
||
def from_global_id(global_id, _schema) do | ||
case String.split(global_id, ":", parts: 2) do | ||
[type_name, source_id] -> | ||
{:ok, type_name, source_id} | ||
_ -> | ||
{:error, "Could not extract value from ID `#{inspect global_id}`"} | ||
end | ||
end | ||
end | ||
|
||
defmodule Schema do | ||
use Absinthe.Schema | ||
use Absinthe.Relay.Schema, :classic | ||
use Absinthe.Relay.Schema, [ | ||
flavor: :classic, | ||
global_id_translator: CustomIDTranslator, | ||
] | ||
|
||
alias Absinthe.Relay.Node.ParseIDsTest.Foo | ||
alias Absinthe.Relay.Node.ParseIDsTest.Parent | ||
|
@@ -189,8 +209,8 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
|
||
end | ||
|
||
@foo1_id Base.encode64("Foo:1") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should have test cases using the Base64 translator as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And something to verify it's set as the default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, though I assume There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once #97 is fixed, I can refactor these tests a bit further and possibly pull the |
||
@foo2_id Base.encode64("Foo:2") | ||
@foo1_id "Foo:1" | ||
@foo2_id "Foo:2" | ||
|
||
describe "parses one id" do | ||
|
||
|
@@ -293,9 +313,9 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
|
||
describe "parsing nested ids" do | ||
test "works with non-null values" do | ||
encoded_parent_id = Base.encode64("Parent:1") | ||
encoded_child1_id = Base.encode64("Child:1") | ||
encoded_child2_id = Base.encode64("Child:1") | ||
encoded_parent_id = "Parent:1" | ||
encoded_child1_id = "Child:1" | ||
encoded_child2_id = "Child:1" | ||
result = | ||
""" | ||
mutation Foobar { | ||
|
@@ -329,8 +349,8 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
assert {:ok, %{data: %{"updateParent" => expected_parent_data}}} == result | ||
end | ||
test "works with null leaf values" do | ||
encoded_parent_id = Base.encode64("Parent:1") | ||
encoded_child1_id = Base.encode64("Child:1") | ||
encoded_parent_id = "Parent:1" | ||
encoded_child1_id = "Child:1" | ||
result = | ||
""" | ||
mutation Foobar { | ||
|
@@ -365,8 +385,8 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
end | ||
|
||
test "parses incorrect nested ids" do | ||
encoded_parent_id = Base.encode64("Parent:1") | ||
incorrect_id = Node.to_global_id(:other_foo, 1, Schema) | ||
encoded_parent_id = Node.to_global_id!("Parent", 1, Schema) | ||
incorrect_id = Node.to_global_id!(:other_foo, 1, Schema) | ||
mutation = | ||
""" | ||
mutation Foobar { | ||
|
@@ -398,7 +418,7 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
result = | ||
""" | ||
{ | ||
foo(fooId: "#{Node.to_global_id(:other_foo, 1, Schema)}") { | ||
foo(fooId: "#{Node.to_global_id!(:other_foo, 1, Schema)}") { | ||
id | ||
name | ||
} | ||
|
@@ -416,9 +436,9 @@ defmodule Absinthe.Relay.Node.ParseIDsTest do | |
end | ||
|
||
test "parses nested ids with local middleware" do | ||
encoded_parent_id = Base.encode64("Parent:1") | ||
encoded_child1_id = Base.encode64("Child:1") | ||
encoded_child2_id = Base.encode64("Child:1") | ||
encoded_parent_id = "Parent:1" | ||
encoded_child1_id = "Child:1" | ||
encoded_child2_id = "Child:1" | ||
result = | ||
""" | ||
mutation FoobarLocal { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this
@type global_id :: binary
(we're moving away from the_t
suffix in our types to better match wider community conventions).