Packages
absinthe_relay
1.3.1
1.6.0
1.5.2
1.5.1
1.5.0
1.5.0-rc.0
1.5.0-beta.0
1.5.0-alpha.0
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.4.0-rc.1
1.4.0-rc.0
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.3.0-rc.0
1.3.0-beta.1
1.3.0-beta.0
1.2.0
1.2.0-alpha0
1.2.0-alpha.3
1.2.0-alpha.2
1.2.0-alpha.1
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
Relay framework support for Absinthe
Current section
Files
Jump to
Current section
Files
lib/absinthe/relay/node.ex
defmodule Absinthe.Relay.Node do
@moduledoc """
Support for global object identification.
The `node` macro can be used by schema designers to add required
"object identification" support for object types, and to provide a unified
interface for querying them.
More information can be found at:
- https://facebook.github.io/relay/docs/graphql-object-identification.html#content
- https://facebook.github.io/relay/graphql/objectidentification.htm
## Interface
Define a node interface for your schema, providing a type resolver that,
given a resolved object can determine which node object type it belongs to.
```
node interface do
resolve_type fn
%{age: _}, _ ->
:person
%{employee_count: _}, _ ->
:business
_, _ ->
nil
end
end
```
This will create an interface, `:node` that expects one field, `:id`, be
defined -- and that the ID will be a global identifier.
If you use the `node` macro to create your `object` types (see "Object" below),
this can be easily done, layered on top of the standard object type definition
style.
## Field
The node field provides a unified interface to query for an object in the
system using a global ID. The node field should be defined within your schema
`query` and should provide a resolver that, given a map containing the object
type identifier and internal, non-global ID (the incoming global ID will be
parsed into these values for you automatically) can resolve the correct value.
```
query do
# ...
node field do
resolve fn
%{type: :person, id: id}, _ ->
{:ok, Map.get(@people, id)}
%{type: :business, id: id}, _ ->
{:ok, Map.get(@businesses, id)}
end
end
end
```
This creates a field, `:node`, with one argument: `:id`. This is expected to
be a global ID and, once resolved, will result in a value whose type
implements the `:node` interface.
Here's how you easly create object types that can be looked up using this
field:
## Object
To play nicely with the `:node` interface and field, explained above, any
object types need to implement the `:node` interface and generate a global
ID as the value of its `:id` field. Using the `node` macro, you can easily do
this while retaining the usual object type definition style.
```
node object :person do
field :name, :string
field :age, :string
end
```
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 "`.
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
extract/build an internal ID via another method, just provide a function as
an `:id_fetcher` option.
For instance, assuming your raw internal IDs were stored as `:_id`, you could
configure your object like this:
```
node object :thing, id_fetcher: &my_custom_id_fetcher/2 do
field :name, :string
end
```
## Macros
For more details on node-related macros, see
`Absinthe.Relay.Node.Notation`.
"""
# Middleware to handle a global id
# parses the global ID before invoking it
@doc false
def resolve_with_global_id(%{state: :unresolved} = res, _) do
with %{id: global_id} <- res.arguments,
{:ok, result} <- Absinthe.Relay.Node.from_global_id(global_id, res.schema) do
%{res | arguments: result}
else
_ -> res
end
end
def resolve_with_global_id(res) do
res
end
@doc """
Parse a global ID, given a schema.
## Examples
For a valid, existing type in `Schema`:
```
iex> from_global_id("UGVyc29uOjE=", Schema)
{:ok, %{type: :person, id: "1"}}
```
For an invalid global ID value:
```
iex> from_global_id("GHNF", Schema)
{:error, "Could not decode ID value `GHNF'"}
```
For a type that isn't in the schema:
```
iex> from_global_id("Tm9wZToxMjM=", Schema)
{:error, "Unknown type `Nope'"}
```
For a type that is in the schema but isn't a node:
```
iex> from_global_id("Tm9wZToxMjM=", Schema)
{:error, "Type `Item' is not a valid node type"}
```
"""
@spec from_global_id(binary, atom) :: {:ok, %{type: atom, id: binary}} | {:error, binary}
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}'"}
end
end
defp do_from_global_id([type_name, id], _, schema) when byte_size(id) > 0 and byte_size(type_name) > 0 do
case schema.__absinthe_type__(type_name) do
nil ->
{:error, "Unknown type `#{type_name}'"}
%{__reference__: %{identifier: ident}, interfaces: interfaces} ->
if Enum.member?(List.wrap(interfaces), :node) do
{:ok, %{type: ident, id: id}}
else
{:error, "Type `#{type_name}' is not a valid node type"}
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
## Examples
```
iex> to_global_id("Person", "123")
"UGVyc29uOjEyMw=="
iex> to_global_id(:person, "123", SchemaWithPersonType)
"UGVyc29uOjEyMw=="
iex> to_global_id(:person, nil, SchemaWithPersonType)
"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
end
def to_global_id(node_type, source_id) when is_binary(node_type) do
"#{node_type}:#{source_id}" |> Base.encode64
end
def to_global_id(node_type, source_id, schema) when is_atom(node_type) do
case Absinthe.Schema.lookup_type(schema, node_type) do
nil ->
nil
type ->
to_global_id(type.name, source_id)
end
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
global_id_resolver(identifier, &default_id_fetcher/2)
end
def global_id_resolver(identifier, id_fetcher) when is_atom(identifier) do
fn _obj, info ->
type = Absinthe.Schema.lookup_type(info.schema, identifier)
case id_fetcher.(info.source, info) do
nil ->
{:error, @missing_internal_id_error}
internal_id ->
{:ok, to_global_id(type.name, internal_id)}
end
end
end
def global_id_resolver(type_name, id_fetcher) when is_binary(type_name) do
fn _, info ->
case id_fetcher.(info.source, info) do
nil ->
{:error, @missing_internal_id_error}
internal_id ->
{:ok, to_global_id(type_name, internal_id)}
end
end
end
@doc """
The default ID fetcher used to retrieve raw, non-global IDs from values.
* Matches `:id` out of the value.
* If it's `nil`, it returns `nil`
* If it's not nil, it coerces it to a binary using `Kernel.to_string/1`
## Examples
```
iex> default_id_fetcher(%{id: "foo"})
"foo"
iex> default_id_fetcher(%{id: 123})
"123"
iex> default_id_fetcher(%{id: nil})
nil
iex> default_id_fetcher(%{nope: "no_id"})
nil
```
"""
@spec default_id_fetcher(any, Absinthe.Resolution.t) :: nil | binary
def default_id_fetcher(%{id: id}, _info) when is_nil(id), do: nil
def default_id_fetcher(%{id: id}, _info), do: id |> to_string
def default_id_fetcher(_, _), do: nil
end