@cerbos/orm-prisma 1.1.1 → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -101
- package/lib/cjs/index.d.ts +26 -19
- package/lib/cjs/index.js +326 -150
- package/lib/index.d.ts +26 -19
- package/lib/index.js +320 -144
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,172 +2,267 @@
|
|
|
2
2
|
|
|
3
3
|
An adapter library that takes a [Cerbos](https://cerbos.dev) Query Plan ([PlanResources API](https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan)) response and converts it into a [Prisma](https://prisma.io) where class object. This is designed to work alongside a project using the [Cerbos Javascript SDK](https://github.com/cerbos/cerbos-sdk-javascript).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Supported Operators
|
|
8
8
|
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
12
|
-
- `
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- `isSet`
|
|
16
|
-
- Scalar filters
|
|
17
|
-
- Atomic number operations
|
|
18
|
-
- Composite keys
|
|
9
|
+
- Logical operators: `and`, `or`, `not`
|
|
10
|
+
- Comparison operators: `eq`, `ne`, `lt`, `gt`, `lte`, `gte`, `in`
|
|
11
|
+
- Relation operators: `some`, `none`, `is`, `isNot`, `hasIntersection`
|
|
12
|
+
- String operations: `startsWith`, `endsWith`, `contains`, `isSet`
|
|
13
|
+
- Support for nested fields and relations
|
|
14
|
+
- Support for both one-to-one, one-to-many and many-to-many relationships
|
|
19
15
|
|
|
20
16
|
## Requirements
|
|
21
17
|
|
|
22
|
-
- Cerbos > v0.
|
|
18
|
+
- Cerbos > v0.40
|
|
23
19
|
- `@cerbos/http` or `@cerbos/grpc` client
|
|
24
20
|
|
|
25
|
-
##
|
|
21
|
+
## Installation
|
|
26
22
|
|
|
27
|
-
```
|
|
23
|
+
```bash
|
|
28
24
|
npm install @cerbos/orm-prisma
|
|
29
25
|
```
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
The package exports a function:
|
|
32
30
|
|
|
33
31
|
```ts
|
|
34
32
|
import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";
|
|
35
33
|
|
|
36
|
-
queryPlanToPrisma({
|
|
34
|
+
queryPlanToPrisma({
|
|
35
|
+
queryPlan, // The Cerbos query plan response
|
|
36
|
+
mapper, // Map Cerbos field names to Prisma field names
|
|
37
|
+
}): {
|
|
37
38
|
kind: PlanKind,
|
|
38
|
-
filters?: any
|
|
39
|
+
filters?: any // Prisma where conditions
|
|
39
40
|
}
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
### Basic Example
|
|
43
44
|
|
|
44
45
|
```ts
|
|
45
|
-
export enum PlanKind {
|
|
46
|
-
/**
|
|
47
|
-
* The specified action is always allowed for the principal on resources matching the input.
|
|
48
|
-
*/
|
|
49
|
-
ALWAYS_ALLOWED = "KIND_ALWAYS_ALLOWED",
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The specified action is always denied for the principal on resources matching the input.
|
|
53
|
-
*/
|
|
54
|
-
ALWAYS_DENIED = "KIND_ALWAYS_DENIED",
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* The specified action is conditionally allowed for the principal on resources matching the input.
|
|
58
|
-
*/
|
|
59
|
-
CONDITIONAL = "KIND_CONDITIONAL",
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The function reqiures the full query plan from Cerbos to be passed in an object along with a `fieldNameMapper` and option `relationMapper` if the model has relations.
|
|
64
|
-
|
|
65
|
-
A basic implementation can be as simple as:
|
|
66
|
-
|
|
67
|
-
```js
|
|
68
46
|
import { GRPC as Cerbos } from "@cerbos/grpc";
|
|
69
47
|
import { PrismaClient } from "@prisma/client";
|
|
70
|
-
|
|
71
48
|
import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";
|
|
72
49
|
|
|
73
50
|
const prisma = new PrismaClient();
|
|
74
51
|
const cerbos = new Cerbos("localhost:3592", { tls: false });
|
|
75
52
|
|
|
76
|
-
// Fetch
|
|
77
|
-
// resource type and action
|
|
53
|
+
// Fetch query plan from Cerbos
|
|
78
54
|
const queryPlan = await cerbos.planResources({
|
|
79
|
-
principal: {
|
|
80
|
-
resource: { kind: "
|
|
81
|
-
action: "view"
|
|
82
|
-
})
|
|
55
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
56
|
+
resource: { kind: "resource" },
|
|
57
|
+
action: "view",
|
|
58
|
+
});
|
|
83
59
|
|
|
84
|
-
//
|
|
60
|
+
// Convert query plan to Prisma filters
|
|
85
61
|
const result = queryPlanToPrisma({
|
|
86
62
|
queryPlan,
|
|
87
|
-
|
|
88
|
-
"request.resource.attr.
|
|
63
|
+
mapper: {
|
|
64
|
+
"request.resource.attr.title": { field: "title" },
|
|
65
|
+
"request.resource.attr.status": { field: "status" },
|
|
89
66
|
},
|
|
90
|
-
relationMapper: {
|
|
91
|
-
"request.resource.attr.aRelatedModel": {
|
|
92
|
-
"relation": "aRelatedModel",
|
|
93
|
-
"field": "id" // the column it is joined on
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
67
|
});
|
|
97
68
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if(result.kind == PlanKind.ALWAYS_DENIED) {
|
|
101
|
-
return console.log([]);
|
|
69
|
+
if (result.kind === PlanKind.ALWAYS_DENIED) {
|
|
70
|
+
return [];
|
|
102
71
|
}
|
|
103
72
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
73
|
+
// Use filters in Prisma query
|
|
74
|
+
const records = await prisma.resource.findMany({
|
|
75
|
+
where: result.filters,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Use filters in Prisma query with other conditions
|
|
79
|
+
const records = await prisma.resource.findMany({
|
|
107
80
|
where: {
|
|
108
|
-
AND:
|
|
81
|
+
AND: [
|
|
82
|
+
{
|
|
83
|
+
status: "DRAFT"
|
|
84
|
+
},
|
|
85
|
+
result.filters,
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Field Name Mapping
|
|
91
|
+
|
|
92
|
+
Fields can be mapped using either an object or a function:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Object mapping
|
|
96
|
+
const result = queryPlanToPrisma({
|
|
97
|
+
queryPlan,
|
|
98
|
+
mapper: {
|
|
99
|
+
"request.resource.attr.fieldName": { field: "prismaFieldName" },
|
|
109
100
|
},
|
|
110
101
|
});
|
|
111
102
|
|
|
112
|
-
|
|
103
|
+
// Function mapping
|
|
104
|
+
const result = queryPlanToPrisma({
|
|
105
|
+
queryPlan,
|
|
106
|
+
mapper: (fieldName) => ({
|
|
107
|
+
field: fieldName.replace("request.resource.attr.", ""),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
113
110
|
```
|
|
114
111
|
|
|
115
|
-
|
|
112
|
+
### Relations Mapping
|
|
113
|
+
|
|
114
|
+
Relations are mapped with their types and optional field configurations. Fields can be automatically inferred from the path if not explicitly mapped:
|
|
116
115
|
|
|
117
|
-
```
|
|
118
|
-
const
|
|
116
|
+
```ts
|
|
117
|
+
const result = queryPlanToPrisma({
|
|
119
118
|
queryPlan,
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
mapper: {
|
|
120
|
+
// Simple relation mapping - fields will be inferred
|
|
121
|
+
"request.resource.attr.owner": {
|
|
122
|
+
relation: {
|
|
123
|
+
name: "owner",
|
|
124
|
+
type: "one", // "one" for one-to-one, "many" for one-to-many
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Relation with explicit field mapping
|
|
129
|
+
"request.resource.attr.tags": {
|
|
130
|
+
relation: {
|
|
131
|
+
name: "tags",
|
|
132
|
+
type: "many",
|
|
133
|
+
field: "name", // Optional: specify field for direct comparisons
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Relation with nested field mappings
|
|
138
|
+
"request.resource.attr.nested": {
|
|
139
|
+
relation: {
|
|
140
|
+
name: "nested",
|
|
141
|
+
type: "one",
|
|
142
|
+
fields: {
|
|
143
|
+
// Optional: specify mappings for nested fields
|
|
144
|
+
aBool: { field: "aBool" },
|
|
145
|
+
aNumber: { field: "aNumber" },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
122
149
|
},
|
|
123
150
|
});
|
|
151
|
+
```
|
|
124
152
|
|
|
125
|
-
|
|
153
|
+
### Field Inference Example
|
|
126
154
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
When using relations, fields are automatically inferred from the path unless explicitly mapped:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// These mappers are equivalent for handling: request.resource.attr.nested.aNumber
|
|
159
|
+
{
|
|
160
|
+
"request.resource.attr.nested": {
|
|
161
|
+
relation: {
|
|
162
|
+
name: "nested",
|
|
163
|
+
type: "one",
|
|
164
|
+
fields: {
|
|
165
|
+
aNumber: { field: "aNumber" }
|
|
166
|
+
}
|
|
132
167
|
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
133
170
|
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
// Shorter version - aNumber will be inferred from the path
|
|
172
|
+
{
|
|
173
|
+
"request.resource.attr.nested": {
|
|
174
|
+
relation: {
|
|
175
|
+
name: "nested",
|
|
176
|
+
type: "one"
|
|
136
177
|
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
139
180
|
```
|
|
140
181
|
|
|
141
|
-
|
|
182
|
+
### Complex Example with Multiple Relations and Direct Fields
|
|
142
183
|
|
|
143
|
-
```
|
|
144
|
-
const
|
|
184
|
+
```ts
|
|
185
|
+
const result = queryPlanToPrisma({
|
|
145
186
|
queryPlan,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"request.resource.attr.
|
|
149
|
-
relation:
|
|
150
|
-
|
|
187
|
+
mapper: {
|
|
188
|
+
"request.resource.attr.status": { field: "status" },
|
|
189
|
+
"request.resource.attr.owner": {
|
|
190
|
+
relation: {
|
|
191
|
+
name: "owner",
|
|
192
|
+
type: "one",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
"request.resource.attr.tags": {
|
|
196
|
+
relation: {
|
|
197
|
+
name: "tags",
|
|
198
|
+
type: "many",
|
|
199
|
+
field: "name",
|
|
200
|
+
},
|
|
151
201
|
},
|
|
152
202
|
},
|
|
153
203
|
});
|
|
154
204
|
|
|
155
|
-
//
|
|
205
|
+
// Results in Prisma filters like:
|
|
206
|
+
const result = await primsa.resource.findMany({
|
|
207
|
+
where: {
|
|
208
|
+
AND: [
|
|
209
|
+
{ status: { equals: "active" } },
|
|
210
|
+
{ owner: { is: { id: { equals: "user1" } } } },
|
|
211
|
+
{ tags: { some: { name: { in: ["tag1", "tag2"] } } } },
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
```
|
|
156
216
|
|
|
157
|
-
|
|
217
|
+
### Complex Examples
|
|
218
|
+
|
|
219
|
+
#### Lambda Expression Examples
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// Using exists with lambda expressions
|
|
223
|
+
const result = queryPlanToPrisma({
|
|
158
224
|
queryPlan,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
225
|
+
mapper: {
|
|
226
|
+
"request.resource.attr.comments": {
|
|
227
|
+
relation: {
|
|
228
|
+
name: "comments",
|
|
229
|
+
type: "many",
|
|
230
|
+
fields: {
|
|
231
|
+
author: {
|
|
232
|
+
relation: {
|
|
233
|
+
name: "author",
|
|
234
|
+
type: "one",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
status: { field: "status" },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// This can handle complex exists queries like:
|
|
245
|
+
// "Does the resource have any approved comments by specific users?"
|
|
246
|
+
const result = await primsa.resource.findMany({
|
|
247
|
+
where: {
|
|
248
|
+
comments: {
|
|
249
|
+
some: {
|
|
250
|
+
AND: [
|
|
251
|
+
{ status: { equals: "approved" } },
|
|
252
|
+
{
|
|
253
|
+
author: {
|
|
254
|
+
is: {
|
|
255
|
+
id: { in: ["user1", "user2"] },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
167
262
|
},
|
|
168
263
|
});
|
|
169
264
|
```
|
|
170
265
|
|
|
171
266
|
## Full Example
|
|
172
267
|
|
|
173
|
-
A
|
|
268
|
+
A complete example application using this adapter can be found at [https://github.com/cerbos/express-prisma-cerbos](https://github.com/cerbos/express-prisma-cerbos)
|
package/lib/cjs/index.d.ts
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
import { PlanResourcesResponse, PlanKind
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { PlanResourcesResponse, PlanKind } from "@cerbos/core";
|
|
2
|
+
export { PlanKind };
|
|
3
|
+
export type PrismaFilter = Record<string, any>;
|
|
4
|
+
export type MapperConfig = {
|
|
5
|
+
field?: string;
|
|
6
|
+
relation?: {
|
|
7
|
+
name: string;
|
|
8
|
+
type: "one" | "many";
|
|
9
|
+
field?: string;
|
|
10
|
+
fields?: {
|
|
11
|
+
[key: string]: MapperConfig;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
10
14
|
};
|
|
11
|
-
|
|
12
|
-
[key: string]:
|
|
13
|
-
} | ((key: string) =>
|
|
14
|
-
interface QueryPlanToPrismaArgs {
|
|
15
|
+
export type Mapper = {
|
|
16
|
+
[key: string]: MapperConfig;
|
|
17
|
+
} | ((key: string) => MapperConfig);
|
|
18
|
+
export interface QueryPlanToPrismaArgs {
|
|
15
19
|
queryPlan: PlanResourcesResponse;
|
|
16
|
-
|
|
17
|
-
relationMapper?: RelationMapper;
|
|
20
|
+
mapper?: Mapper;
|
|
18
21
|
}
|
|
19
|
-
interface QueryPlanToPrismaResult {
|
|
22
|
+
export interface QueryPlanToPrismaResult {
|
|
20
23
|
kind: PlanKind;
|
|
21
|
-
filters?: any
|
|
24
|
+
filters?: Record<string, any>;
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Converts a Cerbos query plan to a Prisma filter.
|
|
28
|
+
* @param {QueryPlanToPrismaArgs} args - The arguments containing the query plan and mapper.
|
|
29
|
+
* @returns {QueryPlanToPrismaResult} The result containing the kind and filters.
|
|
30
|
+
*/
|
|
31
|
+
export declare function queryPlanToPrisma({ queryPlan, mapper, }: QueryPlanToPrismaArgs): QueryPlanToPrismaResult;
|