From f666f878f32bfe920de5d8fe475912a8edce1621 Mon Sep 17 00:00:00 2001 From: dennemark Date: Wed, 13 Nov 2024 18:08:15 +0100 Subject: [PATCH] Release 1.1.0 --- CHANGELOG.md | 6 ++++ dist/index.d.mts | 4 +++ dist/index.d.ts | 4 +++ dist/index.js | 92 +++++++++++++++++++++++++++++++++++------------- dist/index.mjs | 92 +++++++++++++++++++++++++++++++++++------------- package.json | 2 +- 6 files changed, 149 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef0ce2..f08597f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +## [1.1.0](https://github.com/dennemark/prisma-extension-casl/compare/1.0.1...1.1.0) (2024-11-13) + +### Features + +* :sparkles: add before and afterQuery functions ([7433bee](https://github.com/dennemark/prisma-extension-casl/commit/7433bee16b76eb6c8c506c48ed2792d03e2ddc64)) + ## [1.0.1](https://github.com/dennemark/prisma-extension-casl/compare/1.0.0...1.0.1) (2024-10-29) ### Bug Fixes diff --git a/dist/index.d.mts b/dist/index.d.mts index e064667..4f5084f 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts @@ -35,6 +35,10 @@ type PrismaExtensionCaslOptions = { * that should be returned if permissionField is used. */ addPermissionActions?: string[]; + /** uses transaction to allow using client queries before actual query, if fails, whole query will be rolled back */ + 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; }; type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findUnique' | 'findUniqueOrThrow' | 'aggregate' | 'count' | 'groupBy' | 'update' | 'updateMany' | 'delete' | 'deleteMany'; diff --git a/dist/index.d.ts b/dist/index.d.ts index e064667..4f5084f 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -35,6 +35,10 @@ type PrismaExtensionCaslOptions = { * that should be returned if permissionField is used. */ addPermissionActions?: string[]; + /** uses transaction to allow using client queries before actual query, if fails, whole query will be rolled back */ + 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; }; type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findUnique' | 'findUniqueOrThrow' | 'aggregate' | 'count' | 'groupBy' | 'update' | 'updateMany' | 'delete' | 'deleteMany'; diff --git a/dist/index.js b/dist/index.js index f6a00d0..6e1381a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -915,6 +915,14 @@ function getSubject(model, obj) { const subjectFields = [...modelFields, ...Object.keys(relationFieldsByModel[model])]; return R2(model, pick(obj, subjectFields)); } +function getFluentField(data) { + const dataPath = data?.__internalParams?.dataPath; + if (dataPath?.length > 0) { + return dataPath[dataPath.length - 1]; + } else { + return void 0; + } +} function getFluentModel(startModel, data) { const dataPath = data?.__internalParams?.dataPath; if (dataPath?.length > 0) { @@ -996,7 +1004,7 @@ if (!Set.prototype.isDisjointFrom) { } // src/applyDataQuery.ts -function applyDataQuery(abilities, args, action, model, creationTree) { +function applyDataQuery(abilities, args, operation, action, model, creationTree) { const tree = creationTree ? creationTree : { action, model, children: {}, mutation: [] }; const permittedFields = getPermittedFields(abilities, action, model); const mutationArgs = []; @@ -1038,7 +1046,7 @@ function applyDataQuery(abilities, args, action, model, creationTree) { const relationModelId = propertyFieldsByModel[model][field]; if (relationModelId && mutation[field] !== null) { const fieldId = relationFieldsByModel[model][relationModelId].relationToFields?.[0]; - if (fieldId) { + if (fieldId && operation !== "createMany" && operation !== "createManyAndReturn") { mutation[relationModelId] = { connect: { [fieldId]: mutation[field] } }; delete mutation[field]; } @@ -1058,7 +1066,7 @@ function applyDataQuery(abilities, args, action, model, creationTree) { const isConnection = nestedAction === "connect" || nestedAction === "disconnect"; tree.children[field] = { action: mutationAction, model: relationModel.type, children: {}, mutation: [] }; if (nestedAction !== "disconnect" && nestedArgs !== true) { - const dataQuery = applyDataQuery(abilities, nestedArgs, mutationAction, relationModel.type, tree.children[field]); + const dataQuery = applyDataQuery(abilities, nestedArgs, operation, mutationAction, relationModel.type, tree.children[field]); mutation[field][nestedAction] = dataQuery.args; if (isConnection) { const accessibleQuery = m5(abilities, mutationAction)[relationModel.type]; @@ -1314,7 +1322,7 @@ function applyCaslToQuery(operation, args, abilities, model, queryAllRuleRelatio if (operationAbility.whereQuery && !args.where) { args.where = {}; } - const { args: dataArgs, creationTree: dataCreationTree } = applyDataQuery(abilities, args, operationAbility.action, model); + const { args: dataArgs, creationTree: dataCreationTree } = applyDataQuery(abilities, args, operation, operationAbility.action, model); creationTree = dataCreationTree; args = dataArgs; if (operation === "updateMany") { @@ -1378,8 +1386,18 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat } if (creationTree?.action === "create") { try { - if (!abilities.can("create", getSubject(model, entry))) { - throw new Error(""); + if (creationTree.mutation?.length) { + creationTree.mutation.forEach(({ where }) => { + if (isSubset(where, entry)) { + if (!abilities.can("create", getSubject(model, entry))) { + throw new Error(""); + } + } + }); + } else { + if (!abilities.can("create", getSubject(model, entry))) { + throw new Error(""); + } } } catch (e4) { throw new Error(`It's not allowed to create on ${model} ` + e4); @@ -1387,8 +1405,8 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat } if (creationTree?.action === "update" && creationTree.mutation.length > 0) { creationTree.mutation.forEach(({ fields, where }) => { - fields.forEach((field) => { - if (isSubset(where, entry)) { + if (isSubset(where, entry)) { + fields.forEach((field) => { try { if (!abilities.can("update", getSubject(model, entry), field)) { throw new Error(field); @@ -1396,8 +1414,8 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat } catch (e4) { throw new Error(`It's not allowed to update ${field} on ${model} ` + e4); } - } - }); + }); + } }); } const permittedFields = getPermittedFields(abilities, "read", model, entry); @@ -1442,7 +1460,6 @@ function useCaslAbilities(getAbilityFactory, opts) { return import_client2.Prisma.defineExtension((client) => { const allOperations = (getAbilities) => ({ async $allOperations({ args, query, model, operation, ...rest }) { - const op = operation === "createMany" ? "createManyAndReturn" : operation; 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; @@ -1460,7 +1477,7 @@ function useCaslAbilities(getAbilityFactory, opts) { perf?.clearMarks("prisma-casl-extension-2"); perf?.clearMarks("prisma-casl-extension-3"); perf?.clearMarks("prisma-casl-extension-4"); - if (!(op in caslOperationDict)) { + if (!(operation in caslOperationDict)) { return query(args); } perf?.mark("prisma-casl-extension-0"); @@ -1494,7 +1511,7 @@ function useCaslAbilities(getAbilityFactory, opts) { if (fluentRelationModel && caslQuery.mask) { caslQuery.mask = fluentRelationModel && fluentRelationModel in caslQuery.mask ? caslQuery.mask[fluentRelationModel] : {}; } - const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, op, opts); + const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, operation, opts); if (perf) { perf.mark("prisma-casl-extension-4"); logger?.log( @@ -1509,24 +1526,49 @@ function useCaslAbilities(getAbilityFactory, opts) { }) ); } - return operation === "createMany" ? { count: filteredResult.length } : filteredResult; + return filteredResult; }; const operationAbility = caslOperationDict[operation]; - if (operationAbility.action === "update" || operationAbility.action === "create") { - if (transaction) { - if (transaction.kind === "itx") { - const transactionClient = client._createItxClient(transaction); - return transactionClient[model][op](caslQuery.args).then(cleanupResults); - } else if (transaction.kind === "batch") { - throw new Error("Sequential transactions are not supported in prisma-extension-casl."); - } + 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 client.$transaction(async (tx) => { - return tx[model][op](caslQuery.args).then(cleanupResults); + 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)); } else { - return query(caslQuery.args).then(cleanupResults); + return client.$transaction(async (tx) => { + return transactionQuery(tx); + }); } } }); diff --git a/dist/index.mjs b/dist/index.mjs index 4abb97a..cb687ff 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -890,6 +890,14 @@ function getSubject(model, obj) { const subjectFields = [...modelFields, ...Object.keys(relationFieldsByModel[model])]; return R2(model, pick(obj, subjectFields)); } +function getFluentField(data) { + const dataPath = data?.__internalParams?.dataPath; + if (dataPath?.length > 0) { + return dataPath[dataPath.length - 1]; + } else { + return void 0; + } +} function getFluentModel(startModel, data) { const dataPath = data?.__internalParams?.dataPath; if (dataPath?.length > 0) { @@ -971,7 +979,7 @@ if (!Set.prototype.isDisjointFrom) { } // src/applyDataQuery.ts -function applyDataQuery(abilities, args, action, model, creationTree) { +function applyDataQuery(abilities, args, operation, action, model, creationTree) { const tree = creationTree ? creationTree : { action, model, children: {}, mutation: [] }; const permittedFields = getPermittedFields(abilities, action, model); const mutationArgs = []; @@ -1013,7 +1021,7 @@ function applyDataQuery(abilities, args, action, model, creationTree) { const relationModelId = propertyFieldsByModel[model][field]; if (relationModelId && mutation[field] !== null) { const fieldId = relationFieldsByModel[model][relationModelId].relationToFields?.[0]; - if (fieldId) { + if (fieldId && operation !== "createMany" && operation !== "createManyAndReturn") { mutation[relationModelId] = { connect: { [fieldId]: mutation[field] } }; delete mutation[field]; } @@ -1033,7 +1041,7 @@ function applyDataQuery(abilities, args, action, model, creationTree) { const isConnection = nestedAction === "connect" || nestedAction === "disconnect"; tree.children[field] = { action: mutationAction, model: relationModel.type, children: {}, mutation: [] }; if (nestedAction !== "disconnect" && nestedArgs !== true) { - const dataQuery = applyDataQuery(abilities, nestedArgs, mutationAction, relationModel.type, tree.children[field]); + const dataQuery = applyDataQuery(abilities, nestedArgs, operation, mutationAction, relationModel.type, tree.children[field]); mutation[field][nestedAction] = dataQuery.args; if (isConnection) { const accessibleQuery = m5(abilities, mutationAction)[relationModel.type]; @@ -1289,7 +1297,7 @@ function applyCaslToQuery(operation, args, abilities, model, queryAllRuleRelatio if (operationAbility.whereQuery && !args.where) { args.where = {}; } - const { args: dataArgs, creationTree: dataCreationTree } = applyDataQuery(abilities, args, operationAbility.action, model); + const { args: dataArgs, creationTree: dataCreationTree } = applyDataQuery(abilities, args, operation, operationAbility.action, model); creationTree = dataCreationTree; args = dataArgs; if (operation === "updateMany") { @@ -1353,8 +1361,18 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat } if (creationTree?.action === "create") { try { - if (!abilities.can("create", getSubject(model, entry))) { - throw new Error(""); + if (creationTree.mutation?.length) { + creationTree.mutation.forEach(({ where }) => { + if (isSubset(where, entry)) { + if (!abilities.can("create", getSubject(model, entry))) { + throw new Error(""); + } + } + }); + } else { + if (!abilities.can("create", getSubject(model, entry))) { + throw new Error(""); + } } } catch (e4) { throw new Error(`It's not allowed to create on ${model} ` + e4); @@ -1362,8 +1380,8 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat } if (creationTree?.action === "update" && creationTree.mutation.length > 0) { creationTree.mutation.forEach(({ fields, where }) => { - fields.forEach((field) => { - if (isSubset(where, entry)) { + if (isSubset(where, entry)) { + fields.forEach((field) => { try { if (!abilities.can("update", getSubject(model, entry), field)) { throw new Error(field); @@ -1371,8 +1389,8 @@ function filterQueryResults(result, mask, creationTree, abilities, model, operat } catch (e4) { throw new Error(`It's not allowed to update ${field} on ${model} ` + e4); } - } - }); + }); + } }); } const permittedFields = getPermittedFields(abilities, "read", model, entry); @@ -1417,7 +1435,6 @@ function useCaslAbilities(getAbilityFactory, opts) { return Prisma2.defineExtension((client) => { const allOperations = (getAbilities) => ({ async $allOperations({ args, query, model, operation, ...rest }) { - const op = operation === "createMany" ? "createManyAndReturn" : operation; 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; @@ -1435,7 +1452,7 @@ function useCaslAbilities(getAbilityFactory, opts) { perf?.clearMarks("prisma-casl-extension-2"); perf?.clearMarks("prisma-casl-extension-3"); perf?.clearMarks("prisma-casl-extension-4"); - if (!(op in caslOperationDict)) { + if (!(operation in caslOperationDict)) { return query(args); } perf?.mark("prisma-casl-extension-0"); @@ -1469,7 +1486,7 @@ function useCaslAbilities(getAbilityFactory, opts) { if (fluentRelationModel && caslQuery.mask) { caslQuery.mask = fluentRelationModel && fluentRelationModel in caslQuery.mask ? caslQuery.mask[fluentRelationModel] : {}; } - const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, op, opts); + const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, operation, opts); if (perf) { perf.mark("prisma-casl-extension-4"); logger?.log( @@ -1484,24 +1501,49 @@ function useCaslAbilities(getAbilityFactory, opts) { }) ); } - return operation === "createMany" ? { count: filteredResult.length } : filteredResult; + return filteredResult; }; const operationAbility = caslOperationDict[operation]; - if (operationAbility.action === "update" || operationAbility.action === "create") { - if (transaction) { - if (transaction.kind === "itx") { - const transactionClient = client._createItxClient(transaction); - return transactionClient[model][op](caslQuery.args).then(cleanupResults); - } else if (transaction.kind === "batch") { - throw new Error("Sequential transactions are not supported in prisma-extension-casl."); - } + 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 client.$transaction(async (tx) => { - return tx[model][op](caslQuery.args).then(cleanupResults); + 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)); } else { - return query(caslQuery.args).then(cleanupResults); + return client.$transaction(async (tx) => { + return transactionQuery(tx); + }); } } }); diff --git a/package.json b/package.json index 0bdf7d3..6f74b90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-extension-casl", - "version": "1.0.1", + "version": "1.1.0", "description": "Enforce casl abilities on prisma client ", "main": "dist/index.js", "types": "dist/index.d.ts",