import { Prisma, PrismaClient } from "@prisma/client"; export type PrismaTransactionClient = Omit< PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" >; export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient; export type PrismaReplicaClient = Omit; function isTransactionClient(prisma: PrismaClientOrTransaction): prisma is PrismaTransactionClient { return !("$transaction" in prisma); } export function isPrismaKnownError(error: unknown): error is Prisma.PrismaClientKnownRequestError { return ( typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ); } /* • P2024: Connection timeout errors • P2028: Transaction timeout errors • P2034: Transaction deadlock/conflict errors */ const retryCodes = ["P2024", "P2028", "P2034"]; export function isPrismaRetriableError(error: unknown): boolean { if (!isPrismaKnownError(error)) { return false; } return retryCodes.includes(error.code); } /* • P2025: Record not found errors (in race conditions) [not included for now] */ export function isPrismaRaceConditionError(error: unknown): boolean { if (!isPrismaKnownError(error)) { return false; } return error.code === "P2025"; } export type PrismaTransactionOptions = { /** The maximum amount of time (in ms) Prisma Client will wait to acquire a transaction from the database. The default value is 2000ms. */ maxWait?: number; /** The maximum amount of time (in ms) the interactive transaction can run before being canceled and rolled back. The default value is 5000ms. */ timeout?: number; /** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */ isolationLevel?: Prisma.TransactionIsolationLevel; swallowPrismaErrors?: boolean; /** * The maximum number of times the transaction will be retried in case of a serialization failure. The default value is 0. * * See https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-timing-issues */ maxRetries?: number; }; export async function $transaction( prisma: PrismaClientOrTransaction, fn: (prisma: PrismaTransactionClient) => Promise, prismaError: (error: Prisma.PrismaClientKnownRequestError) => void, options?: PrismaTransactionOptions, attempt = 0 ): Promise { if (isTransactionClient(prisma)) { return fn(prisma); } try { return await (prisma as PrismaClient).$transaction(fn, options); } catch (error) { if (isPrismaKnownError(error)) { if ( retryCodes.includes(error.code) && typeof options?.maxRetries === "number" && attempt < options.maxRetries ) { return $transaction(prisma, fn, prismaError, options, attempt + 1); } prismaError(error); if (options?.swallowPrismaErrors) { return; } } throw error; } } export function isUniqueConstraintError( error: unknown, columns: T ): boolean { if (!isPrismaKnownError(error)) { return false; } if (error.code !== "P2002") { return false; } const target = error.meta?.target; if (!Array.isArray(target)) { return false; } if (target.length !== columns.length) { return false; } for (let i = 0; i < columns.length; i++) { if (target[i] !== columns[i]) { return false; } } return true; }