UNPKG

@myfunc/prisma-transactional

Version:

Decorator that wraps all prisma queries along the whole call stack to a single transaction.

149 lines (148 loc) 6.75 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.patchPrismaTx = exports.ClsServiceManager = exports.ClsService = void 0; const nestjs_cls_1 = require("nestjs-cls"); Object.defineProperty(exports, "ClsService", { enumerable: true, get: function () { return nestjs_cls_1.ClsService; } }); Object.defineProperty(exports, "ClsServiceManager", { enumerable: true, get: function () { return nestjs_cls_1.ClsServiceManager; } }); const const_1 = require("./const"); const manager_1 = require("./manager"); // This function needs to be called after the transaction commit // You should call this at the point where your transaction successfully commits function executeSuccessCallbacks() { const clsService = nestjs_cls_1.ClsServiceManager.getClsService(); const callbacks = clsService.get(const_1.TX_CLIENT_SUCCESS_CALLBACKS) || []; callbacks.forEach((callback) => { try { callback(); } catch (e) { manager_1.Manager.logger.error({ context: 'Prisma.' + executeSuccessCallbacks.name, message: 'Error executing success callback', error: e, }); } }); clsService.set(const_1.TX_CLIENT_SUCCESS_CALLBACKS, []); // Clear the queue after execution } function patchPrismaTx(prisma, config) { const _prisma = prisma; const original$transaction = _prisma.$transaction; _prisma.$transaction = (...args) => { if (typeof args[0] === 'function') { const fn = args[0]; args[0] = async (txClient) => { const clsService = nestjs_cls_1.ClsServiceManager.getClsService(); const maybeExistingTxClient = clsService.get(const_1.TX_CLIENT_KEY); if (maybeExistingTxClient) { manager_1.Manager.logger.verbose?.({ context: 'Prisma.' + patchPrismaTx.name, message: 'Return txClient from ALS', }); return fn(maybeExistingTxClient); } if (clsService.isActive()) { // covering this for completeness, should rarely happen manager_1.Manager.logger.warn({ context: 'Prisma.' + patchPrismaTx.name, message: 'Context active without txClient', }); return executeInContext({ context: clsService, txClient, fn, }); } // this occurs on the top-level return clsService.run(async () => { return executeInContext({ context: clsService, txClient, fn, }); }); }; } return original$transaction.apply(_prisma, args); }; const proxyPrisma = createPrismaProxy(_prisma); manager_1.Manager.setPrismaClient(proxyPrisma); manager_1.Manager.setConfig(config); return proxyPrisma; } exports.patchPrismaTx = patchPrismaTx; async function executeInContext({ context, txClient, fn }) { context.set(const_1.TX_CLIENT_KEY, txClient); manager_1.Manager.logger.verbose?.({ context: 'Prisma.' + executeInContext.name, message: 'Top-level: open context, store txClient and propagate', }); try { const result = await fn(txClient); executeSuccessCallbacks(); return result; } finally { context.set(const_1.TX_CLIENT_KEY, undefined); manager_1.Manager.logger.verbose?.({ context: 'Prisma.' + executeInContext.name, message: 'Top-level: ALS context reset', }); } } function createPrismaProxy(target) { const _target = target; return new Proxy(_target, { get(_, prop, receiver) { // provide an undocumented escape hatch to access the root PrismaClient and start top-level transactions if (prop === '$root') { manager_1.Manager.logger.verbose?.({ context: 'Prisma.' + createPrismaProxy.name, message: '[Proxy] Accessing root Prisma', }); return _target; } const maybeExistingTxClient = nestjs_cls_1.ClsServiceManager.getClsService().get(const_1.TX_CLIENT_KEY); const prisma = maybeExistingTxClient ?? _target; if (prop === '$transaction' && maybeExistingTxClient && typeof _target[prop] === 'function') { manager_1.Manager.logger.verbose?.({ context: 'Prisma.' + createPrismaProxy.name, message: '[Proxy] $transaction called on a txClient, continue nesting it', }); return function (...args) { // grab the callback of the native "prisma.$transaction(callback, options)" invocation and invoke it with the txClient from the ALS if (typeof args[0] === 'function') { return args[0](maybeExistingTxClient); } else if (args[0] instanceof Array) { manager_1.Manager.logger.warn({ context: 'Prisma.' + createPrismaProxy.name, message: 'Nested $transaction called with an array argument, it is probably works out of transaction', }); } else { throw new Error('prisma.$transaction called with a non-function argument'); } }; } return Reflect.get(prisma, prop, receiver); }, set(_, prop, newValue, receiver) { if (prop === '$transaction') { manager_1.Manager.logger.warn({ context: 'Prisma.' + createPrismaProxy.name, message: `Please don't spy on $transaction.`, }); return false; } const maybeExistingTxClient = nestjs_cls_1.ClsServiceManager.getClsService().get(const_1.TX_CLIENT_KEY); const prisma = maybeExistingTxClient ?? _target; return Reflect.set(prisma, prop, newValue, receiver); }, defineProperty(_, prop, attributes) { const maybeExistingTxClient = nestjs_cls_1.ClsServiceManager.getClsService().get(const_1.TX_CLIENT_KEY); const prisma = maybeExistingTxClient ?? _target; return Reflect.defineProperty(prisma, prop, attributes); }, }); }