Skip to content
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

Merged
merged 15 commits into from
Apr 12, 2018
97 changes: 71 additions & 26 deletions lib/absinthe/relay/node.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -110,6 +113,8 @@ defmodule Absinthe.Relay.Node do

require Logger

@type global_id :: binary

# Middleware to handle a global id
# parses the global ID before invoking it
@doc false
Expand All @@ -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:
Expand Down Expand Up @@ -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, 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}'"}
Expand All @@ -192,12 +199,12 @@ 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}'"}
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

Expand All @@ -207,31 +214,69 @@ defmodule Absinthe.Relay.Node do
iex> to_global_id(:person, "123", SchemaWithPersonType)
"UGVyc29uOjEyMw=="
iex> to_global_id(:person, nil, SchemaWithPersonType)
"No source non-global ID value given"
nil
```
"""
@spec to_global_id(atom | binary, integer | binary | nil) :: binary | nil
def to_global_id(_node_type, nil) do
# TODO: Return tuples in v1.5
@spec to_global_id(atom | binary, integer | binary | nil, Absinthe.Schema.t | nil) :: global_id | nil
def to_global_id(node_type, source_id, schema \\ nil)
def to_global_id(_node_type, nil, _schema) do
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
case translate_global_id(schema, :to_global_id, [node_type, source_id]) do
{:ok, global_id} ->
global_id
{:error, err} ->
Logger.warn("Failed to translate (#{inspect node_type}, #{inspect source_id}) to global ID with error: #{err}")
nil
end
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
type ->
to_global_id(type.name, source_id)
to_global_id(type.name, source_id, schema)
end
end

defp translate_global_id(schema, direction, args) do
schema
|> global_id_translator
|> apply(direction, args ++ [schema])
end

@non_relay_schema_error "Non Relay schema provided"
@doc false
# Returns an ID Translator from either the schema config, env config.
# or a default Base64 implementation.
def global_id_translator(nil) do
Absinthe.Relay.Node.IDTranslator.Base64
end
def global_id_translator(schema) do
from_schema =
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

from_env =
Absinthe.Relay
|> Application.get_env(schema, [])
|> Keyword.get(:global_id_translator, nil)

from_schema || from_env || Absinthe.Relay.Node.IDTranslator.Base64
end

@missing_internal_id_error "No source non-global ID value could be fetched from the source object"
@doc false
# The resolver for a global ID. If a type identifier instead of a type name
# is used during field configuration, the type name needs to be looked up
# during resolution.
def global_id_resolver(identifier, nil) do
def global_id_resolver(identifier, nil) do
global_id_resolver(identifier, &default_id_fetcher/2)
end
def global_id_resolver(identifier, id_fetcher) when is_atom(identifier) do
Expand All @@ -241,7 +286,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)}
{:ok, to_global_id(type.name, internal_id, info.schema)}
end
end
end
Expand All @@ -251,7 +296,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)}
{:ok, to_global_id(type_name, internal_id, info.schema)}
end
end
end
Expand Down
77 changes: 77 additions & 0 deletions lib/absinthe/relay/node/id_translator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule Absinthe.Relay.Node.IDTranslator do
@moduledoc """
An ID translator handles encoding and decoding a global ID
used in a Relay node.

This module provides the behaviour for implementing an ID Translator.
An example use case of this module would be a translator that encrypts the
global ID.

To use an ID Translator in your schema there are two methods.

#### Inline Config
```
defmodule MyApp.Schema do
use Absinthe.Schema
use Absinthe.Relay.Schema, [
flavor: :modern,
global_id_translator: MyApp.Absinthe.IDTranslator
]

# ...

end
```

#### Mix Config
```
config Absinthe.Relay, MyApp.Schema,
global_id_translator: MyApp.Absinthe.IDTranslator
```

## Example ID Translator

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
```
"""

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callback docs?

@doc """
Converts a node's type name and ID to a globally unique ID.

Returns `{:ok, global_id}` on success.

Returns `{:error, binary}` on failure.
"""
@callback to_global_id(type_name :: binary, source_id :: binary | integer, schema :: Absinthe.Schema.t) ::
{:ok, global_id :: Absinthe.Relay.Node.global_id} | {:error, binary}

@doc """
Converts a globally unique ID to a node's type name and ID.

Returns `{:ok, type_name, source_id}` on success.

Returns `{:error, binary}` on failure.
"""
@callback from_global_id(global_id :: Absinthe.Relay.Node.global_id, schema :: Absinthe.Schema.t | nil) ::
{:ok, type_name :: binary, source_id :: binary} | {:error, binary}

end
27 changes: 27 additions & 0 deletions lib/absinthe/relay/node/id_translator/base64.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule Absinthe.Relay.Node.IDTranslator.Base64 do
@behaviour Absinthe.Relay.Node.IDTranslator

@moduledoc """
A basic implementation of `Absinthe.Relay.Node.IDTranslator` using Base64 encoding.
"""

@impl true
def to_global_id(type_name, source_id, _schema) do
{:ok, Base.encode64("#{type_name}:#{source_id}")}
end

@impl true
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
19 changes: 16 additions & 3 deletions lib/absinthe/relay/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ defmodule Absinthe.Relay.Schema do
See `Absinthe.Relay`.
"""

defmacro __using__(opts) do
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)
end
end
end
end
end
3 changes: 0 additions & 3 deletions lib/absinthe/relay/schema/notation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ defmodule Absinthe.Relay.Schema.Notation do
end

@spec notations(flavor) :: Macro.t



defp notations(flavor) do
mutation_notation = Absinthe.Relay.Mutation.Notation |> flavored(flavor)
quote do
Expand Down
Loading