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

edge() :: %{node: term(), cursor: cursor()}

Specs

from_query_opts() ::
  [count: non_neg_integer(), max: pos_integer()] | from_slice_opts()

Specs

from_slice_opts() :: [has_previous_page: boolean(), has_next_page: boolean()]

Specs

limit() :: non_neg_integer()

Specs

offset() :: non_neg_integer()

Offset from zero.

Negative offsets are not supported.

Specs

page_info() :: %{
  start_cursor: cursor(),
  end_cursor: cursor(),
  has_previous_page: boolean(),
  has_next_page: boolean()
}
Link to this type

pagination_direction()

View Source

Specs

pagination_direction() :: :forward | :backward

Specs

t() :: %{edges: [edge()], page_info: page_info()}

Link to this section Functions

Link to this function

cursor_to_offset(cursor)

View Source

Specs

cursor_to_offset(binary()) :: {:ok, integer()} | {:error, any()}

Rederives the offset from the cursor string.

Link to this function

from_list(data, args, opts \\ [])

View Source

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
Link to this function

from_query(query, repo_fun, args, opts \\ [])

View Source

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 a before: argument to the query, or an explicit count: option to the from_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
Link to this function

from_slice(items, offset, opts \\ [])

View Source

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

offset(args :: Absinthe.Relay.Connection.Options.t()) ::
  {:ok, offset() | nil} | {:error, any()}

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.

Link to this function

offset_to_cursor(offset)

View Source

Specs

offset_to_cursor(integer()) :: binary()

Creates the cursor string from an offset.