Relay Cursor Connections (Pagination)

Clone the code (optional)

The code for this tutorial is available under github.com/a8m/ent-graphql-example, and tagged (using Git) in each step. If you want to skip the basic setup and start with the initial version of the GraphQL server, you can clone the repository as follows:

  1. cd ent-graphql-example
  2. go run ./cmd/todo/

Ordering can be defined on any comparable field of Ent by annotating it with entgql.Annotation. Note that the given OrderField name must be uppercase and match its enum value in the GraphQL schema.

  1. func (Todo) Fields() []ent.Field {
  2. return []ent.Field{
  3. field.Text("text").
  4. NotEmpty().
  5. Annotations(
  6. entgql.OrderField("TEXT"),
  7. ),
  8. field.Time("created_at").
  9. Default(time.Now).
  10. Immutable().
  11. Annotations(
  12. entgql.OrderField("CREATED_AT"),
  13. ),
  14. field.Enum("status").
  15. NamedValues(
  16. "InProgress", "IN_PROGRESS",
  17. "Completed", "COMPLETED",
  18. ).
  19. Annotations(
  20. entgql.OrderField("STATUS"),
  21. ),
  22. field.Int("priority").
  23. Default(0).
  24. entgql.OrderField("PRIORITY"),
  25. ),
  26. }
  27. }
  1. The next step for enabling pagination is to tell Ent that the Todo type is a Relay Connection.

ent/schema/todo.go

  1. Then, run go generate . and you’ll notice that ent.resolvers.go was changed. Head over to the Todos resolver and update it to pass pagination arguments to .Paginate():

ent.resolvers.go

  1. func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder) (*ent.TodoConnection, error) {
  2. return r.client.Todo.Query().
  3. Paginate(ctx, after, first, before, last,
  4. ent.WithTodoOrder(orderBy),
  5. )
  6. }

Relay Connection Configuration

The entgql.RelayConnection() function indicates that the node or edge should support pagination. Hence,the returned result is a Relay connection rather than a list of nodes ([T!]! => <T>Connection!).

Setting this annotation on schema T (reside in ent/schema), enables pagination for this node and therefore, Ent will generate all Relay types for this schema, such as: <T>Edge, <T>Connection, and PageInfo. For example:

  1. func (Todo) Annotations() []schema.Annotation {
  2. return []schema.Annotation{
  3. entgql.RelayConnection(),
  4. entgql.QueryField(),
  5. }
  6. }

The generated GraphQL schema will be:

Now, we’re ready to test our new GraphQL resolvers. Let’s start with creating a few todo items by running this query multiple times (changing variables is optional):

  1. mutation CreateTodo($input: CreateTodoInput!) {
  2. createTodo(input: $input) {
  3. id
  4. text
  5. createdAt
  6. priority
  7. parent {
  8. id
  9. }
  10. }
  11. }
  12. # Query Variables: { "input": { "text": "Create GraphQL Example", "status": "IN_PROGRESS", "priority": 1 } }
  13. # Output: { "data": { "createTodo": { "id": "2", "text": "Create GraphQL Example", "createdAt": "2021-03-10T15:02:18+02:00", "priority": 1, "parent": null } } }

Then, we can query our todo list using the pagination API:

We can also use the cursor we got in the query above to get all items that come after it.

  1. query {
  2. todos(first: 3, after:"gqFpEKF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU", orderBy: {direction: DESC, field: TEXT}) {
  3. edges {
  4. node {
  5. id
  6. text
  7. }
  8. cursor
  9. }
  10. }
  11. }
  12. # Output: { "data": { "todos": { "edges": [ { "node": { "id": "15", "text": "Create GraphQL Example" }, "cursor": "gqFpD6F2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" }, { "node": { "id": "14", "text": "Create GraphQL Example" }, "cursor": "gqFpDqF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" }, { "node": { "id": "13", "text": "Create GraphQL Example" }, "cursor": "gqFpDaF2tkNyZWF0ZSBHcmFwaFFMIEV4YW1wbGU" } ] } } }

Great! With a few simple changes, our application now supports pagination. Please continue to the next section where we explain how to implement GraphQL field collections and learn how Ent solves the “N+1 problem” in GraphQL resolvers.