PluginsDrizzle

Type variants

Expose one Drizzle table as several GraphQL types with the variant option.

One Drizzle table often needs to appear in the schema as more than one GraphQL type: a public view and a private one, a full record and a lightweight card. Pothos calls these variants. Every table has one primary type (defined with a name, as on the objects page); each additional variant is defined with a variant option in its place.

This page assumes the schema, relations, and builder are already wired up. The examples add an optional email column to the players table so a private variant has something to guard:

export const players = sqliteTable('players', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  number: integer('number').notNull(),
  email: text('email'),
  teamId: integer('team_id').notNull().references(() => teams.id),
});

Defining a variant

Give variant a type name instead of name. Here PlayerPrivateInfo is a second GraphQL type over the same players table, exposing a field the public type shouldn't:

const PlayerPrivateInfo = builder.drizzleObject('players', {
  variant: 'PlayerPrivateInfo',
  select: {},
  fields: (t) => ({
    id: t.exposeID('id'),
    email: t.exposeString('email', { nullable: true }),
  }),
});

Linking variants together

t.variant adds a field that returns another variant of the same row. Reference the primary variant by its table name as a string; reference any other variant by the object ref it returned. An isNull callback can hide the variant when it shouldn't be visible. Here the private info resolves to null unless the parent player is the current viewer:

const PlayerPrivateInfo = builder.drizzleObject('players', {
  variant: 'PlayerPrivateInfo',
  select: {},
  fields: (t) => ({
    email: t.exposeString('email', { nullable: true }),
    // The table name references the primary variant.
    player: t.variant('players'),
  }),
});

const Player = builder.drizzleNode('players', {
  name: 'Player',
  id: { column: (player) => player.id },
  fields: (t) => ({
    name: t.exposeString('name'),
    // Reference another variant by its ref, not the table name.
    privateInfo: t.variant(PlayerPrivateInfo, {
      // Hide private info unless the parent player is the current viewer.
      isNull: (player, args, ctx) => player.id !== ctx.currentPlayerId,
    }),
  }),
});

builder.drizzleNode needs the relay plugin and takes an id option: the column (or columns) that back the node's global id. A variant that doesn't need to be a Relay node can use builder.drizzleObject instead.

Variants on relations

A relation field can return a variant rather than the related table's primary type. Pass the variant ref as the relation's type, and use query to scope which rows it loads. Here a team's schedule returns games through a CompletedGame variant, filtered to games already played:

const CompletedGame = builder.drizzleNode('games', {
  variant: 'CompletedGame',
  id: { column: (game) => game.id },
  fields: (t) => ({
    playedAt: t.exposeString('playedAt'),
    homeTeam: t.relation('homeTeam'),
  }),
});

const TeamSchedule = builder.drizzleObject('teams', {
  variant: 'TeamSchedule',
  fields: (t) => ({
    id: t.exposeID('id'),
    schedule: t.relation('homeGames', {
      // Use the CompletedGame variant for this relation instead of the default Game.
      type: CompletedGame,
      query: { where: { playedAt: { lt: new Date().toISOString() } } },
    }),
  }),
});

Breaking circular references

Two drizzle object refs that reference each other in their fields functions can trip TypeScript into a circular-type error. Split one side out with builder.drizzleObjectField, which attaches a single field after both refs exist. It takes the ref (or type name), the field name, and a field function:

const PlayerPrivateInfo = builder.drizzleObject('players', {
  variant: 'PlayerPrivateInfo',
  select: {},
  fields: (t) => ({
    email: t.exposeString('email', { nullable: true }),
  }),
});

const Player = builder.drizzleNode('players', {
  name: 'Player',
  id: { column: (player) => player.id },
  fields: (t) => ({
    name: t.exposeString('name'),
  }),
});

// Attach the back-reference after both refs exist, breaking the cycle.
builder.drizzleObjectField(PlayerPrivateInfo, 'player', (t) => t.variant(Player));

The same workaround applies to relations that use variants: move the offending relation field into a drizzleObjectField call.