Skip to content

Commit

Permalink
feat: ✨ prisma extension casl
Browse files Browse the repository at this point in the history
  • Loading branch information
dennemark committed Jul 25, 2024
1 parent db726be commit 8ece2c2
Show file tree
Hide file tree
Showing 23 changed files with 4,627 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dev.db
*.env*
pnpm-lock.yaml
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Prisma Extension CASL



[Prisma client extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions) that utilizes [CASL](https://casl.js.org/) to enforce authorization logic on most queries.

> Please be very careful using this library in production! Test your endpoints on your own and raise an issue if some case is not supported by this library!
Supports mainly/only CRUD actions `create`, `read`, `update` and `delete`, which allows us to generate and transform `include`, `select` and `where` queries to enforce nested filtering.
Mutating queries will throw errors in a similar format as CASL. `It's not allowed to "update" "email" on "User"`.

### Examples

Now how does it work?

```ts
const { can, build } = abilityBuilder()
can('read', 'Post', {
thread: {
creatorId: 0
}
})
can('read', 'Thread', 'id')
const caslClient = prismaClient.$extends(
useCaslAbilities(build())
)
const result = await caslClient.post.findMany({
include: {
thread: true
}
})
/**
* creates a query under the hood with assistance of @casl/prisma
*
* and even adds a proper select query to thread
*
*{
* where: {
* AND: [{
* OR: [{
* thread: {
* creatorId: 0
* }
* }]
* }]
* }
* include: {
* thread: {
* select: {
* id: true
* }
* }
* }
* }
*/
```
Mutations will only run, if abilities allow it.

```ts
const { can, build } = abilityBuilder()
can('update', 'Post')
cannot('update', 'Post', 'text')
const caslClient = prismaClient.$extends(
useCaslAbilities(build())
)
const result = await caslClient.post.update({ data: { text: '-' }, where: { id: 0 }})
/**
* will throw an error
* because update on text is not allowed
*/
```

Check out tests for some other examples.


### Limitations

- A limitation is the necessary use of `create`, `read`, `update` and `delete` as actions for nested queries. Since this allows us to deal with nested creations or updates. However there is an option to specify a custom `caslAction` for the highest query. It has no typing and is not tested yet.

```ts
client.user.findUnique({ where: { id: 0 }, caslAction: 'customAction' })
```

- When using prisma probably no one will use columns named `data`, `create`, `update` or `where`. However, if this should be the case, then this library most probably won't work.

- Currently the following case is not supported. It is still possible to read `email`. Waithing for reply [here](https://github.com/stalniy/casl/discussions/948).

```ts
can('read', 'User', {
id: 1
})
cannot('read', 'User', 'email', {
id: 0
})
```
27 changes: 27 additions & 0 deletions dist/index.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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<PrismaCaslOperation, {
action: DefaultCaslAction;
dataQuery: boolean;
whereQuery: boolean;
includeSelectQuery: boolean;
}>;
declare const useCaslAbilities: (abilities: PureAbility<AbilityTuple, PrismaQuery>) => (client: any) => {
$extends: {
extArgs: _prisma_client_runtime_library.InternalArgs<unknown, unknown, {}, unknown>;
};
};
declare function applyCaslToQuery(operation: any, args: any, abilities: PureAbility<AbilityTuple, PrismaQuery>, model: Prisma.ModelName): any;
declare function applyDataQuery(abilities: PureAbility<AbilityTuple, PrismaQuery>, args: any, action: string, model: string): any;
declare function applyWhereQuery(abilities: PureAbility<AbilityTuple, PrismaQuery>, args: any, action: string, model: string, relation?: string): any;
declare const applySelectPermittedFields: (abilities: PureAbility<AbilityTuple, PrismaQuery>, args: any, model: string) => any;
declare const applyIncludeSelectQuery: (abilities: PureAbility<AbilityTuple, PrismaQuery>, 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 };
27 changes: 27 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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<PrismaCaslOperation, {
action: DefaultCaslAction;
dataQuery: boolean;
whereQuery: boolean;
includeSelectQuery: boolean;
}>;
declare const useCaslAbilities: (abilities: PureAbility<AbilityTuple, PrismaQuery>) => (client: any) => {
$extends: {
extArgs: _prisma_client_runtime_library.InternalArgs<unknown, unknown, {}, unknown>;
};
};
declare function applyCaslToQuery(operation: any, args: any, abilities: PureAbility<AbilityTuple, PrismaQuery>, model: Prisma.ModelName): any;
declare function applyDataQuery(abilities: PureAbility<AbilityTuple, PrismaQuery>, args: any, action: string, model: string): any;
declare function applyWhereQuery(abilities: PureAbility<AbilityTuple, PrismaQuery>, args: any, action: string, model: string, relation?: string): any;
declare const applySelectPermittedFields: (abilities: PureAbility<AbilityTuple, PrismaQuery>, args: any, model: string) => any;
declare const applyIncludeSelectQuery: (abilities: PureAbility<AbilityTuple, PrismaQuery>, 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 };
Loading

0 comments on commit 8ece2c2

Please sign in to comment.