-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ filter results in app and not within db query
- Loading branch information
Showing
19 changed files
with
721 additions
and
523 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"editor.tabSize": 2, | ||
"editor.formatOnSave": true, | ||
"editor.formatOnSaveMode": "file", | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll.eslint": "explicit", | ||
"source.sortImports": "explicit" | ||
}, | ||
|
||
"[jsonc]": { | ||
"editor.defaultFormatter": "vscode.typescript-language-features" | ||
}, | ||
"[javascript]": { | ||
"editor.defaultFormatter": "vscode.typescript-language-features" | ||
}, | ||
"[javascriptreact]": { | ||
"editor.defaultFormatter": "vscode.typescript-language-features" | ||
}, | ||
"[typescript]": { | ||
"editor.defaultFormatter": "vscode.typescript-language-features" | ||
}, | ||
"[typescriptreact]": { | ||
"editor.defaultFormatter": "vscode.typescript-language-features" | ||
}, | ||
"typescript.preferences.importModuleSpecifierEnding": "auto" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,154 +1,100 @@ | ||
# 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. | ||
[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. | ||
|
||
> [!CAUTION] | ||
> | ||
> WIP - some abstractions might change in the future and lead to different interpretation of CASL rules. | ||
> | ||
> 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"`. | ||
- Supports only CRUD actions `create`, `read`, `update` and `delete`. | ||
- Rule conditions are applied via `accessibleBy` and if `include` or `select` are used, this will even be nested. | ||
- Mutating queries will throw errors in a similar format as CASL. `It's not allowed to "update" "email" on "User"`. | ||
- The query result with nested entries is filtered by `read` ability. | ||
- On nested `connect`, `disconnect`, `upsert` or `connectOrCreate` mutation queries the client assumes an `update` action for insertion or connection. | ||
|
||
### 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 | ||
* } | ||
* } | ||
* } | ||
* } | ||
*/ | ||
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 | ||
* | ||
*{ | ||
* where: { | ||
* AND: [{ | ||
* OR: [{ | ||
* thread: { | ||
* creatorId: 0 | ||
* } | ||
* }] | ||
* }] | ||
* } | ||
* include: { | ||
* thread: true | ||
* } | ||
* | ||
* and result will be filtered and should look like | ||
* { id: 0, threadId: 0, thread: { id: 0 } } | ||
*/ | ||
``` | ||
|
||
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 | ||
*/ | ||
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 and Constraints | ||
|
||
#### Nested mutations use `update` connection | ||
|
||
On nested `connect`, `disconnect`, `upsert` or `connectOrCreate` mutation queries the client assumes an `update` action for insertion or connection. | ||
|
||
|
||
#### CRUD actions | ||
|
||
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' }) | ||
``` | ||
|
||
#### Avoid columns with prisma naming | ||
|
||
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. | ||
When using prisma probably no one will use columns named `data`, `create`, `update`, `select` or `where`. However, if this should be the case, then this library most probably won't work. | ||
|
||
#### Limit fields via conditions | ||
The main use case is allowing more fields for some users: | ||
```ts | ||
can('read', 'User', 'email') | ||
can('read', 'User', ['email', 'id'], { | ||
id: 0 | ||
}) | ||
client.user.findMany() | ||
// will return all users with email | ||
// however | ||
client.user.findMany({ id: 0 }) | ||
// will show email and id! | ||
``` | ||
If fields should only be permitted on certain conditions, they will only be accessible, if these conditions apply. The reason is, that we do not know if it contradicts another rule. | ||
See this example: | ||
#### Relational conditions need read rights on relation and include statment | ||
|
||
To filter fields, the data of the conditions needs to be available, since filtering does not happen on the database but in our app. | ||
|
||
```ts | ||
can('read', 'User') | ||
can('read', 'User', 'email', { | ||
id: 0 | ||
}) | ||
can('read', 'User', 'id', { | ||
id: 1 | ||
}) | ||
client.user.findMany() | ||
// will return all users and more than the email property | ||
// since we cannot check if id is 0 or 1 | ||
|
||
// however if our query matches the condition its rule will be used | ||
client.user.findMany({ id: 0 }) | ||
// will restrict access to email prop | ||
can("read", "User", "email", { | ||
post: { | ||
ownerId: 0, | ||
}, | ||
}); | ||
can("read", "Post", ["id"]); | ||
client.user.findMany(); // [] | ||
|
||
client.user.findMany({ include: { post: true } }); // [{ email: "-", post: { id: 0 } }] | ||
``` | ||
This makes DX a bit inconvenient, since we have to use our condition within our query. | ||
|
||
|
||
#### Nested fields and wildcards are not supported | ||
|
||
`can('read', 'User', ['nested.field', 'field.*'])` won't work. Although a wildcard could be useful in the future. | ||
#### Nested fields and wildcards are not supported / tested | ||
|
||
#### Conditionally filter fields with cannot | ||
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 | ||
}) | ||
``` | ||
In real world application, rather consider this: | ||
```ts | ||
cannot('read', 'User', 'email') | ||
can('read', 'User', { | ||
id: userId | ||
}) | ||
``` | ||
`can('read', 'User', ['nested.field', 'field.*'])` probably won't work. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.