diff --git a/dist/index.d.mts b/dist/index.d.mts index 55a9b45..5794142 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts @@ -1,27 +1,22 @@ import * as _prisma_client_runtime_library from '@prisma/client/runtime/library'; -import { Prisma } from '@prisma/client'; import { PureAbility, AbilityTuple } from '@casl/ability'; import { PrismaQuery } from '@casl/prisma'; -type DefaultCaslAction = "create" | "read" | "update" | "delete"; -type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findUnique' | 'findUniqueOrThrow' | 'aggregate' | 'count' | 'groupBy' | 'update' | 'updateMany' | 'delete' | 'deleteMany'; -declare const caslOperationDict: Record; +/** + * enrich a prisma client to check for CASL abilities even in nested queries + * + * `client.$extends(useCaslAbilities(build()))` + * + * https://casl.js.org/v6/en/package/casl-prisma + * + * + * @param abilities CASL prisma abilities + * @returns enriched prisma client + */ declare const useCaslAbilities: (abilities: PureAbility) => (client: any) => { $extends: { extArgs: _prisma_client_runtime_library.InternalArgs; }; }; -declare function applyCaslToQuery(operation: any, args: any, abilities: PureAbility, model: Prisma.ModelName): any; -declare function applyDataQuery(abilities: PureAbility, args: any, action: string, model: string): any; -declare function applyWhereQuery(abilities: PureAbility, args: any, action: string, model: string, relation?: string): any; -declare const applySelectPermittedFields: (abilities: PureAbility, args: any, model: string) => any; -declare const applyIncludeSelectQuery: (abilities: PureAbility, args: any, model: string) => any; -declare function capitalizeFirstLetter(string: string): string; -declare function isSubset(obj1: any, obj2: any): boolean; -export { type PrismaCaslOperation, applyCaslToQuery, applyDataQuery, applyIncludeSelectQuery, applySelectPermittedFields, applyWhereQuery, capitalizeFirstLetter, caslOperationDict, isSubset, useCaslAbilities }; +export { useCaslAbilities }; diff --git a/dist/index.d.ts b/dist/index.d.ts index 55a9b45..5794142 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,27 +1,22 @@ import * as _prisma_client_runtime_library from '@prisma/client/runtime/library'; -import { Prisma } from '@prisma/client'; import { PureAbility, AbilityTuple } from '@casl/ability'; import { PrismaQuery } from '@casl/prisma'; -type DefaultCaslAction = "create" | "read" | "update" | "delete"; -type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findUnique' | 'findUniqueOrThrow' | 'aggregate' | 'count' | 'groupBy' | 'update' | 'updateMany' | 'delete' | 'deleteMany'; -declare const caslOperationDict: Record; +/** + * enrich a prisma client to check for CASL abilities even in nested queries + * + * `client.$extends(useCaslAbilities(build()))` + * + * https://casl.js.org/v6/en/package/casl-prisma + * + * + * @param abilities CASL prisma abilities + * @returns enriched prisma client + */ declare const useCaslAbilities: (abilities: PureAbility) => (client: any) => { $extends: { extArgs: _prisma_client_runtime_library.InternalArgs; }; }; -declare function applyCaslToQuery(operation: any, args: any, abilities: PureAbility, model: Prisma.ModelName): any; -declare function applyDataQuery(abilities: PureAbility, args: any, action: string, model: string): any; -declare function applyWhereQuery(abilities: PureAbility, args: any, action: string, model: string, relation?: string): any; -declare const applySelectPermittedFields: (abilities: PureAbility, args: any, model: string) => any; -declare const applyIncludeSelectQuery: (abilities: PureAbility, args: any, model: string) => any; -declare function capitalizeFirstLetter(string: string): string; -declare function isSubset(obj1: any, obj2: any): boolean; -export { type PrismaCaslOperation, applyCaslToQuery, applyDataQuery, applyIncludeSelectQuery, applySelectPermittedFields, applyWhereQuery, capitalizeFirstLetter, caslOperationDict, isSubset, useCaslAbilities }; +export { useCaslAbilities }; diff --git a/dist/index.js b/dist/index.js index 7900ed4..27c345c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -20,18 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru // src/index.ts var src_exports = {}; __export(src_exports, { - applyCaslToQuery: () => applyCaslToQuery, - applyDataQuery: () => applyDataQuery, - applyIncludeSelectQuery: () => applyIncludeSelectQuery, - applySelectPermittedFields: () => applySelectPermittedFields, - applyWhereQuery: () => applyWhereQuery, - capitalizeFirstLetter: () => capitalizeFirstLetter, - caslOperationDict: () => caslOperationDict, - isSubset: () => isSubset, useCaslAbilities: () => useCaslAbilities }); module.exports = __toCommonJS(src_exports); -var import_client = require("@prisma/client"); +var import_client2 = require("@prisma/client"); // node_modules/.pnpm/@ucast+core@1.10.2/node_modules/@ucast/core/dist/es6m/index.mjs var t = class { @@ -349,42 +341,6 @@ function p4(e4, c5, f4) { var a3 = p4(z, M); var u4 = p4(["$and", "$or"].reduce((o3, t3) => (o3[t3] = Object.assign({}, o3[t3], { type: "field" }), o3), Object.assign({}, z, { $nor: Object.assign({}, z.$nor, { type: "field", parse: O.compound }) })), M, { forPrimitives: true }); -// node_modules/.pnpm/@casl+ability@6.7.1/node_modules/@casl/ability/dist/es6m/extra/index.mjs -function c4(t3, n3, r2, e4) { - const o3 = t3.detectSubjectType(r2); - const i4 = t3.possibleRulesFor(n3, o3); - const s3 = /* @__PURE__ */ new Set(); - const u5 = s3.delete.bind(s3); - const c5 = s3.add.bind(s3); - let f4 = i4.length; - while (f4--) { - const t4 = i4[f4]; - if (t4.matchesConditions(r2)) { - const n4 = t4.inverted ? u5 : c5; - e4.fieldsFrom(t4).forEach(n4); - } - } - return Array.from(s3); -} -function h4(t3, n3, r2, e4) { - const o3 = {}; - const i4 = t3.rulesFor(n3, r2); - for (let t4 = 0; t4 < i4.length; t4++) { - const n4 = i4[t4]; - const r3 = n4.inverted ? "$and" : "$or"; - if (!n4.conditions) if (n4.inverted) break; - else { - delete o3[r3]; - return o3; - } - else { - o3[r3] = o3[r3] || []; - o3[r3].push(e4(n4)); - } - } - return o3.$or ? o3 : null; -} - // node_modules/.pnpm/@casl+ability@6.7.1/node_modules/@casl/ability/dist/es6m/index.mjs function O4(t3) { return Array.isArray(t3) ? t3 : [t3]; @@ -671,6 +627,42 @@ var ForbiddenError = class extends yt { ForbiddenError.P = dt; var pt = Object.freeze({ __proto__: null }); +// node_modules/.pnpm/@casl+ability@6.7.1/node_modules/@casl/ability/dist/es6m/extra/index.mjs +function c4(t3, n3, r2, e4) { + const o3 = t3.detectSubjectType(r2); + const i4 = t3.possibleRulesFor(n3, o3); + const s3 = /* @__PURE__ */ new Set(); + const u5 = s3.delete.bind(s3); + const c5 = s3.add.bind(s3); + let f4 = i4.length; + while (f4--) { + const t4 = i4[f4]; + if (t4.matchesConditions(r2)) { + const n4 = t4.inverted ? u5 : c5; + e4.fieldsFrom(t4).forEach(n4); + } + } + return Array.from(s3); +} +function h4(t3, n3, r2, e4) { + const o3 = {}; + const i4 = t3.rulesFor(n3, r2); + for (let t4 = 0; t4 < i4.length; t4++) { + const n4 = i4[t4]; + const r3 = n4.inverted ? "$and" : "$or"; + if (!n4.conditions) if (n4.inverted) break; + else { + delete o3[r3]; + return o3; + } + else { + o3[r3] = o3[r3] || []; + o3[r3].push(e4(n4)); + } + } + return o3.$or ? o3 : null; +} + // node_modules/.pnpm/@casl+prisma@1.4.1_@casl+ability@6.7.1_@prisma+client@5.16.2_prisma@5.16.2_/node_modules/@casl/prisma/dist/es6m/runtime.mjs var v4 = class extends Error { static invalidArgument(t3, e4, r2) { @@ -818,18 +810,8 @@ function createAbilityFactory() { var e3 = createAbilityFactory(); var m5 = st2(); -// src/index.ts -var relationFieldsByModel = Object.fromEntries(import_client.Prisma.dmmf.datamodel.models.map((model) => { - const relationFields = Object.fromEntries(model.fields.filter((field) => field && field.kind === "object" && field.relationName).map((field) => [field.name, field])); - return [model.name, relationFields]; -})); -var propertyFieldsByModel = Object.fromEntries(import_client.Prisma.dmmf.datamodel.models.map((model) => { - const propertyFields = Object.fromEntries(model.fields.filter((field) => !(field && field.kind === "object" && field.relationName)).map((field) => { - const relation = Object.values(relationFieldsByModel[model.name]).find((value) => value.relationFromFields.includes(field.name)); - return [field.name, relation?.name]; - })); - return [model.name, propertyFields]; -})); +// src/helpers.ts +var import_client = require("@prisma/client"); var caslOperationDict = { create: { action: "create", dataQuery: true, whereQuery: false, includeSelectQuery: true }, createMany: { action: "create", dataQuery: true, whereQuery: false, includeSelectQuery: false }, @@ -861,85 +843,85 @@ var caslNestedOperationDict = { disconnect: "update", set: "update" }; -var useCaslAbilities = (abilities) => import_client.Prisma.defineExtension({ - name: "prisma-extension-casl", - query: { - $allModels: { - create({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - createMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - createManyAndReturn({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - upsert({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findFirst({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findFirstOrThrow({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findUnique({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findUniqueOrThrow({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - aggregate({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - count({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - groupBy({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - update({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - updateMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - delete({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - deleteMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); +var relationFieldsByModel = Object.fromEntries(import_client.Prisma.dmmf.datamodel.models.map((model) => { + const relationFields = Object.fromEntries(model.fields.filter((field) => field && field.kind === "object" && field.relationName).map((field) => [field.name, field])); + return [model.name, relationFields]; +})); +var propertyFieldsByModel = Object.fromEntries(import_client.Prisma.dmmf.datamodel.models.map((model) => { + const propertyFields = Object.fromEntries(model.fields.filter((field) => !(field && field.kind === "object" && field.relationName)).map((field) => { + const relation = Object.values(relationFieldsByModel[model.name]).find((value) => value.relationFromFields.includes(field.name)); + return [field.name, relation?.name]; + })); + return [model.name, propertyFields]; +})); +function getPermittedFields(abilities, args, action, model) { + let hasPermittedFields = false; + const omittedFieldsSet = /* @__PURE__ */ new Set(); + const permittedFields = c4(abilities, action, model, { + fieldsFrom: (rule) => { + if (rule.fields) { + if (rule.inverted) { + rule.fields.forEach((field) => omittedFieldsSet.add(field)); + } else { + hasPermittedFields = true; + } + if (rule.conditions) { + if (isSubset(rule.conditions, args.where)) { + return rule.fields; + } else { + } + } else { + return rule.fields; + } } - // async $allOperations({ args, query, model, operation }: { args: any, query: any, model: any, operation: any }) { - // if (!(operation in caslOperationDict)) { - // return query(args) - // } - // args = applyCaslToQuery(operation, args, abilities, model) - // return query(args) - // }, + return []; } + }); + if (hasPermittedFields === false && permittedFields.length === 0 && omittedFieldsSet.size > 0) { + permittedFields.push(...Object.keys(propertyFieldsByModel[model]).filter((field) => !omittedFieldsSet.has(field))); + hasPermittedFields = true; } -}); -function applyCaslToQuery(operation, args, abilities, model) { - const operationAbility = caslOperationDict[operation]; - if (args.caslAction) { - operationAbility.action = args.caslAction; - } - m5(abilities, operationAbility.action)[model]; - if (operationAbility.dataQuery && args.data) { - args.data = applyDataQuery(abilities, args.data, operationAbility.action, model); - } - if (operationAbility.whereQuery) { - args = applyWhereQuery(abilities, args, operationAbility.action, model); - } - if (operationAbility.includeSelectQuery) { - args = applyIncludeSelectQuery(abilities, args, model); + return hasPermittedFields ? permittedFields : void 0; +} +function isSubset(obj1, obj2) { + if (obj1 === obj2) return true; + if (typeof obj1 === "object" && typeof obj2 === "object") { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + for (const item1 of obj1) { + let found = false; + for (const item2 of obj2) { + if (isSubset(item1, item2)) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } else { + for (const key in obj1) { + if (!(key in obj2) || !isSubset(obj1[key], obj2[key])) { + return false; + } + } + return true; + } } - return args; + return false; +} + +// src/applyAccessibleQuery.ts +function applyAccessibleQuery(query, accessibleQuery) { + return { + ...query, + AND: [ + ...query.AND ?? [], + accessibleQuery + ] + }; } + +// src/applyDataQuery.ts function applyDataQuery(abilities, args, action, model) { const permittedFields = getPermittedFields(abilities, args, action, model); const accessibleQuery = m5(abilities, action)[model]; @@ -1001,6 +983,47 @@ function applyDataQuery(abilities, args, action, model) { }); return args; } + +// src/applySelectPermittedFields.ts +var applySelectPermittedFields = (abilities, args, model) => { + const permittedFields = getPermittedFields(abilities, args, "read", model); + if (permittedFields) { + if (args === true) { + args = { + select: {} + }; + } + if (args.include) { + args.select = { ...args.include }; + delete args.include; + } + if (!args.select) { + args.select = {}; + } + const queriedFields = args.select ? Object.keys(args.select) : []; + const remainingFields = queriedFields.filter((field) => { + const isRelation = relationFieldsByModel[model][field] ? true : false; + if (!permittedFields.includes(field) && !isRelation) { + delete args.select[field]; + return false; + } else if (isRelation) { + return false; + } + return true; + }); + if (remainingFields.length === 0) { + permittedFields.forEach((field) => { + args.select = { + ...args.select, + [field]: true + }; + }); + } + } + return args; +}; + +// src/applyWhereQuery.ts function applyWhereQuery(abilities, args, action, model, relation) { const prismaModel = model in relationFieldsByModel ? model : void 0; if (!prismaModel) { @@ -1040,43 +1063,8 @@ function applyWhereQuery(abilities, args, action, model, relation) { return applySelectPermittedFields(abilities, args, model); } } -var applySelectPermittedFields = (abilities, args, model) => { - const permittedFields = getPermittedFields(abilities, args, "read", model); - if (permittedFields) { - if (args === true) { - args = { - select: {} - }; - } - if (args.include) { - args.select = { ...args.include }; - delete args.include; - } - if (!args.select) { - args.select = {}; - } - const queriedFields = args.select ? Object.keys(args.select) : []; - const remainingFields = queriedFields.filter((field) => { - const isRelation = relationFieldsByModel[model][field] ? true : false; - if (!permittedFields.includes(field) && !isRelation) { - delete args.select[field]; - return false; - } else if (isRelation) { - return false; - } - return true; - }); - if (remainingFields.length === 0) { - permittedFields.forEach((field) => { - args.select = { - ...args.select, - [field]: true - }; - }); - } - } - return args; -}; + +// src/applyIncludeSelectQuery.ts var applyIncludeSelectQuery = (abilities, args, model) => { ; ["include", "select"].forEach((method) => { @@ -1099,82 +1087,90 @@ var applyIncludeSelectQuery = (abilities, args, model) => { }); return args; }; -function getPermittedFields(abilities, args, action, model) { - let hasPermittedFields = false; - const omittedFieldsSet = /* @__PURE__ */ new Set(); - const permittedFields = c4(abilities, action, model, { - fieldsFrom: (rule) => { - if (rule.fields) { - if (rule.inverted) { - rule.fields.forEach((field) => omittedFieldsSet.add(field)); - } else { - hasPermittedFields = true; - } - if (rule.conditions) { - if (isSubset(rule.conditions, args.where)) { - return rule.fields; - } else { - } - } else { - return rule.fields; - } - } - return []; - } - }); - if (hasPermittedFields === false && permittedFields.length === 0 && omittedFieldsSet.size > 0) { - permittedFields.push(...Object.keys(propertyFieldsByModel[model]).filter((field) => !omittedFieldsSet.has(field))); - hasPermittedFields = true; + +// src/applyCaslToQuery.ts +function applyCaslToQuery(operation, args, abilities, model) { + const operationAbility = caslOperationDict[operation]; + if (args.caslAction) { + operationAbility.action = args.caslAction; } - return hasPermittedFields ? permittedFields : void 0; -} -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); + m5(abilities, operationAbility.action)[model]; + if (operationAbility.dataQuery && args.data) { + args.data = applyDataQuery(abilities, args.data, operationAbility.action, model); + } + if (operationAbility.whereQuery) { + args = applyWhereQuery(abilities, args, operationAbility.action, model); + } + if (operationAbility.includeSelectQuery) { + args = applyIncludeSelectQuery(abilities, args, model); + } + return args; } -function isSubset(obj1, obj2) { - if (obj1 === obj2) return true; - if (typeof obj1 === "object" && typeof obj2 === "object") { - if (Array.isArray(obj1) && Array.isArray(obj2)) { - for (const item1 of obj1) { - let found = false; - for (const item2 of obj2) { - if (isSubset(item1, item2)) { - found = true; - break; - } - } - if (!found) return false; - } - return true; - } else { - for (const key in obj1) { - if (!(key in obj2) || !isSubset(obj1[key], obj2[key])) { - return false; - } + +// src/index.ts +var useCaslAbilities = (abilities) => import_client2.Prisma.defineExtension({ + name: "prisma-extension-casl", + query: { + $allModels: { + create({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + createMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + createManyAndReturn({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + upsert({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findFirst({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findFirstOrThrow({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findUnique({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findUniqueOrThrow({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + aggregate({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + count({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + groupBy({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + update({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + updateMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + delete({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + deleteMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); } - return true; + // async $allOperations({ args, query, model, operation }: { args: any, query: any, model: any, operation: any }) { + // if (!(operation in caslOperationDict)) { + // return query(args) + // } + // args = applyCaslToQuery(operation, args, abilities, model) + // return query(args) + // }, } } - return false; -} -function applyAccessibleQuery(clause, accessibleQuery) { - return { - ...clause, - AND: [ - ...clause.AND ?? [], - accessibleQuery - ] - }; -} +}); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { - applyCaslToQuery, - applyDataQuery, - applyIncludeSelectQuery, - applySelectPermittedFields, - applyWhereQuery, - capitalizeFirstLetter, - caslOperationDict, - isSubset, useCaslAbilities }); diff --git a/dist/index.mjs b/dist/index.mjs index e1010f0..a45a73c 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1,5 +1,5 @@ // src/index.ts -import { Prisma } from "@prisma/client"; +import { Prisma as Prisma2 } from "@prisma/client"; // node_modules/.pnpm/@ucast+core@1.10.2/node_modules/@ucast/core/dist/es6m/index.mjs var t = class { @@ -317,42 +317,6 @@ function p4(e4, c5, f4) { var a3 = p4(z, M); var u4 = p4(["$and", "$or"].reduce((o3, t3) => (o3[t3] = Object.assign({}, o3[t3], { type: "field" }), o3), Object.assign({}, z, { $nor: Object.assign({}, z.$nor, { type: "field", parse: O.compound }) })), M, { forPrimitives: true }); -// node_modules/.pnpm/@casl+ability@6.7.1/node_modules/@casl/ability/dist/es6m/extra/index.mjs -function c4(t3, n3, r2, e4) { - const o3 = t3.detectSubjectType(r2); - const i4 = t3.possibleRulesFor(n3, o3); - const s3 = /* @__PURE__ */ new Set(); - const u5 = s3.delete.bind(s3); - const c5 = s3.add.bind(s3); - let f4 = i4.length; - while (f4--) { - const t4 = i4[f4]; - if (t4.matchesConditions(r2)) { - const n4 = t4.inverted ? u5 : c5; - e4.fieldsFrom(t4).forEach(n4); - } - } - return Array.from(s3); -} -function h4(t3, n3, r2, e4) { - const o3 = {}; - const i4 = t3.rulesFor(n3, r2); - for (let t4 = 0; t4 < i4.length; t4++) { - const n4 = i4[t4]; - const r3 = n4.inverted ? "$and" : "$or"; - if (!n4.conditions) if (n4.inverted) break; - else { - delete o3[r3]; - return o3; - } - else { - o3[r3] = o3[r3] || []; - o3[r3].push(e4(n4)); - } - } - return o3.$or ? o3 : null; -} - // node_modules/.pnpm/@casl+ability@6.7.1/node_modules/@casl/ability/dist/es6m/index.mjs function O4(t3) { return Array.isArray(t3) ? t3 : [t3]; @@ -639,6 +603,42 @@ var ForbiddenError = class extends yt { ForbiddenError.P = dt; var pt = Object.freeze({ __proto__: null }); +// node_modules/.pnpm/@casl+ability@6.7.1/node_modules/@casl/ability/dist/es6m/extra/index.mjs +function c4(t3, n3, r2, e4) { + const o3 = t3.detectSubjectType(r2); + const i4 = t3.possibleRulesFor(n3, o3); + const s3 = /* @__PURE__ */ new Set(); + const u5 = s3.delete.bind(s3); + const c5 = s3.add.bind(s3); + let f4 = i4.length; + while (f4--) { + const t4 = i4[f4]; + if (t4.matchesConditions(r2)) { + const n4 = t4.inverted ? u5 : c5; + e4.fieldsFrom(t4).forEach(n4); + } + } + return Array.from(s3); +} +function h4(t3, n3, r2, e4) { + const o3 = {}; + const i4 = t3.rulesFor(n3, r2); + for (let t4 = 0; t4 < i4.length; t4++) { + const n4 = i4[t4]; + const r3 = n4.inverted ? "$and" : "$or"; + if (!n4.conditions) if (n4.inverted) break; + else { + delete o3[r3]; + return o3; + } + else { + o3[r3] = o3[r3] || []; + o3[r3].push(e4(n4)); + } + } + return o3.$or ? o3 : null; +} + // node_modules/.pnpm/@casl+prisma@1.4.1_@casl+ability@6.7.1_@prisma+client@5.16.2_prisma@5.16.2_/node_modules/@casl/prisma/dist/es6m/runtime.mjs var v4 = class extends Error { static invalidArgument(t3, e4, r2) { @@ -786,18 +786,8 @@ function createAbilityFactory() { var e3 = createAbilityFactory(); var m5 = st2(); -// src/index.ts -var relationFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model) => { - const relationFields = Object.fromEntries(model.fields.filter((field) => field && field.kind === "object" && field.relationName).map((field) => [field.name, field])); - return [model.name, relationFields]; -})); -var propertyFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model) => { - const propertyFields = Object.fromEntries(model.fields.filter((field) => !(field && field.kind === "object" && field.relationName)).map((field) => { - const relation = Object.values(relationFieldsByModel[model.name]).find((value) => value.relationFromFields.includes(field.name)); - return [field.name, relation?.name]; - })); - return [model.name, propertyFields]; -})); +// src/helpers.ts +import { Prisma } from "@prisma/client"; var caslOperationDict = { create: { action: "create", dataQuery: true, whereQuery: false, includeSelectQuery: true }, createMany: { action: "create", dataQuery: true, whereQuery: false, includeSelectQuery: false }, @@ -829,85 +819,85 @@ var caslNestedOperationDict = { disconnect: "update", set: "update" }; -var useCaslAbilities = (abilities) => Prisma.defineExtension({ - name: "prisma-extension-casl", - query: { - $allModels: { - create({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - createMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - createManyAndReturn({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - upsert({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findFirst({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findFirstOrThrow({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findUnique({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - findUniqueOrThrow({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - aggregate({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - count({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - groupBy({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - update({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - updateMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - delete({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); - }, - deleteMany({ args, query, model, operation }) { - return query(applyCaslToQuery(operation, args, abilities, model)); +var relationFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model) => { + const relationFields = Object.fromEntries(model.fields.filter((field) => field && field.kind === "object" && field.relationName).map((field) => [field.name, field])); + return [model.name, relationFields]; +})); +var propertyFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model) => { + const propertyFields = Object.fromEntries(model.fields.filter((field) => !(field && field.kind === "object" && field.relationName)).map((field) => { + const relation = Object.values(relationFieldsByModel[model.name]).find((value) => value.relationFromFields.includes(field.name)); + return [field.name, relation?.name]; + })); + return [model.name, propertyFields]; +})); +function getPermittedFields(abilities, args, action, model) { + let hasPermittedFields = false; + const omittedFieldsSet = /* @__PURE__ */ new Set(); + const permittedFields = c4(abilities, action, model, { + fieldsFrom: (rule) => { + if (rule.fields) { + if (rule.inverted) { + rule.fields.forEach((field) => omittedFieldsSet.add(field)); + } else { + hasPermittedFields = true; + } + if (rule.conditions) { + if (isSubset(rule.conditions, args.where)) { + return rule.fields; + } else { + } + } else { + return rule.fields; + } } - // async $allOperations({ args, query, model, operation }: { args: any, query: any, model: any, operation: any }) { - // if (!(operation in caslOperationDict)) { - // return query(args) - // } - // args = applyCaslToQuery(operation, args, abilities, model) - // return query(args) - // }, + return []; } + }); + if (hasPermittedFields === false && permittedFields.length === 0 && omittedFieldsSet.size > 0) { + permittedFields.push(...Object.keys(propertyFieldsByModel[model]).filter((field) => !omittedFieldsSet.has(field))); + hasPermittedFields = true; } -}); -function applyCaslToQuery(operation, args, abilities, model) { - const operationAbility = caslOperationDict[operation]; - if (args.caslAction) { - operationAbility.action = args.caslAction; - } - m5(abilities, operationAbility.action)[model]; - if (operationAbility.dataQuery && args.data) { - args.data = applyDataQuery(abilities, args.data, operationAbility.action, model); - } - if (operationAbility.whereQuery) { - args = applyWhereQuery(abilities, args, operationAbility.action, model); - } - if (operationAbility.includeSelectQuery) { - args = applyIncludeSelectQuery(abilities, args, model); + return hasPermittedFields ? permittedFields : void 0; +} +function isSubset(obj1, obj2) { + if (obj1 === obj2) return true; + if (typeof obj1 === "object" && typeof obj2 === "object") { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + for (const item1 of obj1) { + let found = false; + for (const item2 of obj2) { + if (isSubset(item1, item2)) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } else { + for (const key in obj1) { + if (!(key in obj2) || !isSubset(obj1[key], obj2[key])) { + return false; + } + } + return true; + } } - return args; + return false; +} + +// src/applyAccessibleQuery.ts +function applyAccessibleQuery(query, accessibleQuery) { + return { + ...query, + AND: [ + ...query.AND ?? [], + accessibleQuery + ] + }; } + +// src/applyDataQuery.ts function applyDataQuery(abilities, args, action, model) { const permittedFields = getPermittedFields(abilities, args, action, model); const accessibleQuery = m5(abilities, action)[model]; @@ -969,6 +959,47 @@ function applyDataQuery(abilities, args, action, model) { }); return args; } + +// src/applySelectPermittedFields.ts +var applySelectPermittedFields = (abilities, args, model) => { + const permittedFields = getPermittedFields(abilities, args, "read", model); + if (permittedFields) { + if (args === true) { + args = { + select: {} + }; + } + if (args.include) { + args.select = { ...args.include }; + delete args.include; + } + if (!args.select) { + args.select = {}; + } + const queriedFields = args.select ? Object.keys(args.select) : []; + const remainingFields = queriedFields.filter((field) => { + const isRelation = relationFieldsByModel[model][field] ? true : false; + if (!permittedFields.includes(field) && !isRelation) { + delete args.select[field]; + return false; + } else if (isRelation) { + return false; + } + return true; + }); + if (remainingFields.length === 0) { + permittedFields.forEach((field) => { + args.select = { + ...args.select, + [field]: true + }; + }); + } + } + return args; +}; + +// src/applyWhereQuery.ts function applyWhereQuery(abilities, args, action, model, relation) { const prismaModel = model in relationFieldsByModel ? model : void 0; if (!prismaModel) { @@ -1008,43 +1039,8 @@ function applyWhereQuery(abilities, args, action, model, relation) { return applySelectPermittedFields(abilities, args, model); } } -var applySelectPermittedFields = (abilities, args, model) => { - const permittedFields = getPermittedFields(abilities, args, "read", model); - if (permittedFields) { - if (args === true) { - args = { - select: {} - }; - } - if (args.include) { - args.select = { ...args.include }; - delete args.include; - } - if (!args.select) { - args.select = {}; - } - const queriedFields = args.select ? Object.keys(args.select) : []; - const remainingFields = queriedFields.filter((field) => { - const isRelation = relationFieldsByModel[model][field] ? true : false; - if (!permittedFields.includes(field) && !isRelation) { - delete args.select[field]; - return false; - } else if (isRelation) { - return false; - } - return true; - }); - if (remainingFields.length === 0) { - permittedFields.forEach((field) => { - args.select = { - ...args.select, - [field]: true - }; - }); - } - } - return args; -}; + +// src/applyIncludeSelectQuery.ts var applyIncludeSelectQuery = (abilities, args, model) => { ; ["include", "select"].forEach((method) => { @@ -1067,81 +1063,89 @@ var applyIncludeSelectQuery = (abilities, args, model) => { }); return args; }; -function getPermittedFields(abilities, args, action, model) { - let hasPermittedFields = false; - const omittedFieldsSet = /* @__PURE__ */ new Set(); - const permittedFields = c4(abilities, action, model, { - fieldsFrom: (rule) => { - if (rule.fields) { - if (rule.inverted) { - rule.fields.forEach((field) => omittedFieldsSet.add(field)); - } else { - hasPermittedFields = true; - } - if (rule.conditions) { - if (isSubset(rule.conditions, args.where)) { - return rule.fields; - } else { - } - } else { - return rule.fields; - } - } - return []; - } - }); - if (hasPermittedFields === false && permittedFields.length === 0 && omittedFieldsSet.size > 0) { - permittedFields.push(...Object.keys(propertyFieldsByModel[model]).filter((field) => !omittedFieldsSet.has(field))); - hasPermittedFields = true; + +// src/applyCaslToQuery.ts +function applyCaslToQuery(operation, args, abilities, model) { + const operationAbility = caslOperationDict[operation]; + if (args.caslAction) { + operationAbility.action = args.caslAction; } - return hasPermittedFields ? permittedFields : void 0; -} -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); + m5(abilities, operationAbility.action)[model]; + if (operationAbility.dataQuery && args.data) { + args.data = applyDataQuery(abilities, args.data, operationAbility.action, model); + } + if (operationAbility.whereQuery) { + args = applyWhereQuery(abilities, args, operationAbility.action, model); + } + if (operationAbility.includeSelectQuery) { + args = applyIncludeSelectQuery(abilities, args, model); + } + return args; } -function isSubset(obj1, obj2) { - if (obj1 === obj2) return true; - if (typeof obj1 === "object" && typeof obj2 === "object") { - if (Array.isArray(obj1) && Array.isArray(obj2)) { - for (const item1 of obj1) { - let found = false; - for (const item2 of obj2) { - if (isSubset(item1, item2)) { - found = true; - break; - } - } - if (!found) return false; - } - return true; - } else { - for (const key in obj1) { - if (!(key in obj2) || !isSubset(obj1[key], obj2[key])) { - return false; - } + +// src/index.ts +var useCaslAbilities = (abilities) => Prisma2.defineExtension({ + name: "prisma-extension-casl", + query: { + $allModels: { + create({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + createMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + createManyAndReturn({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + upsert({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findFirst({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findFirstOrThrow({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findUnique({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + findUniqueOrThrow({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + aggregate({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + count({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + groupBy({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + update({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + updateMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + delete({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); + }, + deleteMany({ args, query, model, operation }) { + return query(applyCaslToQuery(operation, args, abilities, model)); } - return true; + // async $allOperations({ args, query, model, operation }: { args: any, query: any, model: any, operation: any }) { + // if (!(operation in caslOperationDict)) { + // return query(args) + // } + // args = applyCaslToQuery(operation, args, abilities, model) + // return query(args) + // }, } } - return false; -} -function applyAccessibleQuery(clause, accessibleQuery) { - return { - ...clause, - AND: [ - ...clause.AND ?? [], - accessibleQuery - ] - }; -} +}); export { - applyCaslToQuery, - applyDataQuery, - applyIncludeSelectQuery, - applySelectPermittedFields, - applyWhereQuery, - capitalizeFirstLetter, - caslOperationDict, - isSubset, useCaslAbilities }; diff --git a/package.json b/package.json index cf84470..93fd05c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@casl/ability": "^6.7.1", "@casl/prisma": "^1.4.1", "@prisma/client": "latest", + "@prisma/generator-helper": "^5.17.0", "@types/jest": "^29.5.12", "@types/node": "^20.14.10", "esbuild": "^0.23.0", diff --git a/src/applyAccessibleQuery.ts b/src/applyAccessibleQuery.ts new file mode 100644 index 0000000..fbcbe11 --- /dev/null +++ b/src/applyAccessibleQuery.ts @@ -0,0 +1,20 @@ + +/** + * applies accessibleBy query to query + * + * convenience function + * + * @param query current query (where or connect/disconnect) + * @param accessibleQuery casl accessibleBy query result + * @returns enriched query + */ +export function applyAccessibleQuery(query: any, accessibleQuery: any){ + return { + ...query, + AND: [ + ...(query.AND ?? []), + accessibleQuery + ] + + } + } \ No newline at end of file diff --git a/src/applyCaslToQuery.ts b/src/applyCaslToQuery.ts new file mode 100644 index 0000000..5ac94ae --- /dev/null +++ b/src/applyCaslToQuery.ts @@ -0,0 +1,40 @@ +import { Prisma } from '@prisma/client' +import { accessibleBy } from "@casl/prisma" +import { caslOperationDict, PrismaCaslOperation } from "./helpers" +import { applyDataQuery } from "./applyDataQuery" +import { applyWhereQuery } from "./applyWhereQuery" +import { applyIncludeSelectQuery } from "./applyIncludeSelectQuery" +import { AbilityTuple, PureAbility } from '@casl/ability' +import { PrismaQuery } from '@casl/prisma' + +/** + * Applies CASL authorization logic to prisma query + * + * @param operation Prisma Operation `findUnique` etc + * @param args Prisma query + * @param abilities Casl prisma abilities + * @param model Prisma model + * @returns Enriched query with casl authorization + */ +export function applyCaslToQuery(operation: any, args: any, abilities: PureAbility, model: Prisma.ModelName) { + const operationAbility = caslOperationDict[operation as PrismaCaslOperation] + if (args.caslAction) { + operationAbility.action = args.caslAction + } + + accessibleBy(abilities, operationAbility.action)[model] + + if(operationAbility.dataQuery && args.data) { + args.data = applyDataQuery(abilities, args.data, operationAbility.action, model) + } + + + if (operationAbility.whereQuery) { + args = applyWhereQuery(abilities, args, operationAbility.action, model) + } + + if (operationAbility.includeSelectQuery) { + args = applyIncludeSelectQuery(abilities, args, model) + } + return args +} \ No newline at end of file diff --git a/src/applyDataQuery.ts b/src/applyDataQuery.ts new file mode 100644 index 0000000..4a4ae90 --- /dev/null +++ b/src/applyDataQuery.ts @@ -0,0 +1,109 @@ +import { Prisma } from '@prisma/client' +import { AbilityTuple, PureAbility } from '@casl/ability' +import { accessibleBy, PrismaQuery } from '@casl/prisma' +import { caslNestedOperationDict, getPermittedFields, propertyFieldsByModel, relationFieldsByModel } from './helpers' +import { applyAccessibleQuery } from './applyAccessibleQuery' + +/** + * checks if mutation query is authorized by CASL + * and throws error if not + * + * applies casl where queries to the necessary data and where queries + * + * considers if query is array or not + * and if query is upsert or connectOrCreate + * it checks for `update` capabilities instead of create capabilities + * + * @param abilities Casl prisma abilities + * @param args query with { data/create/update/where } + * @param action Casl action - preferably create/read/update/delete + * @param model prisma model + * @returns enriched query with casl authorization or Error + */ +export function applyDataQuery( + abilities: PureAbility, + args: any, + action: string, + model: string +) { + const permittedFields = getPermittedFields(abilities, args, action, model) + + const accessibleQuery = accessibleBy(abilities, action)[model as Prisma.ModelName] + + const mutationArgs: any[] = [] + ;(Array.isArray(args) ? args : [args]).map((argsEntry)=>{ + let hasWhereQuery = false + // opt 1.: we either have mutations within data/create/update + // order is important for where query + ;['update', 'create', 'data'].forEach((nestedAction)=>{ + if(nestedAction in argsEntry){ + const nestedArgs = argsEntry[nestedAction] + Array.isArray(nestedArgs) ? mutationArgs.push(...nestedArgs) : mutationArgs.push(nestedArgs) + // if the mutation are not within args, we might also have a where query that needs to consider abilities + if(!hasWhereQuery && 'where' in argsEntry){ + hasWhereQuery = true + // if nestedAction is update, we probably have upsert + // if nestedAction is create, we probably have connectOrCreate + // therefore we check for 'update' accessibleQuery + argsEntry.where = applyAccessibleQuery(argsEntry.where, + nestedAction !== 'update' && nestedAction !== 'create' ? accessibleQuery : accessibleBy(abilities, 'update')[model as Prisma.ModelName] + ) + } + } + }) + // opt 2.: or the args themselves are the mutation + if(mutationArgs.length === 0){ + mutationArgs.push(args) + } + + }) + + /** now we go trough all mutation args and throw error if they have not permitted fields or continue in nested mutations */ + mutationArgs.map((mutation: any)=>{ + + // get all mutation arg fields and if they are short code connect (userId instead of user: { connect: id }), we convert it + const queriedFields = (mutation ? Object.keys(mutation) : []).map((field)=>{ + const relationModelId = propertyFieldsByModel[model][field] + if(relationModelId){ + mutation[relationModelId] = { connect: {id: mutation[field]} } + delete mutation[field] + return relationModelId + } else { + return field + } + }) + + queriedFields.forEach((field) => { + const relationModel = relationFieldsByModel[model][field] + // omit relation models also through i.e. stat + if (permittedFields?.includes(field) === false && !relationModel) { + // if fields are not permitted we throw an error and exit + throw new Error(`It's not allowed to "${action}" "${field}" on "${model}"`) + }else if(relationModel){ + // if additional relations are found, we apply data query on them, too + Object.entries(mutation[field]).forEach(([nestedAction, nestedArgs])=>{ + if(nestedAction in caslNestedOperationDict){ + const mutationAction = caslNestedOperationDict[nestedAction] + const isConnection = nestedAction === 'connect' || nestedAction === 'disconnect' + + mutation[field][nestedAction] = applyDataQuery(abilities, nestedArgs, mutationAction, relationModel.type) + // connection works like a where query, so we apply it + if(isConnection){ + const accessibleQuery = accessibleBy(abilities, mutationAction)[relationModel.type as Prisma.ModelName] + + if(Array.isArray(mutation[field][nestedAction])){ + mutation[field][nestedAction] = mutation[field][nestedAction].map((q)=>applyAccessibleQuery(q, accessibleQuery)) + }else{ + mutation[field][nestedAction] = applyAccessibleQuery(mutation[field][nestedAction], accessibleQuery) + } + } + }else{ + throw new Error(`Unknown nested action ${nestedAction} on ${model}`) + } + }) + } + }) + + }) + return args +} \ No newline at end of file diff --git a/src/applyIncludeSelectQuery.ts b/src/applyIncludeSelectQuery.ts new file mode 100644 index 0000000..abe3a77 --- /dev/null +++ b/src/applyIncludeSelectQuery.ts @@ -0,0 +1,46 @@ +import { AbilityTuple, PureAbility } from '@casl/ability' +import { PrismaQuery } from '@casl/prisma' +import { relationFieldsByModel } from './helpers' +import { applyWhereQuery } from './applyWhereQuery' + + +/** + * applies casl authorization to include and select queries + * for the specified model + * if queries include relations, this function will be recursed on those models + * + * if necessary, where query will be added + * + * @param abilities Casl prisma abilities + * @param args query with { include, select} + * @param model prisma model + * @returns enriched query with casl authorization + */ +export const applyIncludeSelectQuery = ( + abilities: PureAbility, + args: any, + model: string, +) => { + ;["include", "select"].forEach((method) => { + if (args && args[method]) { + for (const relation in args[method]) { + if (model in relationFieldsByModel && relation in relationFieldsByModel[model]) { + const relationField = relationFieldsByModel[model][relation] + if (relationField) { + if (relationField.isList) { + const methodQuery = applyWhereQuery(abilities, args[method][relation], 'read', relationField.type) + // if select function is empty, we do not query the relation + args[method][relation] = methodQuery.select && Object.keys(methodQuery.select).length === 0 ? false : methodQuery + } else { + args = applyWhereQuery(abilities, args, 'read', relationField.type, relation) + } + + args[method][relation] = applyIncludeSelectQuery(abilities, args[method][relation], relationField.type) + } + } + } + } + }) + return args + +} \ No newline at end of file diff --git a/src/applySelectPermittedFields.ts b/src/applySelectPermittedFields.ts new file mode 100644 index 0000000..0a3ce82 --- /dev/null +++ b/src/applySelectPermittedFields.ts @@ -0,0 +1,57 @@ + +import { AbilityTuple, PureAbility } from '@casl/ability' +import { PrismaQuery } from '@casl/prisma' +import { getPermittedFields, relationFieldsByModel } from './helpers' + + +/** + * gets permitted fields for current model + * and adds select query with only permitted properties + * and if args has include, it will be converted to select + * + * @param abilities Casl prisma abilities + * @param args query + * @param model prisma model + * @returns enriched query with selection of fields considering casl authorization + */ +export const applySelectPermittedFields = (abilities: PureAbility, args: any, model: string) => { + + const permittedFields = getPermittedFields(abilities, args, 'read', model) + if (permittedFields) { + // prepare select statement and transform include to select if necessary + if (args === true) { + args = { + select: {} + } + } + if (args.include) { + args.select = { ...args.include } + delete args.include + } + if (!args.select) { + args.select = {} + } + const queriedFields = args.select ? Object.keys(args.select) : [] + // remove all fields that are not a relation or not permitted + const remainingFields = queriedFields.filter((field) => { + const isRelation = relationFieldsByModel[model][field] ? true : false + if (!(permittedFields.includes(field)) && !isRelation) { + delete args.select[field] + return false + } else if (isRelation) { + return false + } + return true + }) + // if not fields are left, we use a default query of all permitted fields of the model + if (remainingFields.length === 0) { + permittedFields.forEach((field) => { + args.select = { + ...args.select, + [field]: true + } + }) + } + } + return args +} diff --git a/src/applyWhereQuery.ts b/src/applyWhereQuery.ts new file mode 100644 index 0000000..060ca85 --- /dev/null +++ b/src/applyWhereQuery.ts @@ -0,0 +1,81 @@ + +import { Prisma } from '@prisma/client' +import { AbilityTuple, PureAbility } from '@casl/ability' +import { accessibleBy, PrismaQuery } from '@casl/prisma' +import { applySelectPermittedFields } from './applySelectPermittedFields' +import { relationFieldsByModel } from './helpers' +import { applyAccessibleQuery } from './applyAccessibleQuery' + + +/** + * applies casl authorization conditions to where query for model + * + * if include or select have a relation + * its where condition needs to be applied to the main where clause, too + * this is possible, by optionally setting the relation prop + * with the name of its field in the include / select query + * + * @param abilities Casl prisma abilities + * @param args query with { where } + * @param action Casl action - preferably create/read/update/delete + * @param model prisma model + * @param relation (optional) relation field + * @returns enriched query with casl authorization + */ +export function applyWhereQuery( + abilities: PureAbility, + args: any, + action: string, + model: string, + relation?: string +) { + const prismaModel = model in relationFieldsByModel ? model as Prisma.ModelName : undefined + if (!prismaModel) { + throw new Error(`Model ${model} does not exist on Prisma Client`) + } + + const accessibleQuery = accessibleBy(abilities, action)[prismaModel] + + if (Object.keys(accessibleQuery).length > 0) { + if (args === true) { + args = {} + } + + if (!args.where) { + args.where = {} + } + + // Add the accessibleBy conditions to the where clause + args.where = applyAccessibleQuery(args.where, relation && accessibleQuery ? { [relation]: accessibleQuery } : accessibleQuery) + } + + if (relation) { + // if we add a where clause to a relation + // we fake the where query, since it is otherwise buried in AND: [ OR: ...] + // to get the select query + const method = args.include ? "include" : "select" + + + const selectQuery = applySelectPermittedFields(abilities, { + ...args[method][relation], + where: { + ...(args[method][relation].where ?? {}), + ...accessibleQuery + }, + }, model) + const result = { + ...args, + [method]: { + ...args[method], + [relation]: { + ...args[method][relation], + select: selectQuery.select + } + } + } + + return result + } else { + return applySelectPermittedFields(abilities, args, model) + } +} \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..be22f3e --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,154 @@ +import { Prisma } from '@prisma/client' +import { DMMF } from '@prisma/generator-helper' +import { AbilityTuple, PureAbility } from '@casl/ability' +import { permittedFieldsOf } from '@casl/ability/extra' +import { PrismaQuery } from '@casl/prisma' + +type DefaultCaslAction = "create" | "read" | "update" | "delete" + +export type PrismaCaslOperation = + 'create' | + 'createMany' | + 'createManyAndReturn' | + 'upsert' | + 'findFirst' | + 'findFirstOrThrow' | + 'findMany' | + 'findUnique' | + 'findUniqueOrThrow' | + 'aggregate' | + 'count' | + 'groupBy' | + 'update' | + 'updateMany' | + 'delete' | + 'deleteMany' + +export const caslOperationDict: Record< + PrismaCaslOperation, + { + action: DefaultCaslAction + dataQuery: boolean + whereQuery: boolean + includeSelectQuery: boolean + } +> = { + create: { action: 'create', dataQuery: true, whereQuery: false, includeSelectQuery: true }, + createMany: { action: 'create', dataQuery: true, whereQuery: false, includeSelectQuery: false }, + createManyAndReturn: { action: 'create', dataQuery: true, whereQuery: false, includeSelectQuery: true }, + upsert: { action: 'create', dataQuery: true, whereQuery: true, includeSelectQuery: false }, + findFirst: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, + findFirstOrThrow: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, + findMany: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, + findUnique: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, + findUniqueOrThrow: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, + aggregate: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: false }, + count: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: false }, + groupBy: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: false }, + update: { action: 'update', dataQuery: true, whereQuery: true, includeSelectQuery: true }, + updateMany: { action: 'update', dataQuery: true, whereQuery: true, includeSelectQuery: false }, + delete: { action: 'delete', dataQuery: false, whereQuery: true, includeSelectQuery: true }, + deleteMany: { action: 'delete', dataQuery: false, whereQuery: true, includeSelectQuery: false }, +} as const + +export const caslNestedOperationDict: Record = { + upsert: 'create', + connect: 'update', + connectOrCreate:'create', + create: 'create', + createMany: 'create', + update: 'update', + updateMany: 'update', + delete: 'delete', + deleteMany: 'delete', + disconnect: 'update', + set: 'update' +} + +export const relationFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model: DMMF.Model) => { + const relationFields = Object.fromEntries(model.fields + .filter((field) => field && field.kind === 'object' && field.relationName) + .map((field) => ([field.name, field]))) + return [model.name, relationFields] +})) + +export const propertyFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model: DMMF.Model) => { + const propertyFields = Object.fromEntries(model.fields + .filter((field) => !(field && field.kind === 'object' && field.relationName)) + .map((field) => { + const relation = Object.values(relationFieldsByModel[model.name]).find((value: any)=>value.relationFromFields.includes(field.name)) + return [field.name, relation?.name] + })) + return [model.name, propertyFields] +})) + + +export function getPermittedFields( + abilities: PureAbility, + args: any, + action: string, + model: string +){ + let hasPermittedFields = false + const omittedFieldsSet = new Set() + const permittedFields = permittedFieldsOf(abilities, action, model, { + fieldsFrom: rule => { + if (rule.fields) { + if(rule.inverted){ + rule.fields.forEach((field)=>omittedFieldsSet.add(field)) + } + else{ + hasPermittedFields = true + } + if (rule.conditions) { + if (isSubset(rule.conditions, args.where)) { + return rule.fields + } else { + // console.warn(`${model} fields ${JSON.stringify(rule.fields)} can only be read with following conditions: ${JSON.stringify(rule.conditions)}`) + } + } else { + return rule.fields + } + } + return [] + } + }) + + // if can rules allow read access on all properties, but some cannot rules omit fields, we add all permitted properties to select to create an inverted version + // newer prisma version will allow omit besides select for cleaner interface. + if(hasPermittedFields === false && permittedFields.length === 0 && omittedFieldsSet.size > 0){ + permittedFields.push(...Object.keys(propertyFieldsByModel[model]).filter((field)=> !omittedFieldsSet.has(field))) + hasPermittedFields = true + } + return hasPermittedFields ? permittedFields : undefined +} + + +export function isSubset(obj1: any, obj2: any): boolean { + if (obj1 === obj2) return true; + + if (typeof obj1 === "object" && typeof obj2 === "object") { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + for (const item1 of obj1) { + let found = false; + for (const item2 of obj2) { + if (isSubset(item1, item2)) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } else { + + for (const key in obj1) { + if (!(key in obj2) || !isSubset(obj1[key], obj2[key])) { + return false; + } + } + return true; + } + } + return false; +} diff --git a/src/index.ts b/src/index.ts index ef7fb33..8232919 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,83 +1,20 @@ import { Prisma } from '@prisma/client' import { AbilityTuple, PureAbility } from '@casl/ability' -import { permittedFieldsOf } from '@casl/ability/extra' -import { accessibleBy, PrismaQuery } from '@casl/prisma' - -const relationFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model: Prisma.DMMF.Model) => { - const relationFields = Object.fromEntries(model.fields - .filter((field) => field && field.kind === 'object' && field.relationName) - .map((field) => ([field.name, field]))) - return [model.name, relationFields] -})) -const propertyFieldsByModel = Object.fromEntries(Prisma.dmmf.datamodel.models.map((model: Prisma.DMMF.Model) => { - const propertyFields = Object.fromEntries(model.fields - .filter((field) => !(field && field.kind === 'object' && field.relationName)) - .map((field) => { - const relation = Object.values(relationFieldsByModel[model.name]).find((value: any)=>value.relationFromFields.includes(field.name)) - return [field.name, relation?.name] - })) - return [model.name, propertyFields] -})) - -type DefaultCaslAction = "create" | "read" | "update" | "delete" -export type PrismaCaslOperation = - 'create' | - 'createMany' | - 'createManyAndReturn' | - 'upsert' | - 'findFirst' | - 'findFirstOrThrow' | - 'findMany' | - 'findUnique' | - 'findUniqueOrThrow' | - 'aggregate' | - 'count' | - 'groupBy' | - 'update' | - 'updateMany' | - 'delete' | - 'deleteMany' -export const caslOperationDict: Record< - PrismaCaslOperation, - { - action: DefaultCaslAction - dataQuery: boolean - whereQuery: boolean - includeSelectQuery: boolean - } -> = { - create: { action: 'create', dataQuery: true, whereQuery: false, includeSelectQuery: true }, - createMany: { action: 'create', dataQuery: true, whereQuery: false, includeSelectQuery: false }, - createManyAndReturn: { action: 'create', dataQuery: true, whereQuery: false, includeSelectQuery: true }, - upsert: { action: 'create', dataQuery: true, whereQuery: true, includeSelectQuery: false }, - findFirst: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, - findFirstOrThrow: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, - findMany: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, - findUnique: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, - findUniqueOrThrow: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: true }, - aggregate: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: false }, - count: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: false }, - groupBy: { action: 'read', dataQuery: false, whereQuery: true, includeSelectQuery: false }, - update: { action: 'update', dataQuery: true, whereQuery: true, includeSelectQuery: true }, - updateMany: { action: 'update', dataQuery: true, whereQuery: true, includeSelectQuery: false }, - delete: { action: 'delete', dataQuery: false, whereQuery: true, includeSelectQuery: true }, - deleteMany: { action: 'delete', dataQuery: false, whereQuery: true, includeSelectQuery: false }, -} as const - -const caslNestedOperationDict: Record = { - upsert: 'create', - connect: 'update', - connectOrCreate:'create', - create: 'create', - createMany: 'create', - update: 'update', - updateMany: 'update', - delete: 'delete', - deleteMany: 'delete', - disconnect: 'update', - set: 'update' -} - +import { PrismaQuery } from '@casl/prisma' +import { applyCaslToQuery } from './applyCaslToQuery' + + +/** + * enrich a prisma client to check for CASL abilities even in nested queries + * + * `client.$extends(useCaslAbilities(build()))` + * + * https://casl.js.org/v6/en/package/casl-prisma + * + * + * @param abilities CASL prisma abilities + * @returns enriched prisma client + */ export const useCaslAbilities = (abilities: PureAbility) => Prisma.defineExtension({ name: "prisma-extension-casl", @@ -145,333 +82,8 @@ export const useCaslAbilities = (abilities: PureAbility, model: Prisma.ModelName) { - const operationAbility = caslOperationDict[operation as PrismaCaslOperation] - if (args.caslAction) { - operationAbility.action = args.caslAction - } - - accessibleBy(abilities, operationAbility.action)[model] - - if(operationAbility.dataQuery && args.data) { - args.data = applyDataQuery(abilities, args.data, operationAbility.action, model) - } - - - if (operationAbility.whereQuery) { - args = applyWhereQuery(abilities, args, operationAbility.action, model) - } - - if (operationAbility.includeSelectQuery) { - args = applyIncludeSelectQuery(abilities, args, model) - } - return args -} - - -export function applyDataQuery( - abilities: PureAbility, - args: any, - action: string, - model: string -) { - const permittedFields = getPermittedFields(abilities, args, action, model) - - const accessibleQuery = accessibleBy(abilities, action)[model as Prisma.ModelName] - - const mutationArgs: any[] = [] - ;(Array.isArray(args) ? args : [args]).map((argsEntry)=>{ - let hasWhereQuery = false - // opt 1.: we either have mutations within data/create/update - // order is important for where query - ;['update', 'create', 'data'].forEach((nestedAction)=>{ - if(nestedAction in argsEntry){ - const nestedArgs = argsEntry[nestedAction] - Array.isArray(nestedArgs) ? mutationArgs.push(...nestedArgs) : mutationArgs.push(nestedArgs) - // if the mutation are not within args, we might also have a where query that needs to consider abilities - if(!hasWhereQuery && 'where' in argsEntry){ - hasWhereQuery = true - // if nestedAction is update, we probably have upsert - // if nestedAction is create, we probably have connectOrCreate - // therefore we check for 'update' accessibleQuery - argsEntry.where = applyAccessibleQuery(argsEntry.where, - nestedAction !== 'update' && nestedAction !== 'create' ? accessibleQuery : accessibleBy(abilities, 'update')[model as Prisma.ModelName] - ) - } - } - }) - // opt 2.: or the args themselves are the mutation - if(mutationArgs.length === 0){ - mutationArgs.push(args) - } - - }) - - /** now we go trough all mutation args and throw error if they have not permitted fields or continue in nested mutations */ - mutationArgs.map((mutation: any)=>{ - - // get all mutation arg fields and if they are short code connect (userId instead of user: { connect: id }), we convert it - const queriedFields = (mutation ? Object.keys(mutation) : []).map((field)=>{ - const relationModelId = propertyFieldsByModel[model][field] - if(relationModelId){ - mutation[relationModelId] = { connect: {id: mutation[field]} } - delete mutation[field] - return relationModelId - } else { - return field - } - }) - - queriedFields.forEach((field) => { - const relationModel = relationFieldsByModel[model][field] - // omit relation models also through i.e. stat - if (permittedFields?.includes(field) === false && !relationModel) { - // if fields are not permitted we throw an error and exit - throw new Error(`It's not allowed to "${action}" "${field}" on "${model}"`) - }else if(relationModel){ - // if additional relations are found, we apply data query on them, too - Object.entries(mutation[field]).forEach(([nestedAction, nestedArgs])=>{ - if(nestedAction in caslNestedOperationDict){ - const mutationAction = caslNestedOperationDict[nestedAction] - const isConnection = nestedAction === 'connect' || nestedAction === 'disconnect' - - mutation[field][nestedAction] = applyDataQuery(abilities, nestedArgs, mutationAction, relationModel.type) - // connection works like a where query, so we apply it - if(isConnection){ - const accessibleQuery = accessibleBy(abilities, mutationAction)[relationModel.type as Prisma.ModelName] - - if(Array.isArray(mutation[field][nestedAction])){ - mutation[field][nestedAction] = mutation[field][nestedAction].map((q)=>applyAccessibleQuery(q, accessibleQuery)) - }else{ - mutation[field][nestedAction] = applyAccessibleQuery(mutation[field][nestedAction], accessibleQuery) - } - } - }else{ - throw new Error(`Unknown nested action ${nestedAction} on ${model}`) - } - }) - } - }) - - }) - return args -} - - -export function applyWhereQuery( - abilities: PureAbility, - args: any, - action: string, - model: string, - relation?: string -) { - const prismaModel = model in relationFieldsByModel ? model as Prisma.ModelName : undefined - if (!prismaModel) { - throw new Error(`Model ${model} does not exist on Prisma Client`) - } - - const accessibleQuery = accessibleBy(abilities, action)[prismaModel] - - if (Object.keys(accessibleQuery).length > 0) { - if (args === true) { - args = {} - } - - if (!args.where) { - args.where = {} - } - - // Add the accessibleBy conditions to the where clause - args.where = applyAccessibleQuery(args.where, relation && accessibleQuery ? { [relation]: accessibleQuery } : accessibleQuery) - } - - if (relation) { - // if we add a where clause to a relation - // we fake the where query, since it is otherwise buried in AND: [ OR: ...] - // to get the select query - const method = args.include ? "include" : "select" - - - const selectQuery = applySelectPermittedFields(abilities, { - ...args[method][relation], - where: { - ...(args[method][relation].where ?? {}), - ...accessibleQuery - }, - }, model) - const result = { - ...args, - [method]: { - ...args[method], - [relation]: { - ...args[method][relation], - select: selectQuery.select - } - } - } - - return result - } else { - return applySelectPermittedFields(abilities, args, model) - } -} - -export const applySelectPermittedFields = (abilities: PureAbility, args: any, model: string) => { - - const permittedFields = getPermittedFields(abilities, args, 'read', model) - if (permittedFields) { - // prepare select statement and transform include to select if necessary - if (args === true) { - args = { - select: {} - } - } - if (args.include) { - args.select = { ...args.include } - delete args.include - } - if (!args.select) { - args.select = {} - } - const queriedFields = args.select ? Object.keys(args.select) : [] - // remove all fields that are not a relation or not permitted - const remainingFields = queriedFields.filter((field) => { - const isRelation = relationFieldsByModel[model][field] ? true : false - if (!(permittedFields.includes(field)) && !isRelation) { - delete args.select[field] - return false - } else if (isRelation) { - return false - } - return true - }) - // if not fields are left, we use a default query of all permitted fields of the model - if (remainingFields.length === 0) { - permittedFields.forEach((field) => { - args.select = { - ...args.select, - [field]: true - } - }) - } - } - return args -} - - -// Recursively apply accessibleBy to nested relations -export const applyIncludeSelectQuery = ( - abilities: PureAbility, - args: any, - model: string, -) => { - ;["include", "select"].forEach((method) => { - if (args && args[method]) { - for (const relation in args[method]) { - if (model in relationFieldsByModel && relation in relationFieldsByModel[model]) { - const relationField = relationFieldsByModel[model][relation] - if (relationField) { - if (relationField.isList) { - const methodQuery = applyWhereQuery(abilities, args[method][relation], 'read', relationField.type) - // if select function is empty, we do not query the relation - args[method][relation] = methodQuery.select && Object.keys(methodQuery.select).length === 0 ? false : methodQuery - } else { - args = applyWhereQuery(abilities, args, 'read', relationField.type, relation) - } - - args[method][relation] = applyIncludeSelectQuery(abilities, args[method][relation], relationField.type) - } - } - } - } - }) - return args - -} -function getPermittedFields( - abilities: PureAbility, - args: any, - action: string, - model: string -){ - let hasPermittedFields = false - const omittedFieldsSet = new Set() - const permittedFields = permittedFieldsOf(abilities, action, model, { - fieldsFrom: rule => { - if (rule.fields) { - if(rule.inverted){ - rule.fields.forEach((field)=>omittedFieldsSet.add(field)) - } - else{ - hasPermittedFields = true - } - if (rule.conditions) { - if (isSubset(rule.conditions, args.where)) { - return rule.fields - } else { - // console.warn(`${model} fields ${JSON.stringify(rule.fields)} can only be read with following conditions: ${JSON.stringify(rule.conditions)}`) - } - } else { - return rule.fields - } - } - return [] - } - }) - - // if can rules allow read access on all properties, but some cannot rules omit fields, we add all permitted properties to select to create an inverted version - // newer prisma version will allow omit besides select for cleaner interface. - if(hasPermittedFields === false && permittedFields.length === 0 && omittedFieldsSet.size > 0){ - permittedFields.push(...Object.keys(propertyFieldsByModel[model]).filter((field)=> !omittedFieldsSet.has(field))) - hasPermittedFields = true - } - return hasPermittedFields ? permittedFields : undefined -} - -export function capitalizeFirstLetter(string: string) { - return string.charAt(0).toUpperCase() + string.slice(1) -} -export function isSubset(obj1: any, obj2: any): boolean { - if (obj1 === obj2) return true; - - if (typeof obj1 === "object" && typeof obj2 === "object") { - if (Array.isArray(obj1) && Array.isArray(obj2)) { - for (const item1 of obj1) { - let found = false; - for (const item2 of obj2) { - if (isSubset(item1, item2)) { - found = true; - break; - } - } - if (!found) return false; - } - return true; - } else { - - for (const key in obj1) { - if (!(key in obj2) || !isSubset(obj1[key], obj2[key])) { - return false; - } - } - return true; - } - } - return false; -} - -function applyAccessibleQuery(clause: any, accessibleQuery: any){ - return { - ...clause, - AND: [ - ...(clause.AND ?? []), - accessibleQuery - ] - - } -} \ No newline at end of file diff --git a/test/abilities.ts b/test/abilities.ts index 89ca3f0..61191a2 100644 --- a/test/abilities.ts +++ b/test/abilities.ts @@ -1,8 +1,5 @@ -import { permission } from 'process' - -import { AbilityBuilder, ExtractSubjectType, PureAbility, subject } from '@casl/ability' +import { AbilityBuilder, PureAbility } from '@casl/ability' import { - accessibleBy, createPrismaAbility, PrismaQuery, Subjects, @@ -25,20 +22,3 @@ type AppAbility = PureAbility< export function abilityBuilder(){ return new AbilityBuilder(createPrismaAbility) } -export function defineAbilities({userId}: { userId: number}) { - - const { can: allow, cannot: forbid, build } = new AbilityBuilder(createPrismaAbility) - - allow(['create', 'read'], 'Thread') - allow(['update', 'delete'], 'Thread', { - creatorId: userId - }) - allow(['read', 'create'], 'Post') - allow(['update', 'delete'], 'Post', { - authorId: userId - }) - - - return build() - } - \ No newline at end of file diff --git a/test/applyCaslToQuery.test.ts b/test/applyCaslToQuery.test.ts index b72aebb..e5ce8fd 100644 --- a/test/applyCaslToQuery.test.ts +++ b/test/applyCaslToQuery.test.ts @@ -1,6 +1,7 @@ import { abilityBuilder } from './abilities' -import { applyCaslToQuery, caslOperationDict } from '../dist' +import { applyCaslToQuery } from '../src/applyCaslToQuery' +import { caslOperationDict } from '../src/helpers' describe('apply casl to query', () => { diff --git a/test/applyDataQuery.test.ts b/test/applyDataQuery.test.ts index c328135..9a1277c 100644 --- a/test/applyDataQuery.test.ts +++ b/test/applyDataQuery.test.ts @@ -1,6 +1,6 @@ import { abilityBuilder } from './abilities' -import { applyDataQuery } from '../dist' +import { applyDataQuery } from '../src/applyDataQuery' describe('apply data query', () => { diff --git a/test/applyIncludeSelectQuery.test.ts b/test/applyIncludeSelectQuery.test.ts index 6e133d0..e6d2b9f 100644 --- a/test/applyIncludeSelectQuery.test.ts +++ b/test/applyIncludeSelectQuery.test.ts @@ -1,6 +1,6 @@ import { abilityBuilder } from './abilities' -import { applyIncludeSelectQuery } from '../dist' +import { applyIncludeSelectQuery } from '../src/applyIncludeSelectQuery' describe('apply include select query', () => { it('applies select method', () => { diff --git a/test/applySelectPermittedFields.test.ts b/test/applySelectPermittedFields.test.ts index bbe1039..27ec713 100644 --- a/test/applySelectPermittedFields.test.ts +++ b/test/applySelectPermittedFields.test.ts @@ -1,6 +1,6 @@ import { abilityBuilder } from './abilities' -import { applySelectPermittedFields, } from '../dist' +import { applySelectPermittedFields } from '../src/applySelectPermittedFields' diff --git a/test/applyWhereQuery.test.ts b/test/applyWhereQuery.test.ts index 78a4f3b..fbd4a0f 100644 --- a/test/applyWhereQuery.test.ts +++ b/test/applyWhereQuery.test.ts @@ -1,6 +1,6 @@ import { abilityBuilder } from './abilities' -import { applyWhereQuery } from '../dist' +import { applyWhereQuery } from '../src/applyWhereQuery' describe('apply where query', () => { diff --git a/test/extension.test.ts b/test/extension.test.ts index 996af62..c0d6945 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -3,7 +3,7 @@ import { seedClient } from './client' import { seed } from './seed' import { abilityBuilder } from './abilities' -import { applyCaslToQuery, useCaslAbilities } from '../dist' +import { useCaslAbilities } from '../src/index' beforeEach(async () => { @@ -240,7 +240,7 @@ describe('prisma extension casl', () => { }) }) describe('findMany', () => { - it.only('filters only readable posts', async () => { + it('filters only readable posts', async () => { const { can, build } = abilityBuilder() can('read', 'Post', { thread: { diff --git a/test/subset.test.ts b/test/subset.test.ts index 16b3b4d..1241c2c 100644 --- a/test/subset.test.ts +++ b/test/subset.test.ts @@ -1,5 +1,5 @@ -import { isSubset } from '../dist' +import { isSubset } from '../src/helpers' describe('subset', () => {