From 64077dc2f588e7fc76085852c33714944524cc14 Mon Sep 17 00:00:00 2001 From: dennemark Date: Thu, 5 Dec 2024 15:09:24 +0100 Subject: [PATCH] Release 1.1.6 --- CHANGELOG.md | 6 ++ dist/index.d.mts | 11 ++++ dist/index.d.ts | 11 ++++ dist/index.js | 139 +++++++++++++++++++++++++++++------------------ dist/index.mjs | 139 +++++++++++++++++++++++++++++------------------ package.json | 2 +- 6 files changed, 201 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f046cfa..1fc33dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +## [1.1.6](https://github.com/dennemark/prisma-extension-casl/compare/1.1.5...1.1.6) (2024-12-05) + +### Bug Fixes + +* :bug: custom batching function to allow before and after queries ([233abda](https://github.com/dennemark/prisma-extension-casl/commit/233abdafbac3607761e822dd528d7b7733f4d72d)) + ## [1.1.5](https://github.com/dennemark/prisma-extension-casl/compare/1.1.4...1.1.5) (2024-12-05) ### Bug Fixes diff --git a/dist/index.d.mts b/dist/index.d.mts index c2a450c..cf7a37c 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts @@ -39,6 +39,10 @@ type PrismaExtensionCaslOptions = { beforeQuery?: (tx: Prisma.TransactionClient) => Promise; /** uses transaction to allow using client queries after actual query, if fails, whole query will be rolled back */ afterQuery?: (tx: Prisma.TransactionClient) => Promise; + /** max wait for batch transaction - default 30000 */ + txMaxWait?: number; + /** timeout for batch transaction - default 30000 */ + txTimeout?: number; }; type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findUnique' | 'findUniqueOrThrow' | 'aggregate' | 'count' | 'groupBy' | 'update' | 'updateMany' | 'delete' | 'deleteMany'; @@ -109,6 +113,13 @@ declare function useCaslAbilities(getAbilityFactory: () => AbilityBuilder Promise; /** uses transaction to allow using client queries after actual query, if fails, whole query will be rolled back */ afterQuery?: (tx: Prisma.TransactionClient) => Promise; + /** max wait for batch transaction - default 30000 */ + txMaxWait?: number; + /** timeout for batch transaction - default 30000 */ + txTimeout?: number; }; type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findUnique' | 'findUniqueOrThrow' | 'aggregate' | 'count' | 'groupBy' | 'update' | 'updateMany' | 'delete' | 'deleteMany'; @@ -109,6 +113,13 @@ declare function useCaslAbilities(getAbilityFactory: () => AbilityBuilder { - const transactionsToBatch = /* @__PURE__ */ new Set(); + let tickActive = false; + const batches = {}; const allOperations = (getAbilities) => ({ async $allOperations({ args, query, model, operation, ...rest }) { const fluentModel = getFluentModel(model, rest); const [fluentRelationModel, fluentRelationField] = (fluentModel !== model ? Object.entries(relationFieldsByModel[model]).find(([k2, v4]) => v4.type === fluentModel) : void 0) ?? [void 0, void 0]; - const transaction = rest.__internalParams.transaction; + const __internalParams = rest.__internalParams; + const transaction = __internalParams.transaction; const debug = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") && args.debugCasl; const debugAllErrors = args.debugCasl; delete args.debugCasl; @@ -1552,70 +1556,99 @@ function useCaslAbilities(getAbilityFactory, opts) { if (transaction && transaction.kind === "batch") { throw new Error("Sequential transactions are not supported in prisma-extension-casl."); } - const transactionQuery = async (txClient) => { - if (opts?.beforeQuery) { - await opts.beforeQuery(txClient); - } - if (operationAbility.action === "update" || operationAbility.action === "create" || operation === "deleteMany") { - const getMany = operation === "deleteMany" || operation === "updateMany"; - const manyResult = getMany ? await txClient[model].findMany(caslQuery.args.where ? { where: caslQuery.args.where } : void 0).then((res) => { - return operation === "updateMany" ? res.map((r2) => ({ ...caslQuery.args.data, id: r2.id })) : res; - }) : []; - const op = operation === "createMany" ? "createManyAndReturn" : operation; - return txClient[model][op](caslQuery.args).then(async (result) => { - if (opts?.afterQuery) { - await opts.afterQuery(txClient); - } - const filteredResult = cleanupResults(getMany ? manyResult : result); - const results = operation === "createMany" ? { count: result.length } : getMany ? { count: manyResult.length } : filteredResult; - return results; - }); - } else { - return txClient[model][operation](caslQuery.args).then(async (result) => { - if (opts?.afterQuery) { - await opts.afterQuery(txClient); - } - const fluentField = getFluentField(rest); - if (fluentField) { - return cleanupResults(result?.[fluentField]); - } - return cleanupResults(result); - }); - } - }; - if (transaction && transaction.kind === "itx") { - return transactionQuery(client._createItxClient(transaction)); + const hash = transaction?.id ?? "batch"; + if (!batches[hash]) { + batches[hash] = []; + } + if (!tickActive) { + tickActive = true; + process.nextTick(() => { + dispatchBatches(transaction); + tickActive = false; + }); + } + const batchQuery = (model2, action, args2, callback) => new Promise((resolve, reject) => { + batches[hash].push({ + params: __internalParams, + model: model2, + action, + args: args2, + reject, + resolve, + callback + }); + }); + if (operationAbility.action === "update" || operationAbility.action === "create" || operation === "deleteMany") { + const getMany = operation === "deleteMany" || operation === "updateMany"; + const op = operation === "createMany" ? "createManyAndReturn" : operation; + return batchQuery(model, op, caslQuery.args, async (result) => { + const filteredResult = cleanupResults(result); + const results = operation === "createMany" || operation === "deleteMany" || operation === "updateMany" ? { count: result.length } : filteredResult; + return results; + }); } else { - return client.$transaction(async (tx) => { - const transactionId = tx[Symbol.for("prisma.client.transaction.id")].toString(); - transactionsToBatch.add(transactionId); - return transactionQuery(tx).finally(() => { - transactionsToBatch.delete(transactionId); - }); - }, { - //https://github.com/prisma/prisma/issues/20015 - maxWait: 1e4 - // default prisma pool timeout. would be better to get it from client + return batchQuery(model, operation, caslQuery.args, async (result) => { + const fluentField = getFluentField(rest); + if (fluentField) { + return cleanupResults(result?.[fluentField]); + } + return cleanupResults(result); }); } } }); client._requestHandler.dataloader.options.batchBy = (request) => { const batchId = getBatchId(request.protocolQuery); - if (request.transaction?.id && (!transactionsToBatch.has(request.transaction.id.toString()) && batchId)) { - return `transaction-${request.transaction.id}`; + if (request.transaction?.id) { + return `transaction-${request.transaction.id}${batchId ? `-${batchId}` : ""}`; } return batchId; }; + const dispatchBatches = (transaction) => { + for (const [key, batch] of Object.entries(batches)) { + delete batches[key]; + const runBatchTransaction = async (tx) => { + if (opts?.beforeQuery) { + await opts.beforeQuery(tx); + } + const results = await Promise.all( + batch.map((request) => { + return tx[request.model][request.action](request.args).then((res) => request.callback(res)).catch((e4) => { + throw e4; + }); + }) + ); + if (opts?.afterQuery) { + await opts?.afterQuery(tx); + } + return results; + }; + new Promise((resolve, reject) => { + if (transaction && transaction.kind === "itx") { + runBatchTransaction(client._createItxClient(transaction)).then(resolve).catch(reject); + } else { + client.$transaction(async (tx) => { + return runBatchTransaction(tx); + }, { + maxWait: txMaxWait, + timeout: txTimeout + }).then(resolve).catch(reject); + } + }).then((results) => { + results.forEach((result, index) => { + batch[index].resolve(result); + }); + }).catch((e4) => { + for (const request of batch) { + request.reject(e4); + } + delete batches[key]; + }); + } + }; return client.$extends({ name: "prisma-extension-casl", client: { - // https://github.com/prisma/prisma/issues/20678 - // $transaction(...props: Parameters<(typeof client)['$transaction']>): ReturnType<(typeof client)['$transaction']> { - // return transactionStore.run({ alreadyInTransaction: true }, () => { - // return client.$transaction(...props); - // }); - // }, $casl(extendFactory) { return client.$extends({ query: { diff --git a/dist/index.mjs b/dist/index.mjs index ee6592d..f1a335a 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1449,13 +1449,17 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat // src/index.ts function useCaslAbilities(getAbilityFactory, opts) { + const txMaxWait = opts?.txMaxWait ?? 3e4; + const txTimeout = opts?.txTimeout ?? 3e4; return Prisma2.defineExtension((client) => { - const transactionsToBatch = /* @__PURE__ */ new Set(); + let tickActive = false; + const batches = {}; const allOperations = (getAbilities) => ({ async $allOperations({ args, query, model, operation, ...rest }) { const fluentModel = getFluentModel(model, rest); const [fluentRelationModel, fluentRelationField] = (fluentModel !== model ? Object.entries(relationFieldsByModel[model]).find(([k2, v4]) => v4.type === fluentModel) : void 0) ?? [void 0, void 0]; - const transaction = rest.__internalParams.transaction; + const __internalParams = rest.__internalParams; + const transaction = __internalParams.transaction; const debug = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") && args.debugCasl; const debugAllErrors = args.debugCasl; delete args.debugCasl; @@ -1526,70 +1530,99 @@ function useCaslAbilities(getAbilityFactory, opts) { if (transaction && transaction.kind === "batch") { throw new Error("Sequential transactions are not supported in prisma-extension-casl."); } - const transactionQuery = async (txClient) => { - if (opts?.beforeQuery) { - await opts.beforeQuery(txClient); - } - if (operationAbility.action === "update" || operationAbility.action === "create" || operation === "deleteMany") { - const getMany = operation === "deleteMany" || operation === "updateMany"; - const manyResult = getMany ? await txClient[model].findMany(caslQuery.args.where ? { where: caslQuery.args.where } : void 0).then((res) => { - return operation === "updateMany" ? res.map((r2) => ({ ...caslQuery.args.data, id: r2.id })) : res; - }) : []; - const op = operation === "createMany" ? "createManyAndReturn" : operation; - return txClient[model][op](caslQuery.args).then(async (result) => { - if (opts?.afterQuery) { - await opts.afterQuery(txClient); - } - const filteredResult = cleanupResults(getMany ? manyResult : result); - const results = operation === "createMany" ? { count: result.length } : getMany ? { count: manyResult.length } : filteredResult; - return results; - }); - } else { - return txClient[model][operation](caslQuery.args).then(async (result) => { - if (opts?.afterQuery) { - await opts.afterQuery(txClient); - } - const fluentField = getFluentField(rest); - if (fluentField) { - return cleanupResults(result?.[fluentField]); - } - return cleanupResults(result); - }); - } - }; - if (transaction && transaction.kind === "itx") { - return transactionQuery(client._createItxClient(transaction)); + const hash = transaction?.id ?? "batch"; + if (!batches[hash]) { + batches[hash] = []; + } + if (!tickActive) { + tickActive = true; + process.nextTick(() => { + dispatchBatches(transaction); + tickActive = false; + }); + } + const batchQuery = (model2, action, args2, callback) => new Promise((resolve, reject) => { + batches[hash].push({ + params: __internalParams, + model: model2, + action, + args: args2, + reject, + resolve, + callback + }); + }); + if (operationAbility.action === "update" || operationAbility.action === "create" || operation === "deleteMany") { + const getMany = operation === "deleteMany" || operation === "updateMany"; + const op = operation === "createMany" ? "createManyAndReturn" : operation; + return batchQuery(model, op, caslQuery.args, async (result) => { + const filteredResult = cleanupResults(result); + const results = operation === "createMany" || operation === "deleteMany" || operation === "updateMany" ? { count: result.length } : filteredResult; + return results; + }); } else { - return client.$transaction(async (tx) => { - const transactionId = tx[Symbol.for("prisma.client.transaction.id")].toString(); - transactionsToBatch.add(transactionId); - return transactionQuery(tx).finally(() => { - transactionsToBatch.delete(transactionId); - }); - }, { - //https://github.com/prisma/prisma/issues/20015 - maxWait: 1e4 - // default prisma pool timeout. would be better to get it from client + return batchQuery(model, operation, caslQuery.args, async (result) => { + const fluentField = getFluentField(rest); + if (fluentField) { + return cleanupResults(result?.[fluentField]); + } + return cleanupResults(result); }); } } }); client._requestHandler.dataloader.options.batchBy = (request) => { const batchId = getBatchId(request.protocolQuery); - if (request.transaction?.id && (!transactionsToBatch.has(request.transaction.id.toString()) && batchId)) { - return `transaction-${request.transaction.id}`; + if (request.transaction?.id) { + return `transaction-${request.transaction.id}${batchId ? `-${batchId}` : ""}`; } return batchId; }; + const dispatchBatches = (transaction) => { + for (const [key, batch] of Object.entries(batches)) { + delete batches[key]; + const runBatchTransaction = async (tx) => { + if (opts?.beforeQuery) { + await opts.beforeQuery(tx); + } + const results = await Promise.all( + batch.map((request) => { + return tx[request.model][request.action](request.args).then((res) => request.callback(res)).catch((e4) => { + throw e4; + }); + }) + ); + if (opts?.afterQuery) { + await opts?.afterQuery(tx); + } + return results; + }; + new Promise((resolve, reject) => { + if (transaction && transaction.kind === "itx") { + runBatchTransaction(client._createItxClient(transaction)).then(resolve).catch(reject); + } else { + client.$transaction(async (tx) => { + return runBatchTransaction(tx); + }, { + maxWait: txMaxWait, + timeout: txTimeout + }).then(resolve).catch(reject); + } + }).then((results) => { + results.forEach((result, index) => { + batch[index].resolve(result); + }); + }).catch((e4) => { + for (const request of batch) { + request.reject(e4); + } + delete batches[key]; + }); + } + }; return client.$extends({ name: "prisma-extension-casl", client: { - // https://github.com/prisma/prisma/issues/20678 - // $transaction(...props: Parameters<(typeof client)['$transaction']>): ReturnType<(typeof client)['$transaction']> { - // return transactionStore.run({ alreadyInTransaction: true }, () => { - // return client.$transaction(...props); - // }); - // }, $casl(extendFactory) { return client.$extends({ query: { diff --git a/package.json b/package.json index 14a54b5..53b2809 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-extension-casl", - "version": "1.1.5", + "version": "1.1.6", "description": "Enforce casl abilities on prisma client ", "main": "dist/index.js", "types": "dist/index.d.ts",