Connections

Paginate Drizzle queries and relations using GraphQL selections

Connection fields combine cursor pagination with Pothos's selection planning. Define the rows a field may return; Pothos adds pagination constraints and selects the columns and relations requested under each node. The same field supports forward and backward pagination.

Use t.drizzleConnection when your resolver queries the rows and t.relatedConnection for a Drizzle relation. Both require the Relay plugin.

Page through published posts

The feed returns published posts ordered by creation time. Its resolver passes the result of query() to Drizzle, just as a t.drizzleField does:

posts: t.drizzleConnection({
  type: 'posts',
  totalCount: () => {
    return db.$count(posts, eq(posts.published, true));
  },
  resolve: (query) => {
    return db.query.posts.findMany(
      query({
        where: { published: true },
        // Three posts share this timestamp. Pothos adds the primary key
        // to the cursor ordering, so traversing pages still visits each once.
        orderBy: { createdAt: 'desc' },
      }),
    );
  },
}),

Pothos uses the selected node fields to plan the database query, including the nested author. It also adds the ordering columns needed for cursors, even when the client does not select them. When creation times tie, Pothos appends a primary-key tie breaker so posts have distinct positions. See Ordering and cursors for other orderings and column requirements.

A client requests the first page with first:

query PostFeed($after: String) {
  posts(first: 2, after: $after) {
    totalCount
    edges {
      cursor
      node {
        title
        author {
          fullName
        }
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

For the next page, pass the previous endCursor as after. To paginate backward, use last and before, taking before from a page's startCursor:

query PreviousPosts($before: String) {
  posts(last: 2, before: $before) {
    nodes {
      title
    }
    pageInfo {
      startCursor
      hasPreviousPage
    }
  }
}

Backward pagination retains the connection's declared ordering; it selects the preceding rows rather than reversing the response. nodes is a convenience field enabled by relay: { nodesOnConnection: true }. With the default Relay configuration, use edges { node }.

An author's postsConnection applies the same publication filter as the feed. t.relatedConnection plans that relation from the parent selection without a custom resolver:

postsConnection: t.relatedConnection('posts', {
  query: { where: { published: true }, orderBy: { createdAt: 'desc' } },
  totalCount: true,
}),

The query option accepts an object or a callback receiving field arguments and context, as it does on t.relation. Connection orderBy accepts column names with 'asc' or 'desc', or a column or array of columns for ascending order. Pothos can invert these orderings when fetching a backward page. The default ordering uses the table's primary key.

Both connection methods create Connection and Edge types. Pass additional options to customize those types, as with the Relay plugin's connection fields.

Connection totalCount

totalCount describes the matching rows across all pages. The author's totalCount: true counts both published posts even when first: 1 returns only one. Pothos loads the count only when it is selected; a query for only totalCount does not load the related posts.

For t.relatedConnection, Pothos derives the count from the relation and its filter. For a root t.drizzleConnection, supply a totalCount resolver, as the feed does above. Its filter must match the rows returned by the connection. This resolver receives the usual parent, args, context, and info arguments. A count-only root query skips the main resolver.

By default, a related count includes the where returned by the connection's query. Setting filterConnectionTotalCount: false in the builder's drizzle options ignores that field filter for counts. A filter on the Drizzle relation itself still applies. Keep the default for the public author field: counting drafts would reveal data excluded by its publication filter.

For a many-to-many relation defined with through, the count matches the rows being paginated. If two junction rows reach the same target, that target appears and is counted twice. This differs from t.relatedCount, which counts distinct related targets.

Connections with custom edges

A direct many-to-many relation works with t.relatedConnection. When the attachment itself has fields, such as a caption, the edge needs data from the junction row while its node represents Media. Connection helpers shows how to combine that mapping with selection planning, pagination, and custom edge fields in one connection.