Skip to content

Commit

Permalink
feat: adds support for custom generate prisma client
Browse files Browse the repository at this point in the history
  • Loading branch information
stalniy committed Sep 19, 2022
1 parent b57c7a2 commit 832a50e
Show file tree
Hide file tree
Showing 16 changed files with 235 additions and 116 deletions.
60 changes: 59 additions & 1 deletion packages/casl-prisma/README.md
Expand Up @@ -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<Model<User, 'User'>>;
Expand All @@ -128,6 +128,64 @@ type AppSubjects = Subjects<{
}>; // 'User' | Model<User, 'User'>
```

## 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<K> extends keyof PrismaClient
? Extract<Parameters<PrismaClient[Uncapitalize<K>]['findFirst']>[0], { where?: any }>["where"]
: never
};

type WhereInput<TModelName extends Prisma.ModelName> = Extract<ModelWhereInput[TModelName], Record<any, any>>;

interface PrismaQueryTypeFactory extends hkt.GenericFactory {
produce: WhereInput<ExtractModelName<this[0], ModelName>>
}

type PrismaModel = Model<Record<string, any>, string>;
// Higher Order type that allows to infer passed in Prisma Model name
export type PrismaQuery<T extends PrismaModel = PrismaModel> =
WhereInput<ExtractModelName<T, ModelName>> & hkt.Container<PrismaQueryTypeFactory>;

type WhereInputPerModel = {
[K in ModelName]: WhereInput<K>;
};

const createPrismaAbility = createAbilityFactory<ModelName, PrismaQuery>();
const accessibleBy = createAccessibleByFactory<WhereInputPerModel, PrismaQuery>();

export {
createPrismaAbility,
accessibleBy,
};
```

## Want to help?

Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for [contributing].
Expand Down
16 changes: 11 additions & 5 deletions packages/casl-prisma/package.json
Expand Up @@ -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": {
Expand All @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/casl-prisma/runtime.d.ts
@@ -0,0 +1 @@
export * from './dist/types/runtime';
1 change: 1 addition & 0 deletions packages/casl-prisma/runtime.js
@@ -0,0 +1 @@
module.exports = require('./dist/es6c/runtime');
16 changes: 10 additions & 6 deletions 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<AppAbility>
}>], PrismaQuery>

type AppAbilityFactory = (
rules?: RawRuleOf<AppAbility>[],
options?: AbilityOptionsOf<AppAbility>
) => AppAbility
export const createAppAbility = createPrismaAbility as AppAbilityFactory
12 changes: 6 additions & 6 deletions 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] }
})
Expand All @@ -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'
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions 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',
Expand Down
21 changes: 0 additions & 21 deletions packages/casl-prisma/src/PrismaAbility.ts

This file was deleted.

@@ -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;
Expand Down Expand Up @@ -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<K>;
export const createAccessibleByFactory = <
TResult extends Record<string, unknown>,
TPrismaQuery
>() => {
return function accessibleBy(ability: PureAbility<any, TPrismaQuery>, action = 'read'): TResult {
return new Proxy({
_ability: ability,
_action: action
}, proxyHandlers) as unknown as TResult;
};
};

export function accessibleBy(ability: PrismaAbility<any, any>, action = 'read'): AccessibleQuery {
return createQuery(ability, action);
}
21 changes: 21 additions & 0 deletions 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<string, any>
>() {
return function createAbility<
A extends AbilityTuple = [string, TModelName],
C extends TPrismaQuery = TPrismaQuery
>(
rules?: RawRuleFrom<A, C>[],
options?: AbilityOptions<A, C>
) {
return new PureAbility<A, C>(rules, {
...options,
conditionsMatcher: prismaQuery,
fieldMatcher: fieldPatternMatcher,
});
};
}
40 changes: 35 additions & 5 deletions 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<ModelName, PrismaQuery>();
const accessibleBy = createAccessibleByFactory<WhereInputPerModel, PrismaQuery>();

export {
createPrismaAbility,
accessibleBy,
};

type ExtendedAbilityTuple<T extends AbilityTuple> = [T[0], 'all' | T[1]];

/**
* @deprecated use createPrismaAbility instead
*/
export class PrismaAbility<
A extends AbilityTuple = [string, ModelName],
C extends PrismaQuery = PrismaQuery
> extends PureAbility<ExtendedAbilityTuple<A>, C> {
constructor(
rules?: RawRuleFrom<ExtendedAbilityTuple<A>, C>[],
options?: AbilityOptions<ExtendedAbilityTuple<A>, C>
) {
super(rules, {
conditionsMatcher: prismaQuery,
fieldMatcher: fieldPatternMatcher,
...options,
});
}
}
39 changes: 0 additions & 39 deletions packages/casl-prisma/src/prisma/PrismaQuery.ts

This file was deleted.

29 changes: 29 additions & 0 deletions 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, TName extends string> = T & ForcedSubject<TName>;
export type Subjects<T extends Partial<Record<string, Record<string, unknown>>>> =
| keyof T
| { [K in keyof T]: Model<T[K], K & string> }[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<TModelName>
? TObject['__caslSubjectType__']
: TObject extends { __typename: TModelName }
? TObject['__typename']
: TModelName;

0 comments on commit 832a50e

Please sign in to comment.