@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 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
- 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
17
 
22
- - Cerbos > v0.16
18
+ - Cerbos > v0.40
23
19
  - `@cerbos/http` or `@cerbos/grpc` client
24
20
 
25
- ## Usage
21
+ ## Installation
26
22
 
27
- ```
23
+ ```bash
28
24
  npm install @cerbos/orm-prisma
29
25
  ```
30
26
 
31
- This package exports a function:
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({ queryPlan, fieldNameMapper, relationMapper }): {
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 // a filter to pass to the findMany() function of Prisma
39
+ filters?: any // Prisma where conditions
39
40
  }
40
41
  ```
41
42
 
42
- where `PlanKind` is:
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 the query plan from Cerbos passing in the principal
77
- // resource type and action
53
+ // Fetch query plan from Cerbos
78
54
  const queryPlan = await cerbos.planResources({
79
- principal: {....},
80
- resource: { kind: "resourceKind" },
81
- action: "view"
82
- })
55
+ principal: { id: "user1", roles: ["USER"] },
56
+ resource: { kind: "resource" },
57
+ action: "view",
58
+ });
83
59
 
84
- // Generate the prisma filter from the query plan
60
+ // Convert query plan to Prisma filters
85
61
  const result = queryPlanToPrisma({
86
62
  queryPlan,
87
- fieldNameMapper: {
88
- "request.resource.attr.aFieldName": "prismaModelFieldName"
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
- // The query plan says the user would always be denied
99
- // return empty or throw an error depending on your app.
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
- // Pass the filters in as where conditions
105
- // If you have prexisting where conditions, you can pass them in an AND clause
106
- 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({
107
80
  where: {
108
- 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" },
109
100
  },
110
101
  });
111
102
 
112
- console.log(result)
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
- 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
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
- ```js
118
- const filters = queryPlanToPrisma({
116
+ ```ts
117
+ const result = queryPlanToPrisma({
119
118
  queryPlan,
120
- fieldNameMapper: {
121
- "request.resource.attr.aFieldName": "prismaModelFieldName",
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
- //or
153
+ ### Field Inference Example
126
154
 
127
- const filters = queryPlanToPrisma({
128
- queryPlan,
129
- fieldNameMapper: (fieldName: string): string => {
130
- if (fieldName.indexOf("request.resource.") > 0) {
131
- 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
+ }
132
167
  }
168
+ }
169
+ }
133
170
 
134
- if (fieldName.indexOf("request.principal.") > 0) {
135
- 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"
136
177
  }
137
- },
138
- });
178
+ }
179
+ }
139
180
  ```
140
181
 
141
- The `relationMapper` is used to convert references to fields which are joins at the database level
182
+ ### Complex Example with Multiple Relations and Direct Fields
142
183
 
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
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
- //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
- }
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 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;