@cerbos/orm-prisma 1.1.0 → 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 CHANGED
@@ -1,173 +1,268 @@
1
1
  # Cerbos + Prisma ORM Adapter
2
2
 
3
- An adapater 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).
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
- The following conditions are supported: `and`, `or`, `eq`, `ne`, `lt`, `gt`, `lte`, `gte` and `in`, as well as relational filters `some`, `none`, `is` and `isNot`.
5
+ ## Features
6
6
 
7
- Not Supported:
7
+ ### Supported Operators
8
8
 
9
- - `every`
10
- - `contains`
11
- - `search`
12
- - `mode`
13
- - `startsWith`
14
- - `endsWith`
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
- - Cerbos > v0.16
17
+
18
+ - Cerbos > v0.40
22
19
  - `@cerbos/http` or `@cerbos/grpc` client
23
20
 
24
- ## Usage
21
+ ## Installation
25
22
 
26
- ```
23
+ ```bash
27
24
  npm install @cerbos/orm-prisma
28
25
  ```
29
26
 
30
- This package exports a function:
27
+ ## Usage
28
+
29
+ The package exports a function:
31
30
 
32
31
  ```ts
33
32
  import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";
34
33
 
35
- queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper }): {
34
+ queryPlanToPrisma({
35
+ queryPlan, // The Cerbos query plan response
36
+ mapper, // Map Cerbos field names to Prisma field names
37
+ }): {
36
38
  kind: PlanKind,
37
- filters?: any // a filter to pass to the findMany() function of Prisma
39
+ filters?: any // Prisma where conditions
38
40
  }
39
41
  ```
40
42
 
41
- where `PlanKind` is:
43
+ ### Basic Example
42
44
 
43
45
  ```ts
44
- export enum PlanKind {
45
- /**
46
- * The specified action is always allowed for the principal on resources matching the input.
47
- */
48
- ALWAYS_ALLOWED = "KIND_ALWAYS_ALLOWED",
49
-
50
- /**
51
- * The specified action is always denied for the principal on resources matching the input.
52
- */
53
- ALWAYS_DENIED = "KIND_ALWAYS_DENIED",
54
-
55
- /**
56
- * The specified action is conditionally allowed for the principal on resources matching the input.
57
- */
58
- CONDITIONAL = "KIND_CONDITIONAL",
59
- }
60
- ```
61
-
62
- 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.
63
-
64
- A basic implementation can be as simple as:
65
-
66
- ```js
67
46
  import { GRPC as Cerbos } from "@cerbos/grpc";
68
47
  import { PrismaClient } from "@prisma/client";
69
-
70
48
  import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";
71
49
 
72
50
  const prisma = new PrismaClient();
73
51
  const cerbos = new Cerbos("localhost:3592", { tls: false });
74
52
 
75
- // Fetch the query plan from Cerbos passing in the principal
76
- // resource type and action
53
+ // Fetch query plan from Cerbos
77
54
  const queryPlan = await cerbos.planResources({
78
- principal: {....},
79
- resource: { kind: "resourceKind" },
80
- action: "view"
81
- })
55
+ principal: { id: "user1", roles: ["USER"] },
56
+ resource: { kind: "resource" },
57
+ action: "view",
58
+ });
82
59
 
83
- // Generate the prisma filter from the query plan
60
+ // Convert query plan to Prisma filters
84
61
  const result = queryPlanToPrisma({
85
62
  queryPlan,
86
- fieldNameMapper: {
87
- "request.resource.attr.aFieldName": "prismaModelFieldName"
63
+ mapper: {
64
+ "request.resource.attr.title": { field: "title" },
65
+ "request.resource.attr.status": { field: "status" },
88
66
  },
89
- relationMapper: {
90
- "request.resource.attr.aRelatedModel": {
91
- "relation": "aRelatedModel",
92
- "field": "id" // the column it is joined on
93
- }
94
- }
95
67
  });
96
68
 
97
- // The query plan says the user would always be denied
98
- // return empty or throw an error depending on your app.
99
- if(result.kind == PlanKind.ALWAYS_DENIED) {
100
- return console.log([]);
69
+ if (result.kind === PlanKind.ALWAYS_DENIED) {
70
+ return [];
101
71
  }
102
72
 
103
- // Pass the filters in as where conditions
104
- // If you have prexisting where conditions, you can pass them in an AND clause
105
- const result = await prisma.myModel.findMany({
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({
106
80
  where: {
107
- AND: result.filters
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" },
108
100
  },
109
101
  });
110
102
 
111
- console.log(result)
103
+ // Function mapping
104
+ const result = queryPlanToPrisma({
105
+ queryPlan,
106
+ mapper: (fieldName) => ({
107
+ field: fieldName.replace("request.resource.attr.", ""),
108
+ }),
109
+ });
112
110
  ```
113
111
 
114
- The `fieldNameMapper` is used to convert the field names in the query plan response to names of fields in the Prisma model - this can be done as a map or a function:
112
+ ### Relations Mapping
115
113
 
116
- ```js
117
- const filters = queryPlanToPrisma({
114
+ Relations are mapped with their types and optional field configurations. Fields can be automatically inferred from the path if not explicitly mapped:
115
+
116
+ ```ts
117
+ const result = queryPlanToPrisma({
118
118
  queryPlan,
119
- fieldNameMapper: {
120
- "request.resource.attr.aFieldName": "prismaModelFieldName"
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
+ },
149
+ },
122
150
  });
151
+ ```
123
152
 
124
- //or
153
+ ### Field Inference Example
125
154
 
126
- const filters = queryPlanToPrisma({
127
- queryPlan,
128
- fieldNameMapper: (fieldName: string): string => {
129
- if(fieldName.indexOf("request.resource.") > 0) {
130
- return fieldName.replace("request.resource.attr", "")
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
+ }
131
167
  }
168
+ }
169
+ }
132
170
 
133
- if(fieldName.indexOf("request.principal.") > 0) {
134
- return fieldName.replace("request.principal.attr", "")
171
+ // Shorter version - aNumber will be inferred from the path
172
+ {
173
+ "request.resource.attr.nested": {
174
+ relation: {
175
+ name: "nested",
176
+ type: "one"
135
177
  }
136
178
  }
137
- });
179
+ }
138
180
  ```
139
181
 
182
+ ### Complex Example with Multiple Relations and Direct Fields
140
183
 
141
- The `relationMapper` is used to convert references to fields which are joins at the database level
142
-
143
- ```js
144
- const filters = queryPlanToPrisma({
184
+ ```ts
185
+ const result = queryPlanToPrisma({
145
186
  queryPlan,
146
- fieldNameMapper: {},
147
- relationMapper: {
148
- "request.resource.attr.aRelatedModel": {
149
- "relation": "aRelatedModel",
150
- "field": "id" // the column it is joined on
151
- }
152
- }
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
+ },
201
+ },
202
+ },
153
203
  });
154
204
 
155
- //or
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
- const filters = queryPlanToPrisma({
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
- fieldNameMapper: {},
160
- relationMapper: (fieldName: string): string => {
161
- if(fieldName.indexOf("request.resource.") > 0) {
162
- return {
163
- "relation": fieldName.replace("request.resource.attr.", ""),
164
- "field": "id" // the column it is joined on
165
- }
166
- }
167
- }
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
+ },
262
+ },
168
263
  });
169
264
  ```
170
265
 
171
266
  ## Full Example
172
267
 
173
- A full Prisma application making use of this adapater can be found at [https://github.com/cerbos/express-prisma-cerbos](https://github.com/cerbos/express-prisma-cerbos)
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)
@@ -1,24 +1,31 @@
1
- import { PlanResourcesResponse, PlanKind as PK } from "@cerbos/core";
2
- export declare type PlanKind = PK;
3
- export declare const PlanKind: typeof PK;
4
- declare type FieldMapper = {
5
- [key: string]: string;
6
- } | ((key: string) => string);
7
- declare type Relation = {
8
- relation: string;
9
- field: string;
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
- declare type RelationMapper = {
12
- [key: string]: Relation;
13
- } | ((key: string) => Relation);
14
- interface QueryPlanToPrismaArgs {
15
+ export type Mapper = {
16
+ [key: string]: MapperConfig;
17
+ } | ((key: string) => MapperConfig);
18
+ export interface QueryPlanToPrismaArgs {
15
19
  queryPlan: PlanResourcesResponse;
16
- fieldNameMapper: FieldMapper;
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
- export declare function queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper, }: QueryPlanToPrismaArgs): QueryPlanToPrismaResult;
24
- export {};
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;