PluginsPrisma Next

Transactions

Running mutations and GraphQL operations in a Prisma transaction

Use Prisma's db.transaction when several writes must succeed or fail together. The plugin works with your transaction client; your application controls when the transaction commits or rolls back. The examples use the imported db and builder from Setup, and the User ref from your schema. They use the shorter SQLite client paths; on PostgreSQL, use db.orm.public.User and tx.orm.public.User for models in the public schema.

Write several rows atomically

Pothos doesn't create transactions for you. To run a mutation's writes in one, call transaction in the mutation field, then load the result once it commits:

import { randomUUID } from 'node:crypto';

builder.mutationField('signUp', (t) =>
  t.prismaField({
    type: 'User',
    args: {
      email: t.arg.string({ required: true }),
      firstName: t.arg.string({ required: true }),
      lastName: t.arg.string({ required: true }),
    },
    resolve: async (users, _root, args) => {
      const id = await db.transaction(async (tx) => {
        const user = await tx.orm.User.create({ ...args, id: randomUUID() });
        await tx.orm.Post.create({
          id: randomUUID(),
          authorId: user.id,
          title: 'Hello!',
          content: '',
          createdAt: new Date(),
        });
        return user.id;
      });
      return users.where({ id }).first();
    },
  }),
);

To read the result inside the transaction too, build the last write with collectionFromInfo and return the row it writes:

import { collectionFromInfo } from '@pothos/plugin-prisma-next';

builder.mutationField('renameUser', (t) =>
  t.field({
    type: User,
    nullable: true,
    args: { id: t.arg.id({ required: true }), firstName: t.arg.string({ required: true }) },
    resolve: (_root, args, ctx, info) =>
      db.transaction(async (tx) => {
        const users = await collectionFromInfo({
          collection: tx.orm.User.where({ id: String(args.id) }),
          context: ctx,
          info,
        });
        return users.update({ firstName: args.firstName });
      }),
  }),
);

Don't return a tx.orm Collection from the callback: it runs after the transaction ends, and fails.

Share a transaction across an operation

To give each request its own client, such as a transaction, pass a function as client along with the base client's context. Use this configuration before defining your types and building the schema:

import SchemaBuilder from '@pothos/core';
import PrismaNextPlugin from '@pothos/plugin-prisma-next';
import { contract, db } from './prisma/db';
import type { PostgresQueryMembers } from '@prisma/orm-postgres/runtime';

const builder = new SchemaBuilder<{
  PrismaContract: typeof contract;
  Context: { db: PostgresQueryMembers<typeof contract> };
}>({
  plugins: [PrismaNextPlugin],
  prisma: {
    client: (ctx) => ctx.db,
    context: db.context,
  },
});

The configured client applies to queries the plugin starts itself. In your own collection and resolve callbacks, use ctx.db in this mode too. Calls to the imported db use the base client and run outside this transaction.

Call graphql() inside the transaction and pass tx in the GraphQL context:

const result = await db.transaction(async (tx) => {
  const result = await graphql({
    schema,
    source,
    contextValue: { db: tx },
  });
  // GraphQL reports errors in the result rather than rejecting, so throw to roll back.
  if (result.errors?.length) {
    throw result.errors[0];
  }
  return result;
});

With @defer or @stream, read every incremental result inside the callback: they load after the initial result, and fail once the transaction has ended.