Skip to content

Commit 4ac2531

Browse files
committed
feat: extends createPrisma types to accept AppAbility as generic type
updates docs
1 parent 7071c8e commit 4ac2531

File tree

5 files changed

+46
-29
lines changed

5 files changed

+46
-29
lines changed

packages/casl-prisma/README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,18 @@ pnpm add @casl/prisma @casl/ability
2121

2222
## Usage
2323

24-
This package is a bit different from all others because it provides a custom `PrismaAbility` class that is configured to check permissions using Prisma [WhereInput](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#where):
24+
This package is a bit different from all others because it provides a custom `createPrismaAbility` factory function that is configured to check permissions using Prisma [WhereInput](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#where):
2525

2626
```ts
2727
import { User, Post, Prisma } from '@prisma/client';
28-
import { AbilityClass, AbilityBuilder, subject } from '@casl/ability';
29-
import { PrismaAbility, Subjects } from '@casl/prisma';
28+
import { PureAbility, AbilityBuilder, subject } from '@casl/ability';
29+
import { createPrismaAbility, PrismaQuery, Subjects } from '@casl/prisma';
3030

31-
type AppAbility = PrismaAbility<[string, Subjects<{
31+
type AppAbility = PureAbility<[string, Subjects<{
3232
User: User,
3333
Post: Post
34-
}>]>;
35-
const AppAbility = PrismaAbility as AbilityClass<AppAbility>;
36-
const { can, cannot, build } = new AbilityBuilder(AppAbility);
34+
}>], PrismaQuery>;
35+
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createPrismaAbility);
3736

3837
can('read', 'Post', { authorId: 1 });
3938
cannot('read', 'Post', { title: { startsWith: '[WIP]:' } });
@@ -74,7 +73,7 @@ const accessiblePosts = await prisma.post.findMany({
7473
});
7574
```
7675

77-
That function accepts `PrismaAbility` instance and `action` (defaults to `read`), returns an object with keys that corresponds to Prisma model names and values being aggregated from permission rules `WhereInput` objects.
76+
That function accepts `Ability` instance and `action` (defaults to `read`), returns an object with keys that corresponds to Prisma model names and values being aggregated from permission rules `WhereInput` objects.
7877

7978
**Important**: in case user doesn't have ability to access any posts, `accessibleBy` throws `ForbiddenError`, so be ready to catch it!
8079

@@ -128,6 +127,16 @@ type AppSubjects = Subjects<{
128127
}>; // 'User' | Model<User, 'User'>
129128
```
130129

130+
To support rule definition for `all`, we just need to explicitly do it:
131+
132+
```ts
133+
type AppSubjects = 'all' | Subjects<{
134+
User: User
135+
}>; // 'User' | Model<User, 'User'>
136+
137+
type AppAbility = PureAbility<[string, AppSubjects], PrismaQuery>;
138+
```
139+
131140
## Custom PrismaClient output path
132141

133142
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:
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import { AbilityOptionsOf, PureAbility, RawRuleOf } from '@casl/ability'
1+
import { PureAbility } from '@casl/ability'
22
import { User, Post } from '@prisma/client'
3-
import { createPrismaAbility, PrismaQuery, Subjects } from '../src'
3+
import { PrismaQuery, Subjects } from '../src'
44

55
export type AppAbility = PureAbility<[string, 'all' | Subjects<{
66
User: User,
77
Post: Post
88
}>], PrismaQuery>
9-
10-
type AppAbilityFactory = (
11-
rules?: RawRuleOf<AppAbility>[],
12-
options?: AbilityOptionsOf<AppAbility>
13-
) => AppAbility
14-
export const createAppAbility = createPrismaAbility as AppAbilityFactory

packages/casl-prisma/spec/PrismaAbility.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { AbilityBuilder, PureAbility, subject } from '@casl/ability'
22
import { User, Post, Prisma } from '@prisma/client'
3-
import { Model as M, PrismaQuery } from '../src'
4-
import { createAppAbility } from './AppAbility'
3+
import { createPrismaAbility, Model as M, PrismaQuery } from '../src'
4+
import { AppAbility } from './AppAbility'
55

66
describe('PrismaAbility', () => {
77
it('uses PrismaQuery to evaluate conditions', () => {
8-
const { can, build } = new AbilityBuilder(createAppAbility)
8+
const { can, build } = new AbilityBuilder<AppAbility>(createPrismaAbility)
99
can('read', 'Post', {
1010
authorId: { notIn: [1, 2] }
1111
})
@@ -24,7 +24,7 @@ describe('PrismaAbility', () => {
2424

2525
describe('types', () => {
2626
it('ensures that only specified models can be used as subjects', () => {
27-
expect(createAppAbility([
27+
expect(createPrismaAbility<AppAbility>([
2828
{
2929
action: 'read',
3030
subject: 'Post'
@@ -54,7 +54,7 @@ describe('PrismaAbility', () => {
5454
})
5555

5656
it('provides type validation in `AbilityBuilder`', () => {
57-
const { can } = new AbilityBuilder(createAppAbility)
57+
const { can } = new AbilityBuilder<AppAbility>(createPrismaAbility)
5858

5959
can('read', 'Post', {
6060
// @ts-expect-error referencing User property

packages/casl-prisma/spec/accessibleBy.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { ForbiddenError } from '@casl/ability'
2-
import { accessibleBy } from '../src'
3-
import { createAppAbility } from './AppAbility'
2+
import { accessibleBy, createPrismaAbility } from '../src'
3+
import { AppAbility } from './AppAbility'
44

55
describe('accessibleBy', () => {
6-
const ability = createAppAbility([
6+
const ability = createPrismaAbility<AppAbility>([
77
{
88
action: 'read',
99
subject: 'Post',
Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
1-
import { AbilityOptions, AbilityTuple, fieldPatternMatcher, PureAbility, RawRuleFrom } from '@casl/ability';
1+
import {
2+
AbilityOptions,
3+
AbilityOptionsOf,
4+
AbilityTuple,
5+
fieldPatternMatcher,
6+
PureAbility,
7+
RawRuleFrom,
8+
RawRuleOf
9+
} from '@casl/ability';
210
import { prismaQuery } from './prisma/prismaQuery';
311

412
export function createAbilityFactory<
513
TModelName extends string,
614
TPrismaQuery extends Record<string, any>
715
>() {
8-
return function createAbility<
16+
function createAbility<
17+
T extends PureAbility<any, TPrismaQuery>
18+
>(rules?: RawRuleOf<T>[], options?: AbilityOptionsOf<T>): T;
19+
function createAbility<
920
A extends AbilityTuple = [string, TModelName],
1021
C extends TPrismaQuery = TPrismaQuery
1122
>(
1223
rules?: RawRuleFrom<A, C>[],
1324
options?: AbilityOptions<A, C>
14-
) {
15-
return new PureAbility<A, C>(rules, {
25+
): PureAbility<A, C>;
26+
function createAbility(rules: any[] = [], options = {}): PureAbility<any, any> {
27+
return new PureAbility(rules, {
1628
...options,
1729
conditionsMatcher: prismaQuery,
1830
fieldMatcher: fieldPatternMatcher,
1931
});
20-
};
32+
}
33+
34+
return createAbility;
2135
}

0 commit comments

Comments
 (0)