How to set up cursor-based pagination with Absinthe?

Does anybody have a good guide for setting up cursor-based pagination, as recommended in The GraphQL docs, using Absinthe?

3 Likes

I don’t have a guide but I recently made a demo project that had cursor based pagination using Relay. Absinthe Relay image upload with Expo React Native

3 Likes

It’s possible to add Relay support for Absinthe, and as mentionned by @slouchpie it support cursor based pagination.

1 Like

Hmm. While it’s encouraging that absinthe_relay | Hex is published by @benwilson512, and I see that the Relay project hosts the formal specification for this style of pagination, we don’t have Relay consumers in mind for our API; the users will be internal to a company and using a variety of programming languages.

I’ll have to think about whether adding Relay support via absinthe_relay as the Absinthe docs show is the best way to enable pagination.

1 Like

Neither do we. The Relay spec is still pretty useful though, both for pagination and for its node pattern. Using it however is of course optional.

1 Like

Thanks!

Another question: all the examples I see are about paginating associations - eg, the locations of a business. Do people also use this pattern for paginating top-level records, like “list all businesses”?

Oh, I think they do. I see this example in a blog post (using JavaScript).

{
  news {
    totalCount
    edges {
      node {
        body
        title
      }
      cursor
    }
    pageInfo {
      startCursor
      hasNextPage
    }
  }
}
```
1 Like

This is exactly what I do in the demo project I linked above. I have a “pagination fragment” on the “RootQueryType”.

1 Like

We use this for cursor pagination for our GraphQL API. Not sure if it’s what you’re after though.

2 Likes

I’m looking at absinthe_relay and I see functions like cursor_to_offset/1 and offset_to_cursor/1 and from_query/4 referring to pagination using LIMIT + OFFSET queries.

I’m confused because I thought the main purpose of using cursors was to avoid using OFFSET for pagination due to its poor performance with later pages in large record sets. I was hoping to use a cursor to encode something like “the next page starts after id 200”. Is that approach compatible with absinthe_relay?

2 Likes

I think I’m figuring this out. I’ll post a code snippet if I can get it working properly.

2 Likes

Yes, but you’ll want to use the from_slice function. Basically, Absinthe.Relay from_query does a dump simple limit + offset in order to be able to work on everyone’s ecto config without any prior knowledge. If a user has a better way to interpret cursors in light of their particular database structures then their best option is to make the regular Ecto query and use from_slice.

2 Likes

Do you have an example of how you are using the library? I’m trying to learn how to build a GraphQL API in elixir+phoenix, and I can’t wrap my head around how to do the pagination well.

@nathanl not a guide per-se but some examples from other open source Elixir codebases that may help you:

5 Likes

I created a library (only on GitHub for now) to support keyset pagination with absinthe_relay. It’s basically a drop-in replacement in your resolver. An example of paginating a usersConnection:

# in your resolver function
AbsintheRelayKeysetConnection.from_query(
  ecto_users_query,
  &MyRepo.all/1,
  # these args would come from the API user
  %{
    sorts: [%{name: :desc}, %{id: :desc}],
    first: 10,
    after: "0QTwn5SRWyJNbyIsMjZd"
  },
  # you would hard-code this configuration
  %{unique_column: :id}
)

The reason a unique_column is specified here is to be a “tie breaker” to ensure a unique value for sorting and paginating. See the moduledocs for more details.

If using this, you might put (for example) under your usersConnection:

arg(:sorts, list_of(:user_order_by))

You’d use user_order_by to define the ways you allow sorting for that record type. (You might want to consider what index support you have or need.)

  input_object :user_order_by do
    @desc "By email"
    field(:email, :sort_direction)

    @desc "By first name"
    field(:first_name, :sort_direction)

   # etc
  end

:sort_direction is a simple enum type:

  enum :sort_direction do
    @desc "Ascending (for example, 1,2,3 or a,b,c)."
    value(:asc)
    @desc "Descending (for example, 3,2,1 or c,b,a)."
    value(:desc)
  end

@benwilson512 I’d love to contribute this to absinthe_relay | Hex if you’re interested. If not, I might release it as a separate package.

7 Likes

Hey Nathan! That library is great! I’ve some changes I plan to send as a PR (behaviour for Cursor to allow custom cursor implementations, making and marking the default Cursor as “tamper-resistant”, rather than tamper-proof, using Ecto’s dynamic/2 to allow > 3). Still working on some things, including adding tests for a NaiveDateTime sort, and I’m thinking of allowing the key for sorts to be configurable, although this could also be changed in the resolvers before calling from_query/4.

I’m also planning some tests on nullable columns, to see how it affects sorting (IIRC, == nil isn’t allowed in Ecto, and is_nil/1 should be used instead). Maybe this will be outside the scope though, given that asc and desc don’t cover the cases for all possible sorts:

  • :asc
  • :asc_nulls_last
  • :asc_nulls_first
  • :desc
  • :desc_nulls_last
  • :desc_nulls_first

And I’m not sure if it makes sense to allow all these cases.

5 Likes

Thanks @jeroenvisser101! Nice PR - let’s continue talking there.

FYI - this library is now published at absinthe_relay_keyset_connection | Hex

5 Likes