PluginsPrisma

Relations

Add relation fields with t.relation, shape them with query and args, and add relation counts.

t.relation adds a field for a relation declared in your Prisma schema. The plugin pre-loads it through the query of whichever t.prismaField started the request, so a chain of relations resolves without a query per level.

builder.queryType({
  fields: (t) => ({
    myTeam: t.prismaField({
      type: 'Team',
      resolve: async (query, _root, _args, ctx) =>
        prisma.team.findUniqueOrThrow({
          ...query,
          where: { id: ctx.teamId },
        }),
    }),
  }),
});

builder.prismaObject('Team', {
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
    players: t.relation('players'),
  }),
});

builder.prismaObject('Player', {
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
    team: t.relation('team'),
  }),
});

Each t.relation contributes an include ({ include: { players: true } }) to the query argument of the prismaField that resolves the parent. When a relation field's parent is itself a relation, the includes nest, and the whole chain flows back to the prismaField at the root. This query:

query {
  myTeam {
    players {
      team {
        id
      }
    }
  }
}

hands the myTeam resolver a query shaped like:

{
  include: {
    players: {
      include: {
        team: true,
      },
    },
  },
}

That single include resolves the whole tree. A few cases make one query impossible; when they arise, Pothos loads the missing pieces itself.

Fallback queries

When some data can't be pre-loaded, Pothos issues a findUnique for the parent of the un-loaded fields and selects just the missing relations. These queries are efficient: Pothos batches the requirements of several fields into one, and Prisma batches the resulting per-parent queries (the N+1 shape) down to a single SQL statement.

A fallback query kicks in when:

  • The parent object wasn't loaded through a t.prismaField or t.relation.
  • The root t.prismaField didn't spread its query argument into the Prisma call.
  • The query selects the same relation more than once with different filters, sorting, or limits.
  • The query aliases the same relation field with different arguments that produce different relation query options.
  • A relation field's query is incompatible with the parent object's default includes.

These are uncommon in normal use, and the plugin handles them automatically when they occur.

Filters, sorting, and arguments

t.prismaField takes arguments like any field and you pass them into your own Prisma call. t.relation is different. You aren't writing the Prisma query, the planner is, so you shape the relation with a query option. It's either a query object or a function of the field's arguments and the request context:

builder.prismaObject('Team', {
  fields: (t) => ({
    id: t.exposeID('id'),
    players: t.relation('players', {
      // Arguments are declared like any other field.
      args: {
        byNumber: t.arg.boolean(),
      },
      // Build the relation query from those arguments.
      query: (args, _context) => ({
        orderBy: args.byNumber ? { number: 'asc' } : { name: 'asc' },
      }),
    }),
  }),
});

The object query returns is merged into the include for this relation, so it accepts the usual relation query keys: where, skip, take, orderBy. The function receives the field arguments and the request context. It does not receive the parent object: the relation is pre-loaded before the parent exists, which is exactly what avoids the N+1 query.

Relation counts

Prisma can return relation counts alongside other includes. t.relationCount exposes one as an Int field:

builder.prismaObject('Team', {
  fields: (t) => ({
    id: t.exposeID('id'),
    playerCount: t.relationCount('players', {
      where: {
        number: { gte: 1 },
      },
    }),
  }),
});

Filtering a relation count needs Prisma 4.2.0 or newer, under the filteredRelationCount preview feature. Before 4.2.0, t.relationCount still works but can only return a total, so drop the where option.