@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
JavaScript
;
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);
},
});
}