Pothos

Connections

To implement a relation as a connection, you can use t.relatedConnection instead of t.relation:

builder.drizzleNode('users', {
  name: 'User',
  fields: (t) => ({
    posts: t.relatedConnection('posts'),
  }),
});

This will automatically define the Connection, and Edge types, and their respective fields. To customize the Connection and Edge types, options for these types can be passed as additional arguments to t.relatedConnection, just like t.connection from the relay plugin. See the relay plugin docs for more details.

You can also define a query like with t.relation. The only difference with t.relatedConnection is that the orderBy format is slightly changed.

To comply with the relay spec and efficiently support backwards pagination, some queries need to be performed in reverse order, which requires inverting the orderBy clause. To do this automatically, the t.relatedConnection method accepts orderBy as an object keyed by column name with 'asc' or 'desc' values, like { createdAt: 'desc' }, rather than using the asc(column) and desc(column) helpers from drizzle. orderBy can also be returned as a single column, or an array of columns when ordering by multiple columns, which orders ascending.

Ordering defaults to using the table primaryKey, and the orderBy columns will also be used to derive the connections cursor.

builder.drizzleNode('users', {
  name: 'User',
  fields: (t) => ({
    posts: t.relatedConnection('posts', {
      query: () => ({
        where: {
          published: true,
        },
        orderBy: {
          id: 'desc',
        },
      }),
    }),
  }),
});

Connection totalCount

You can add a totalCount field to your connection by setting the totalCount option to true:

builder.drizzleNode('users', {
  name: 'User',
  fields: (t) => ({
    posts: t.relatedConnection('posts', {
      totalCount: true,
      query: () => ({
        where: {
          published: true,
        },
        orderBy: {
          id: 'desc',
        },
      }),
    }),
  }),
});

This will automatically add a totalCount field to the connection type. The count query is only executed when the totalCount field is actually requested in the GraphQL query, and it's included as a subquery in the main database query for efficiency.

query {
  user(id: "...") {
    posts(first: 10) {
      totalCount
      edges {
        node {
          id
          title
        }
      }
    }
  }
}

The count applies the where returned by the field's query, so it counts the same rows the connection paginates (only published posts in the example above). To count every related row regardless of the filter, set filterConnectionTotalCount: false in the drizzle plugin options:

const builder = new SchemaBuilder<PothosTypes>({
  plugins: [DrizzlePlugin],
  drizzle: {
    client: db,
    getTableConfig,
    relations,
    // count every related row for totalCount, ignoring the where from query (defaults to true)
    filterConnectionTotalCount: false,
  },
});

A where on the relation itself always applies to the count, as does the junction table of a many-to-many relation defined with .through(...). The count joins the junction table the same way the rows do, so a row that matches the junction twice counts twice, and appears twice in the connection.

Drizzle connections

Similar to t.drizzleField, t.drizzleConnection allows you to define a connection field that acts as an entry point to your drizzle query. The orderBy in t.drizzleConnection works the same way as it does for t.relatedConnection

builder.queryFields((t) => ({
  posts: t.drizzleConnection({
    type: 'posts',
    resolve: (query, root, args, ctx) =>
      db.query.posts.findMany(
        query({
          where: {
            published: true,
          },
          orderBy: {
            id: 'desc',
          },
        }),
      ),
  }),
}));

drizzleConnection totalCount

You can add a totalCount field to a drizzleConnection by providing a totalCount callback function that returns the count:

builder.queryFields((t) => ({
  posts: t.drizzleConnection({
    type: 'posts',
    // Use db.$count() for a simple count query
    totalCount: () => db.$count(posts, eq(posts.published, true)),
    resolve: (query, root, args, ctx) =>
      db.query.posts.findMany(
        query({
          where: {
            published: true,
          },
          orderBy: {
            id: 'desc',
          },
        }),
      ),
  }),
}));

The totalCount callback receives the same arguments as a normal resolver (parent, args, context, info), allowing you to implement custom count logic based on the query context. The example above uses db.$count() for a simple count, but you can use any Drizzle query approach.

When only the totalCount field is requested (without edges or nodes), the main query is skipped entirely and only the count query is executed for efficiency.

Indirect relations as connections

In many cases, you can define many to many connections via drizzle relations, allowing the relatedConnection API to work across more complex relations. In some cases you may want to define a connection for a relation not expressed directly as a relation in your drizzle schema. For these cases, you can use the drizzleConnectionHelpers, which allows you to define connection with the t.connection API.

// Create a drizzle object for the node type of your connection
const Role = builder.drizzleObject('roles', {
  name: 'Role',
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
  }),
});



// Create connection helpers for the media type.  This will allow you
// to use the normal t.connection with a drizzle type
const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
  // select the data needed for the nodes
  select: (nestedSelection) => ({
    with: {
      // use nestedSelection to create the correct selection for the node
      role: nestedSelection(),
    },
  }),
  // resolve the node from the returned list item
  resolveNode: (userRole) => userRole.role,
});

builder.drizzleObjectField('users', 'rolesConnection', (t) =>
  t.connection({
    // The type for the Node
    type: Role,
    // since we are not using t.relatedConnection we need to manually
    // include the selections for our connection
    select: (args, ctx, nestedSelection) => ({
      with: {
        userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
      },
    }),
    // This helper takes a list of nodes and formats them for the connection
    resolve: (user, args, ctx) => {
      return rolesConnection.resolve(user.userRoles, args, ctx, user);
    },
  }),
);

The above example assumes that you are paginating a relation to a join table, where the pagination args are applied based on the relation to that join table, but the nodes themselves are nested deeper.

drizzleConnectionHelpers can also be used to manually create a connection where the edge and connections share the same model, and pagination happens directly on a relation to nodes type (even if that relation is nested).

const commentConnectionHelpers = drizzleConnectionHelpers(builder, 'comments');

const SelectPost = builder.drizzleObject('posts', {
  fields: (t) => ({
    title: t.exposeString('title'),
    comments: t.connection({
      type: commentConnectionHelpers.ref,
      select: (args, ctx, nestedSelection) => ({
        with: {
          comments: commentConnectionHelpers.getQuery(args, ctx, nestedSelection),
        },
      }),
      resolve: (parent, args, ctx) => commentConnectionHelpers.resolve(parent.comments, args, ctx),
    }),
  }),
});

Arguments, ordering and filtering can also be defined in the helpers:

const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
  // define additional arguments
  args: (t) => ({}),
  query: (args) => ({
    // define an order
    orderBy: {
      roleId: 'asc',
    }
    // define a filter
    where: {
      accepted: true,
    }
  }),
  // select the data needed for the nodes
  select: (nestedSelection) => ({
    with: {
      // use nestedSelection to create the correct selection for the node
      role: nestedSelection(),
    },
  }),
  // resolve the node from the returned list item
  resolveNode: (userRole) => userRole.role,
});


builder.drizzleObjectField('users', 'rolesConnection', (t) =>
  t.connection({
    type: Role,
    // add the args from the connection helper to the field
    args: rolesConnection.getArgs(),
    select: (args, ctx, nestedSelection) => ({
      with: {
        userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
      },
    }),
    resolve: (user, args, ctx) => rolesConnection.resolve(user.userRoles, args, ctx, user),
  }),
);

Extending connection edges

In some cases you may want to expose some data from an indirect connection on the edge object.

const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
  select: (nestedSelection) => ({
    with: {
      role: nestedSelection(),
    },
  }),
  resolveNode: (userRole) => userRole.role,
});

builder.drizzleObjectFields('users', (t) => ({
  rolesConnection: t.connection(
    {
      type: Role,
      select: (args, ctx, nestedSelection) => ({
        with: {
          userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
        },
      }),
      resolve: (user, args, ctx) =>
        rolesConnection.resolve(
          user.userRoles,
          args,
          ctx,
          user,
        ),
    },
    {},
    // options for the edge object
    {
      // define the additional fields on the edge object
      fields: (edge) => ({
        createdAt: edge.field({
          type: 'DateTime',
          // the parent shape for edge fields is inferred from the connections resolve function
          resolve: (role) => role.createdAt,
        }),
      }),
    },
  ),
}));

drizzleConnectionHelpers for non-relation connections

You can also use drizzleConnectionHelpers for non-relation connections where you want a connection where your edges and nodes are not the same type.

Note that when doing this, you need to be careful to properly merge the where clause generated by the connection helper with any additional where clause you need to apply to your query

const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
  select: (nestedSelection) => ({
    with: {
      role: nestedSelection(),
    },
  }),
  resolveNode: (userRole) => userRole.role,
});

builder.queryFields((t) => ({
  roles: t.connection({
    type: Role,
    args: {
      userId: t.arg.int({ required: true }),
    },
    nodeNullable: true,
    resolve: async (_, args, ctx, info) => {
      const query = rolesConnection.getQuery(args, ctx, info);
      const userRoles = await db.query.userRoles.findMany({
        ...query,
        where: {
          ...query.where,
          userId: args.userId,
        },
      });
      return rolesConnection.resolve(userRoles, args, ctx);
    },
  }),
}));

On this page