diff --git a/packages/casl-prisma/README.md b/packages/casl-prisma/README.md index ba4c01ebd..41e5d5742 100644 --- a/packages/casl-prisma/README.md +++ b/packages/casl-prisma/README.md @@ -105,7 +105,7 @@ It's a generic type that provides `Prisma.ModelWhereInput` in generic way. We ne ```ts import { User } from '@prisma/client'; -import { Model } from '@casl/prisma'; +import { Model, PrismaQuery } from '@casl/prisma'; // almost the same as Prisma.UserWhereInput except that it's a higher order type type UserWhereInput = PrismaQuery>; @@ -128,6 +128,64 @@ type AppSubjects = Subjects<{ }>; // 'User' | Model ``` +## Custom PrismaClient output path + +Prisma allows [to generate client into a custom directory](https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/generating-prisma-client#using-a-custom-output-path) in this case `@prisma/client` doesn't re-export needed types anymore and `@casl/prisma` cannot automatically detect and infer types. In this case, we need to provide required types manually. Let's assume that we have the next configuration: + +```prisma +generator client { + provider = "prisma-client-js" + output = "../src/generated/client" +} +``` + +Then we need to create a custom file for casl-prisma integration: + +```ts +// src/casl-prisma.ts +import { + createAbilityFactory, + createAccessibleByFactory, + prismaQuery, + ExtractModelName, + Model +} from "@casl/prisma/runtime"; +import { hkt, AbilityOptions, AbilityTuple, fieldPatternMatcher, PureAbility, RawRuleFrom } from "@casl/ability"; + +import type { Prisma, PrismaClient } from "./generated/client"; +import type { ExtractModelName, Model } from "./prisma/prismaQuery"; + +type ModelName = Prisma.ModelName; +type ModelWhereInput = { + [K in Prisma.ModelName]: Uncapitalize extends keyof PrismaClient + ? Extract]['findFirst']>[0], { where?: any }>["where"] + : never +}; + +type WhereInput = Extract>; + +interface PrismaQueryTypeFactory extends hkt.GenericFactory { + produce: WhereInput> +} + +type PrismaModel = Model, string>; +// Higher Order type that allows to infer passed in Prisma Model name +export type PrismaQuery = + WhereInput> & hkt.Container; + +type WhereInputPerModel = { + [K in ModelName]: WhereInput; +}; + +const createPrismaAbility = createAbilityFactory(); +const accessibleBy = createAccessibleByFactory(); + +export { + createPrismaAbility, + accessibleBy, +}; +``` + ## Want to help? Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for [contributing]. diff --git a/packages/casl-prisma/package.json b/packages/casl-prisma/package.json index a29d951cc..698f3ec0d 100644 --- a/packages/casl-prisma/package.json +++ b/packages/casl-prisma/package.json @@ -10,6 +10,11 @@ "types": "./dist/types/index.d.ts", "import": "./dist/es6m/index.mjs", "require": "./dist/es6c/index.js" + }, + "./runtime": { + "types": "./dist/types/runtime.d.ts", + "import": "./dist/es6m/runtime.mjs", + "require": "./dist/es6c/runtime.js" } }, "repository": { @@ -22,8 +27,9 @@ }, "homepage": "https://casl.js.org", "scripts": { - "prebuild": "rm -rf dist/* && npm run build.types", - "build": "BUILD_TYPES=es6m,es6c dx rollup -e @casl/ability/extra,@casl/ability,@prisma/client,@ucast/core,@ucast/js", + "prebuild": "rm -rf dist/* && npm run build.types && npm run build.runtime", + "build.runtime": "BUILD_TYPES=es6m,es6c dx rollup -i src/runtime.ts -e @casl/ability/extra,@casl/ability,@prisma/client,@ucast/core,@ucast/js", + "build": "BUILD_TYPES=es6m,es6c dx rollup -e @casl/ability/extra,@casl/ability,@prisma/client,@ucast/core,@ucast/js,./runtime", "build.types": "dx tsc", "lint": "dx eslint src/ spec/", "test": "dx jest", @@ -47,14 +53,14 @@ "devDependencies": { "@casl/ability": "^6.0.0", "@casl/dx": "workspace:^1.0.0", - "@prisma/client": "^4.0.0", + "@prisma/client": "^4.3.1", "@types/jest": "^28.0.0", - "prisma": "^4.0.0" + "prisma": "^4.3.1" }, "files": [ "dist", "*.d.ts", - "index.js" + "runtime.js" ], "dependencies": { "@ucast/core": "^1.10.0", diff --git a/packages/casl-prisma/runtime.d.ts b/packages/casl-prisma/runtime.d.ts new file mode 100644 index 000000000..56a77b903 --- /dev/null +++ b/packages/casl-prisma/runtime.d.ts @@ -0,0 +1 @@ +export * from './dist/types/runtime'; diff --git a/packages/casl-prisma/runtime.js b/packages/casl-prisma/runtime.js new file mode 100644 index 000000000..7079eafa5 --- /dev/null +++ b/packages/casl-prisma/runtime.js @@ -0,0 +1 @@ +module.exports = require('./dist/es6c/runtime'); diff --git a/packages/casl-prisma/spec/AppAbility.ts b/packages/casl-prisma/spec/AppAbility.ts index 50a3e98a3..ebc19f7e5 100644 --- a/packages/casl-prisma/spec/AppAbility.ts +++ b/packages/casl-prisma/spec/AppAbility.ts @@ -1,10 +1,14 @@ -import { AbilityClass } from '@casl/ability' +import { AbilityOptionsOf, PureAbility, RawRuleOf } from '@casl/ability' import { User, Post } from '@prisma/client' -import { PrismaAbility, Subjects } from '../src' +import { createPrismaAbility, PrismaQuery, Subjects } from '../src' -export type AppAbility = PrismaAbility<[string, Subjects<{ +export type AppAbility = PureAbility<[string, 'all' | Subjects<{ User: User, Post: Post -}>]> -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const AppAbility = PrismaAbility as AbilityClass +}>], PrismaQuery> + +type AppAbilityFactory = ( + rules?: RawRuleOf[], + options?: AbilityOptionsOf +) => AppAbility +export const createAppAbility = createPrismaAbility as AppAbilityFactory diff --git a/packages/casl-prisma/spec/PrismaAbility.spec.ts b/packages/casl-prisma/spec/PrismaAbility.spec.ts index 77d7ec031..0fddcc40f 100644 --- a/packages/casl-prisma/spec/PrismaAbility.spec.ts +++ b/packages/casl-prisma/spec/PrismaAbility.spec.ts @@ -1,11 +1,11 @@ -import { AbilityBuilder, subject } from '@casl/ability' +import { AbilityBuilder, PureAbility, subject } from '@casl/ability' import { User, Post, Prisma } from '@prisma/client' import { Model as M, PrismaQuery } from '../src' -import { AppAbility } from './AppAbility' +import { createAppAbility } from './AppAbility' describe('PrismaAbility', () => { it('uses PrismaQuery to evaluate conditions', () => { - const { can, build } = new AbilityBuilder(AppAbility) + const { can, build } = new AbilityBuilder(createAppAbility) can('read', 'Post', { authorId: { notIn: [1, 2] } }) @@ -24,7 +24,7 @@ describe('PrismaAbility', () => { describe('types', () => { it('ensures that only specified models can be used as subjects', () => { - expect(new AppAbility([ + expect(createAppAbility([ { action: 'read', subject: 'Post' @@ -50,11 +50,11 @@ describe('PrismaAbility', () => { action: 'read', subject: 'all' } - ])).toBeInstanceOf(AppAbility) + ])).toBeInstanceOf(PureAbility) }) it('provides type validation in `AbilityBuilder`', () => { - const { can } = new AbilityBuilder(AppAbility) + const { can } = new AbilityBuilder(createAppAbility) can('read', 'Post', { // @ts-expect-error referencing User property diff --git a/packages/casl-prisma/spec/accessibleBy.spec.ts b/packages/casl-prisma/spec/accessibleBy.spec.ts index e119f97a7..e71df42eb 100644 --- a/packages/casl-prisma/spec/accessibleBy.spec.ts +++ b/packages/casl-prisma/spec/accessibleBy.spec.ts @@ -1,9 +1,9 @@ import { ForbiddenError } from '@casl/ability' import { accessibleBy } from '../src' -import { AppAbility } from './AppAbility' +import { createAppAbility } from './AppAbility' describe('accessibleBy', () => { - const ability = new AppAbility([ + const ability = createAppAbility([ { action: 'read', subject: 'Post', diff --git a/packages/casl-prisma/src/PrismaAbility.ts b/packages/casl-prisma/src/PrismaAbility.ts deleted file mode 100644 index b55cf2281..000000000 --- a/packages/casl-prisma/src/PrismaAbility.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { AbilityOptions, AbilityTuple, fieldPatternMatcher, PureAbility, RawRuleFrom } from '@casl/ability'; -import { PrismaQuery, prismaQuery } from './prisma/PrismaQuery'; - -type ExtendedAbilityTuple = [T[0], 'all' | T[1]]; - -export class PrismaAbility< - A extends AbilityTuple = [string, Prisma.ModelName], - C extends PrismaQuery = PrismaQuery -> extends PureAbility, C> { - constructor( - rules?: RawRuleFrom, C>[], - options?: AbilityOptions, C> - ) { - super(rules, { - conditionsMatcher: prismaQuery, - fieldMatcher: fieldPatternMatcher, - ...options, - }); - } -} diff --git a/packages/casl-prisma/src/accessibleBy.ts b/packages/casl-prisma/src/accessibleByFactory.ts similarity index 60% rename from packages/casl-prisma/src/accessibleBy.ts rename to packages/casl-prisma/src/accessibleByFactory.ts index 3ae44c926..ad1344cf9 100644 --- a/packages/casl-prisma/src/accessibleBy.ts +++ b/packages/casl-prisma/src/accessibleByFactory.ts @@ -1,8 +1,5 @@ -import { Prisma } from '@prisma/client'; import { rulesToQuery } from '@casl/ability/extra'; -import { AnyAbility, ForbiddenError } from '@casl/ability'; -import { PrismaAbility } from './PrismaAbility'; -import { WhereInput } from './prisma/PrismaQuery'; +import { AnyAbility, ForbiddenError, PureAbility } from '@casl/ability'; function convertToPrismaQuery(rule: AnyAbility['rules'][number]) { return rule.inverted ? { NOT: rule.conditions } : rule.conditions; @@ -33,17 +30,15 @@ const proxyHandlers: ProxyHandler<{ _ability: AnyAbility, _action: string }> = { return prismaQuery; } }; -function createQuery(ability: PrismaAbility, action: string) { - return new Proxy({ - _ability: ability, - _action: action - }, proxyHandlers) as unknown as AccessibleQuery; -} -type AccessibleQuery = { - [K in Prisma.ModelName]: WhereInput; +export const createAccessibleByFactory = < + TResult extends Record, + TPrismaQuery +>() => { + return function accessibleBy(ability: PureAbility, action = 'read'): TResult { + return new Proxy({ + _ability: ability, + _action: action + }, proxyHandlers) as unknown as TResult; + }; }; - -export function accessibleBy(ability: PrismaAbility, action = 'read'): AccessibleQuery { - return createQuery(ability, action); -} diff --git a/packages/casl-prisma/src/createAbilityFactory.ts b/packages/casl-prisma/src/createAbilityFactory.ts new file mode 100644 index 000000000..95aa4fded --- /dev/null +++ b/packages/casl-prisma/src/createAbilityFactory.ts @@ -0,0 +1,21 @@ +import { AbilityOptions, AbilityTuple, fieldPatternMatcher, PureAbility, RawRuleFrom } from '@casl/ability'; +import { prismaQuery } from './prisma/prismaQuery'; + +export function createAbilityFactory< + TModelName extends string, + TPrismaQuery extends Record +>() { + return function createAbility< + A extends AbilityTuple = [string, TModelName], + C extends TPrismaQuery = TPrismaQuery + >( + rules?: RawRuleFrom[], + options?: AbilityOptions + ) { + return new PureAbility(rules, { + ...options, + conditionsMatcher: prismaQuery, + fieldMatcher: fieldPatternMatcher, + }); + }; +} diff --git a/packages/casl-prisma/src/index.ts b/packages/casl-prisma/src/index.ts index 47d269924..55718cba0 100644 --- a/packages/casl-prisma/src/index.ts +++ b/packages/casl-prisma/src/index.ts @@ -1,5 +1,35 @@ -export { prismaQuery } from './prisma/PrismaQuery'; -export type { PrismaQuery, Model, Subjects } from './prisma/PrismaQuery'; -export { accessibleBy } from './accessibleBy'; -export { PrismaAbility } from './PrismaAbility'; -export { ParsingQueryError } from './errors/ParsingQueryError'; +import { AbilityOptions, AbilityTuple, fieldPatternMatcher, PureAbility, RawRuleFrom } from '@casl/ability'; +import { createAbilityFactory, createAccessibleByFactory, prismaQuery } from './runtime'; +import { WhereInputPerModel, ModelName, PrismaQuery } from './prismaClientBoundTypes'; + +export type { PrismaQuery } from './prismaClientBoundTypes'; +export { prismaQuery, Model, Subjects, ParsingQueryError } from './runtime'; + +const createPrismaAbility = createAbilityFactory(); +const accessibleBy = createAccessibleByFactory(); + +export { + createPrismaAbility, + accessibleBy, +}; + +type ExtendedAbilityTuple = [T[0], 'all' | T[1]]; + +/** + * @deprecated use createPrismaAbility instead + */ +export class PrismaAbility< + A extends AbilityTuple = [string, ModelName], + C extends PrismaQuery = PrismaQuery +> extends PureAbility, C> { + constructor( + rules?: RawRuleFrom, C>[], + options?: AbilityOptions, C> + ) { + super(rules, { + conditionsMatcher: prismaQuery, + fieldMatcher: fieldPatternMatcher, + ...options, + }); + } +} diff --git a/packages/casl-prisma/src/prisma/PrismaQuery.ts b/packages/casl-prisma/src/prisma/PrismaQuery.ts deleted file mode 100644 index d1c0515b5..000000000 --- a/packages/casl-prisma/src/prisma/PrismaQuery.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { PrismaClient, Prisma } from '@prisma/client'; -import { AnyInterpreter, createTranslatorFactory } from '@ucast/core'; -import { ForcedSubject, hkt } from '@casl/ability'; -import { PrismaQueryParser } from './PrismaQueryParser'; -import { interpretPrismaQuery } from './interpretPrismaQuery'; - -type ModelDelegates = { - [K in Prisma.ModelName]: Uncapitalize extends keyof PrismaClient - ? PrismaClient[Uncapitalize] - : never -}; -export type WhereInput = - Extract[0], { where?: any }>['where'], Record>; -type ExtractModelName = T extends { kind: Prisma.ModelName } - ? T['kind'] - : T extends ForcedSubject - ? T['__caslSubjectType__'] - : T extends { __typename: Prisma.ModelName } - ? T['__typename'] - : Prisma.ModelName; - -interface PrismaQueryTypeFactory extends hkt.GenericFactory { - produce: WhereInput> -} - -export type Model = T & ForcedSubject; -export type Subjects>>> = - | keyof T - | { [K in keyof T]: Model }[keyof T]; - -type PrismaModel = Model, Prisma.ModelName>; -export type PrismaQuery = - WhereInput> & hkt.Container; - -const parser = new PrismaQueryParser(); -export const prismaQuery = createTranslatorFactory( - parser.parse, - interpretPrismaQuery as AnyInterpreter -); diff --git a/packages/casl-prisma/src/prisma/prismaQuery.ts b/packages/casl-prisma/src/prisma/prismaQuery.ts new file mode 100644 index 000000000..a34002346 --- /dev/null +++ b/packages/casl-prisma/src/prisma/prismaQuery.ts @@ -0,0 +1,29 @@ +import { AnyInterpreter, createTranslatorFactory } from '@ucast/core'; +import { ForcedSubject } from '@casl/ability'; +import { PrismaQueryParser } from './PrismaQueryParser'; +import { interpretPrismaQuery } from './interpretPrismaQuery'; + +const parser = new PrismaQueryParser(); +export const prismaQuery = createTranslatorFactory( + parser.parse, + interpretPrismaQuery as AnyInterpreter +); + +export type Model = T & ForcedSubject; +export type Subjects>>> = + | keyof T + | { [K in keyof T]: Model }[keyof T]; + +/** + * Extracts Prisma model name from given object and possible list of all subjects + */ +export type ExtractModelName< + TObject, + TModelName extends string +> = TObject extends { kind: TModelName } + ? TObject['kind'] + : TObject extends ForcedSubject + ? TObject['__caslSubjectType__'] + : TObject extends { __typename: TModelName } + ? TObject['__typename'] + : TModelName; diff --git a/packages/casl-prisma/src/prismaClientBoundTypes.ts b/packages/casl-prisma/src/prismaClientBoundTypes.ts new file mode 100644 index 000000000..9ebacf871 --- /dev/null +++ b/packages/casl-prisma/src/prismaClientBoundTypes.ts @@ -0,0 +1,29 @@ +import type { Prisma, PrismaClient } from '@prisma/client'; +import type { hkt } from '@casl/ability'; +import type { ExtractModelName, Model } from './prisma/prismaQuery'; + +export type ModelName = Prisma.ModelName; + +type ModelWhereInput = { + [K in Prisma.ModelName]: Uncapitalize extends keyof PrismaClient + ? Extract]['findFirst']>[0], { where?: any }>['where'] + : never +}; + +// eslint-disable-next-line max-len +export type WhereInput = Extract< +ModelWhereInput[TModelName], +Record +>; + +interface PrismaQueryTypeFactory extends hkt.GenericFactory { + produce: WhereInput> +} + +type PrismaModel = Model, string>; +export type PrismaQuery = + WhereInput> & hkt.Container; + +export type WhereInputPerModel = { + [K in ModelName]: WhereInput; +}; diff --git a/packages/casl-prisma/src/runtime.ts b/packages/casl-prisma/src/runtime.ts new file mode 100644 index 000000000..9a398f07f --- /dev/null +++ b/packages/casl-prisma/src/runtime.ts @@ -0,0 +1,5 @@ +export { prismaQuery } from './prisma/prismaQuery'; +export type { Model, Subjects, ExtractModelName } from './prisma/prismaQuery'; +export { createAccessibleByFactory } from './accessibleByFactory'; +export { createAbilityFactory } from './createAbilityFactory'; +export { ParsingQueryError } from './errors/ParsingQueryError'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 000c21ca9..212f5a088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,20 +111,20 @@ importers: specifiers: '@casl/ability': ^6.0.0 '@casl/dx': workspace:^1.0.0 - '@prisma/client': ^4.0.0 + '@prisma/client': ^4.3.1 '@types/jest': ^28.0.0 '@ucast/core': ^1.10.0 '@ucast/js': ^3.0.1 - prisma: ^4.0.0 + prisma: ^4.3.1 dependencies: '@ucast/core': 1.10.0 '@ucast/js': 3.0.1 devDependencies: '@casl/ability': link:../casl-ability '@casl/dx': link:../dx - '@prisma/client': 4.0.0_prisma@4.0.0 + '@prisma/client': 4.3.1_prisma@4.3.1 '@types/jest': 28.1.6 - prisma: 4.0.0 + prisma: 4.3.1 packages/casl-react: specifiers: @@ -3598,8 +3598,8 @@ packages: '@octokit/openapi-types': 7.0.0 dev: false - /@prisma/client/4.0.0_prisma@4.0.0: - resolution: {integrity: sha512-g1h2OGoRo7anBVQ9Cw3gsbjwPtvf7i0pkGxKeZICtwkvE5CZXW+xZF4FZdmrViYkKaAShbISL0teNpu9ecpf4g==} + /@prisma/client/4.3.1_prisma@4.3.1: + resolution: {integrity: sha512-FA0/d1VMJNWqzU7WVWTNWJ+lGOLR9JUBnF73GdIPAEVo/6dWk4gHx0EmgeU+SMv4MZoxgOeTBJF2azhg7x0hMw==} engines: {node: '>=14.17'} requiresBuild: true peerDependencies: @@ -3608,16 +3608,16 @@ packages: prisma: optional: true dependencies: - '@prisma/engines-version': 3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11 - prisma: 4.0.0 + '@prisma/engines-version': 4.3.0-32.c875e43600dfe042452e0b868f7a48b817b9640b + prisma: 4.3.1 dev: true - /@prisma/engines-version/3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11: - resolution: {integrity: sha512-PiZhdD624SrYEjyLboI0X7OugNbxUzDJx9v/6ldTKuqNDVUCmRH/Z00XwDi/dgM4FlqOSO+YiUsSiSKjxxG8cw==} + /@prisma/engines-version/4.3.0-32.c875e43600dfe042452e0b868f7a48b817b9640b: + resolution: {integrity: sha512-8yWpXkQRmiSfsi2Wb/ZS5D3RFbeu/btL9Pm/gdF4phB0Lo5KGsDFMxFMgaD64mwED2nHc8ZaEJg/+4Jymb9Znw==} dev: true - /@prisma/engines/3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11: - resolution: {integrity: sha512-u/rG4lDHALolWBLr3yebZ+N2qImp3SDMcu7bHNJuRDaYvYEXy/MqfNRNEgd9GoPsXL3gofYf0VzJf2AmCG3YVw==} + /@prisma/engines/4.3.1: + resolution: {integrity: sha512-4JF/uMaEDAPdcdZNOrnzE3BvrbGpjgV0FcPT3EVoi6I86fWkloqqxBt+KcK/+fIRR0Pxj66uGR9wVH8U1Y13JA==} requiresBuild: true dev: true @@ -10147,13 +10147,13 @@ packages: ansi-styles: 5.2.0 react-is: 18.1.0 - /prisma/4.0.0: - resolution: {integrity: sha512-Dtsar03XpCBkcEb2ooGWO/WcgblDTLzGhPcustbehwlFXuTMliMDRzXsfygsgYwQoZnAUKRd1rhpvBNEUziOVw==} + /prisma/4.3.1: + resolution: {integrity: sha512-90xo06wtqil76Xsi3mNpc4Js3SdDRR5g4qb9h+4VWY4Y8iImJY6xc3PX+C9xxTSt1lr0Q89A0MLkJjd8ax6KiQ==} engines: {node: '>=14.17'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11 + '@prisma/engines': 4.3.1 dev: true /process-nextick-args/2.0.1: