View Source Absinthe.Relay.Connection (absinthe_relay v1.5.2)
Support for paginated result sets.
Define connection types that provide a standard mechanism for slicing and paginating result sets.
For information about the connection model, see the Relay Cursor Connections Specification at https://facebook.github.io/relay/graphql/connections.htm.
connection
Connection
Given an object type, eg:
object :pet do
field :name, :string
end
You can create a connection type to paginate them by:
connection node_type: :pet
This will automatically define two new types: :pet_connection
and
:pet_edge
.
We define a field that uses these types to paginate associated records
by using connection field
. Here, for instance, we support paginating a
person's pets:
object :person do
field :first_name, :string
connection field :pets, node_type: :pet do
resolve fn
pagination_args, %{source: person} ->
Absinthe.Relay.Connection.from_list(
Enum.map(person.pet_ids, &pet_from_id(&1)),
pagination_args
)
end
end
end
end
The :pets
field is automatically set to return a :pet_connection
type,
and configured to accept the standard pagination arguments after
, before
,
first
, and last
. We create the connection by using
Absinthe.Relay.Connection.from_list/2
, which takes a list and the pagination
arguments passed to the resolver.
It is possible to provide additional pagination arguments to a relay connection:
connection field :pets, node_type: :pet do
arg :custom_arg, :custom
# other args...
resolve fn
pagination_args_and_custom_args, %{source: person} ->
# ... return {:ok, a_connection}
end
end
Note: Absinthe.Relay.Connection.from_list/2
expects that the full list of
records be materialized and provided. If you're using Ecto, you probably want
to use Absinthe.Relay.Connection.from_query/2
instead.
Here's how you might request the names of the first $petCount
pets a person
owns:
query FindPets($personId: ID!, $petCount: Int!) {
person(id: $personId) {
pets(first: $petCount) {
pageInfo {
hasPreviousPage
hasNextPage
}
edges {
node {
name
}
}
}
}
}
edges
here is the list of intermediary edge types (created for you
automatically) that contain a field, node
, that is the same :node_type
you
passed earlier (:pet
).
pageInfo
is a field that contains information about the current
view; the startCursor
, endCursor
, hasPreviousPage
, and
hasNextPage
fields.
pagination-direction
Pagination Direction
By default, connections will support bidirectional pagination, but you can
also restrict the connection to just the :forward
or :backward
direction
using the :paginate
argument:
connection field :pets, node_type: :pet, paginate: :forward do
customizing-types
Customizing Types
If you'd like to add additional fields to the generated connection and edge
types, you can do that by providing a block to the connection
macro, eg,
here we add a field, :twice_edges_count
to the connection type, and another,
:node_name_backwards
, to the edge type:
connection node_type: :pet do
field :twice_edges_count, :integer do
resolve fn
_, %{source: conn} ->
{:ok, length(conn.edges) * 2}
end
end
edge do
field :node_name_backwards, :string do
resolve fn
_, %{source: edge} ->
{:ok, edge.node.name |> String.reverse}
end
end
end
end
Just remember that if you use the block form of connection
, you must call
the edge
macro within the block.
customizing-the-node-itself
Customizing the node itself
It's also possible to customize the way the node
field of the
connection's edge is resolved. This can, for example, be useful if
you're working with a NoSQL database that returns relationships as
lists of IDs. Consider the following example which paginates over
the user's account array, but resolves each one of them
independently.
object :account do
field :id, non_null(:id)
field :name, :string
end
connection node_type :account do
edge do
field :node, :account do
resolve fn %{node: id}, _args, _info ->
Account.find(id)
end
end
end
end
object :user do
field :name, string
connection field :accounts, node_type: :account do
resolve fn %{accounts: accounts}, _args, _info ->
Absinthe.Relay.Connection.from_list(ids, args)
end
end
end
This would resolve the connections into a list of the user's associated accounts, and then for each node find that particular account (preferrably batched).
creating-connections
Creating Connections
This module provides two functions that mirror similar JavaScript functions,
from_list/2,3
and from_slice/2,3
. We also provide from_query/2,3
if you
have Ecto as a dependency for convenience.
Use from_list
when you have all items in a list that you're going to
paginate over.
Use from_slice
when you have items for a particular request, and merely need
a connection produced from these items.
supplying-edge-information
Supplying Edge Information
In some cases you may wish to supply extra information about the edge so that it can be used in the schema. For example:
connection node_type: :user do
edge do
field :role, :string
end
end
To do this, pass from_list
a list of 2-element tuples
where the first element is the node and the second element
either a map or a keyword list of the edge attributes.
[
{%{name: "Jim"}, role: "owner"},
{%{name: "Sari"}, role: "guest"},
{%{name: "Lee"}, %{role: "guest"}}, # This is OK, too
]
|> Connection.from_list(args)
This is useful when using ecto to include relationship information
on the edge itself via from_query
:
# In a UserResolver module
alias Absinthe.Relay
def list_teams(args, %{context: %{current_user: user}}) do
TeamAssignment
|> from
|> where([a], a.user_id == ^user.id)
|> join(:left, [a], t in assoc(a, :team))
|> select([a,t], {t, map(a, [:role])})
|> Relay.Connection.from_query(&Repo.all/1, args)
end
Be aware that if you pass :node
in the arguments provided as the second
element of the edge tuple, that value will be ignored and a warning logged.
If you provide a :cursor
argument, then your value will override
the internally generated cursor. This may or may not be desirable.
schema-macros
Schema Macros
For more details on connection-related macros, see
Absinthe.Relay.Connection.Notation
.
Link to this section Summary
Functions
Rederives the offset from the cursor string.
Get a connection object for a list of data.
Build a connection from an Ecto Query
Build a connection from slice
The direction and desired number of records in the pagination arguments.
Same as limit/1
with user provided upper bound.
Returns the offset for a page.
Creates the cursor string from an offset.
Link to this section Types
Specs
cursor() :: binary()
An opaque pagination cursor
Internally it has the base64 encoded structure:
arrayconnection::$offset
Specs
Specs
from_query_opts() :: [count: non_neg_integer(), max: pos_integer()] | from_slice_opts()
Specs
Specs
limit() :: non_neg_integer()
Specs
offset() :: non_neg_integer()
Offset from zero.
Negative offsets are not supported.
Specs
Specs
pagination_direction() :: :forward | :backward
Specs
Link to this section Functions
Specs
Rederives the offset from the cursor string.
Get a connection object for a list of data.
A simple function that accepts a list and connection arguments, and returns a connection object for use in GraphQL.
The data given to it should constitute all data that further pagination requests may page over. As such, it may be very inefficient if you're pulling data from a database which could be used to more directly retrieve just the desired data.
See also from_query
and from_slice
.
example
Example
#in a resolver module
@items ~w(foo bar baz)
def list(args, _) do
Connection.from_list(@items, args)
end
Specs
from_query( Ecto.Queryable.t(), (Ecto.Queryable.t() -> [term()]), Absinthe.Relay.Connection.Options.t(), from_query_opts() ) :: {:ok, map()} | {:error, any()}
Build a connection from an Ecto Query
This will automatically set a limit and offset value on the Ecto query, and then run the query with whatever function is passed as the second argument.
Notes:
- Your query MUST have an
order_by
value. Offset does not make sense without one. last: N
must always be accompanied by either abefore:
argument to the query, or an explicitcount:
option to thefrom_query
call. Otherwise it is impossible to derive the required offset.
example
Example
# In a PostResolver module
alias Absinthe.Relay
def list(args, %{context: %{current_user: user}}) do
Post
|> where(author_id: ^user.id)
|> Relay.Connection.from_query(&Repo.all/1, args)
end
Specs
from_slice(data :: list(), offset :: offset(), opts :: from_slice_opts()) :: {:ok, t()}
Build a connection from slice
This function assumes you have already retrieved precisely the number of items to be returned in this connection request.
Often this function is used internally by other functions.
example
Example
This is basically how our from_query/2
function works if we didn't need to
worry about backwards pagination.
# In PostResolver module
alias Absinthe.Relay
def list(args, %{context: %{current_user: user}}) do
{:ok, :forward, limit} = Connection.limit(args)
{:ok, offset} = Connection.offset(args)
Post
|> where(author_id: ^user.id)
|> limit(^limit)
|> offset(^offset)
|> Repo.all
|> Relay.Connection.from_slice(offset)
end
Specs
limit(args :: Absinthe.Relay.Connection.Options.t()) :: {:ok, pagination_direction(), limit()} | {:error, any()}
The direction and desired number of records in the pagination arguments.
Specs
limit(args :: Absinthe.Relay.Connection.Options.t(), max :: pos_integer() | nil) :: {:ok, pagination_direction(), limit()} | {:error, any()}
Same as limit/1
with user provided upper bound.
Often backend developers want to provide a maximum value above which no more records can be retrieved, no matter how many are asked for by the front end.
This function provides that capability. For use with from_list
or
from_query
use the :max
option on those functions.
Specs
Returns the offset for a page.
The limit is required because if using backwards pagination the limit will be subtracted from the offset.
If no offset is specified in the pagination arguments, this will return nil
.
Specs
Creates the cursor string from an offset.