@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/lib/cjs/index.js CHANGED
@@ -1,169 +1,345 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.queryPlanToPrisma = exports.PlanKind = void 0;
3
+ exports.PlanKind = void 0;
4
+ exports.queryPlanToPrisma = queryPlanToPrisma;
4
5
  const core_1 = require("@cerbos/core");
5
- exports.PlanKind = core_1.PlanKind;
6
- function queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper = {}, }) {
7
- const toFieldName = (key) => {
8
- if (typeof fieldNameMapper === "function") {
9
- return fieldNameMapper(key);
10
- }
11
- else if (fieldNameMapper[key]) {
12
- return fieldNameMapper[key];
13
- }
14
- else {
15
- return key;
16
- }
17
- };
18
- const toRelationName = (key) => {
19
- if (typeof relationMapper === "function") {
20
- return relationMapper(key);
21
- }
22
- else if (relationMapper[key]) {
23
- return relationMapper[key];
24
- }
25
- return null;
26
- };
6
+ Object.defineProperty(exports, "PlanKind", { enumerable: true, get: function () { return core_1.PlanKind; } });
7
+ /**
8
+ * Converts a Cerbos query plan to a Prisma filter.
9
+ * @param {QueryPlanToPrismaArgs} args - The arguments containing the query plan and mapper.
10
+ * @returns {QueryPlanToPrismaResult} The result containing the kind and filters.
11
+ */
12
+ function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
13
+ // Choose filter conversion based on plan kind
27
14
  switch (queryPlan.kind) {
28
- case exports.PlanKind.ALWAYS_ALLOWED:
29
- return {
30
- kind: exports.PlanKind.ALWAYS_ALLOWED,
31
- filters: {},
32
- };
33
- case exports.PlanKind.ALWAYS_DENIED:
15
+ case core_1.PlanKind.ALWAYS_ALLOWED:
34
16
  return {
35
- kind: exports.PlanKind.ALWAYS_DENIED,
17
+ kind: core_1.PlanKind.ALWAYS_ALLOWED,
36
18
  };
37
- case exports.PlanKind.CONDITIONAL:
19
+ case core_1.PlanKind.ALWAYS_DENIED:
20
+ return { kind: core_1.PlanKind.ALWAYS_DENIED };
21
+ case core_1.PlanKind.CONDITIONAL:
38
22
  return {
39
- kind: exports.PlanKind.CONDITIONAL,
40
- filters: mapOperand(queryPlan.condition, toFieldName, toRelationName),
23
+ kind: core_1.PlanKind.CONDITIONAL,
24
+ filters: buildPrismaFilterFromCerbosExpression(queryPlan.condition, mapper),
41
25
  };
42
26
  default:
43
27
  throw Error(`Invalid query plan.`);
44
28
  }
45
29
  }
46
- exports.queryPlanToPrisma = queryPlanToPrisma;
47
- function isExpression(e) {
48
- return e.operator !== undefined;
49
- }
50
- function isValue(e) {
51
- return e.value !== undefined;
52
- }
53
- function isVariable(e) {
54
- return e.variable !== undefined;
55
- }
56
- function getOperandVariable(operands) {
57
- const op = operands.find((o) => o.hasOwnProperty("name"));
58
- if (!op)
59
- return;
60
- return op.name;
61
- }
62
- function getOperandValue(operands) {
63
- const op = operands.find((o) => isValue(o));
64
- if (!op)
65
- return;
66
- return op.value;
67
- }
68
- const OPERATORS = {
69
- eq: {
70
- relationalCondition: "is",
71
- fieldCondition: "equals",
72
- },
73
- ne: {
74
- relationalCondition: "isNot",
75
- fieldCondition: "not",
76
- },
77
- in: {
78
- relationalCondition: "some",
79
- fieldCondition: "in",
80
- },
81
- lt: {
82
- fieldCondition: "lt",
83
- },
84
- gt: {
85
- fieldCondition: "gt",
86
- },
87
- le: {
88
- fieldCondition: "lte",
89
- },
90
- ge: {
91
- fieldCondition: "gte",
92
- },
93
- };
94
- function mapOperand(operand, getFieldName, getRelationName, output = {}) {
95
- if (!isExpression(operand))
96
- throw Error(`Query plan did not contain an expression for operand ${operand}`);
97
- const { operator, operands } = operand;
98
- // HANDLE NESTING OPERATIONS: AND/OR/NOT
99
- if (operator == "and") {
100
- if (operands.length < 2)
101
- throw Error("Expected atleast 2 operands");
102
- output.AND = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}));
103
- return output;
104
- }
105
- if (operator == "or") {
106
- if (operands.length < 2)
107
- throw Error("Expected atleast 2 operands");
108
- output.OR = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}));
109
- return output;
110
- }
111
- if (operator == "not") {
112
- if (operands.length > 1)
113
- throw Error("Expected only one operand");
114
- output.NOT = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}))[0];
115
- return output;
116
- }
117
- // get the operation parameters
118
- const operation = OPERATORS[operator];
119
- if (!operation)
120
- throw Error(`Unsupported operator ${operator}`);
121
- const opVariable = getOperandVariable(operands);
122
- if (!opVariable)
123
- throw Error(`Unexpected variable ${operands}`);
124
- const opValue = getOperandValue(operands);
125
- const relation = getRelationName(opVariable);
126
- const fieldName = getFieldName(opVariable);
127
- // There is a relational mapper for this variable
128
- if (relation && operation.relationalCondition) {
129
- output[relation.relation] = {
130
- [operation.relationalCondition]: {
131
- [relation.field]: opValue,
132
- },
133
- };
134
- return output;
30
+ /**
31
+ * Resolves a field reference considering relations.
32
+ * @param {string} reference - The field reference.
33
+ * @param {Mapper} mapper - The mapper configuration.
34
+ * @returns {object} The resolved field reference with path and relation.
35
+ */
36
+ const resolveFieldReference = (reference, mapper) => {
37
+ const parts = reference.split(".");
38
+ const lastPart = parts[parts.length - 1];
39
+ // First try exact match
40
+ const config = typeof mapper === "function" ? mapper(reference) : mapper[reference];
41
+ if (config) {
42
+ if (config.relation) {
43
+ const { name, type, field, fields } = config.relation;
44
+ return {
45
+ path: [name, field].filter(Boolean),
46
+ relation: { name, type, field, nestedMapper: fields },
47
+ };
48
+ }
49
+ return { path: [config.field || reference] };
135
50
  }
136
- else if (fieldName && operation.fieldCondition) {
137
- // There is a field mapper for this variable
138
- const fieldPath = fieldName.split(".");
139
- if (fieldPath.length > 1) {
140
- output[fieldPath[0]] = convertPathToJSON(fieldPath.splice(1), {
141
- [operation.fieldCondition]: opValue,
142
- });
143
- }
144
- else {
145
- output[fieldPath[0]] = {
146
- [operation.fieldCondition]: opValue,
51
+ // Check for parent relation with multiple parts
52
+ if (parts.length > 1) {
53
+ const parentPath = parts.slice(0, -1).join(".");
54
+ const parentConfig = typeof mapper === "function" ? mapper(parentPath) : mapper[parentPath];
55
+ if (parentConfig === null || parentConfig === void 0 ? void 0 : parentConfig.relation) {
56
+ const { name, type, fields } = parentConfig.relation;
57
+ const fieldConfig = fields === null || fields === void 0 ? void 0 : fields[lastPart];
58
+ const fieldName = (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.field) || lastPart;
59
+ return {
60
+ path: [name],
61
+ relation: { name, type, field: fieldName, nestedMapper: fields },
147
62
  };
148
63
  }
149
- return output;
150
64
  }
151
- else {
152
- throw Error("Failed to map");
65
+ // Fallback to raw reference if no mapper fits
66
+ return { path: [reference] };
67
+ };
68
+ /**
69
+ * Determines the appropriate Prisma operator based on relation type.
70
+ * @param {object} relation - The relation configuration.
71
+ * @returns {string} The Prisma relation operator.
72
+ */
73
+ const getPrismaRelationOperator = (relation) => (relation.type === "one" ? "is" : "some");
74
+ /**
75
+ * Builds Prisma WHERE conditions from a Cerbos expression.
76
+ * @param {PlanExpressionOperand} expression - The Cerbos expression.
77
+ * @param {Mapper} mapper - The mapper configuration.
78
+ * @returns {PrismaFilter} The Prisma filter.
79
+ */
80
+ const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
81
+ if (!("operator" in expression) || !("operands" in expression)) {
82
+ throw new Error("Invalid Cerbos expression structure");
153
83
  }
154
- }
155
- function convertPathToJSON(segments, value) {
156
- const result = {};
157
- let current = result;
158
- for (let i = 0; i < segments.length; i++) {
159
- const segment = segments[i];
160
- if (i === segments.length - 1) {
161
- current[segment] = value;
162
- }
163
- else {
164
- current[segment] = {};
165
- current = current[segment];
84
+ const { operator, operands } = expression;
85
+ /**
86
+ * Helper function to resolve an operand.
87
+ * @param {PlanExpressionOperand} operand - The operand from expression.
88
+ * @returns {any} An object with a field reference or value.
89
+ */
90
+ const resolveOperand = (operand) => {
91
+ if ("name" in operand && operand.name) {
92
+ return resolveFieldReference(operand.name, mapper);
166
93
  }
94
+ else if ("value" in operand && operand.value !== undefined) {
95
+ return { value: operand.value };
96
+ }
97
+ else if ("operator" in operand) {
98
+ // Nested expression resolution
99
+ const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
100
+ return { value: nestedResult };
101
+ }
102
+ throw new Error("Operand must have name, value, or be an expression");
103
+ };
104
+ // Process the operator type
105
+ switch (operator) {
106
+ case "and": {
107
+ // Combine operands with logical AND
108
+ return {
109
+ AND: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
110
+ };
111
+ }
112
+ case "or": {
113
+ // Combine operands with logical OR
114
+ return {
115
+ OR: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
116
+ };
117
+ }
118
+ case "not": {
119
+ // Negate the operand filter
120
+ return {
121
+ NOT: buildPrismaFilterFromCerbosExpression(operands[0], mapper),
122
+ };
123
+ }
124
+ case "eq":
125
+ case "ne":
126
+ case "lt":
127
+ case "le":
128
+ case "gt":
129
+ case "ge": {
130
+ // Relational operators: find left and right operands
131
+ const leftOperand = operands.find((o) => "name" in o || "operator" in o);
132
+ if (!leftOperand)
133
+ throw new Error("No valid left operand found");
134
+ const rightOperand = operands.find((o) => o !== leftOperand);
135
+ if (!rightOperand)
136
+ throw new Error("No valid right operand found");
137
+ const left = resolveOperand(leftOperand);
138
+ const right = resolveOperand(rightOperand);
139
+ const prismaOperator = {
140
+ eq: "equals",
141
+ ne: "not",
142
+ lt: "lt",
143
+ le: "lte",
144
+ gt: "gt",
145
+ ge: "gte",
146
+ }[operator];
147
+ if ("path" in left) {
148
+ const { path, relation } = left;
149
+ if (relation) {
150
+ const relationOperator = getPrismaRelationOperator(relation);
151
+ if (relation.field) {
152
+ return {
153
+ [relation.name]: {
154
+ [relationOperator]: {
155
+ [relation.field]: { [prismaOperator]: right.value },
156
+ },
157
+ },
158
+ };
159
+ }
160
+ return {
161
+ [relation.name]: {
162
+ [relationOperator]: { [prismaOperator]: right.value },
163
+ },
164
+ };
165
+ }
166
+ // Create nested filter for field path
167
+ return path.reduceRight((acc, key, index) => index === path.length - 1
168
+ ? { [key]: { [prismaOperator]: right.value } }
169
+ : { [key]: acc }, {});
170
+ }
171
+ return { [prismaOperator]: right.value };
172
+ }
173
+ case "in": {
174
+ // Inclusion operator for lists
175
+ const { path, relation } = resolveOperand(operands.find((o) => "name" in o));
176
+ const { value } = resolveOperand(operands.find((o) => "value" in o));
177
+ if (relation) {
178
+ const relationOperator = getPrismaRelationOperator(relation);
179
+ return {
180
+ [relation.name]: { [relationOperator]: { [relation.field]: value } },
181
+ };
182
+ }
183
+ return path.reduceRight((acc, key, index) => index === path.length - 1 ? { [key]: { in: value } } : { [key]: acc }, {});
184
+ }
185
+ case "contains":
186
+ case "startsWith":
187
+ case "endsWith": {
188
+ // String operators; ensure value is a string
189
+ const leftOperand = operands.find((o) => "name" in o);
190
+ if (!leftOperand)
191
+ throw new Error("No operand with 'name' found");
192
+ const rightOperand = operands.find((o) => "value" in o);
193
+ if (!rightOperand)
194
+ throw new Error("No operand with 'value' found");
195
+ const { path, relation } = resolveOperand(leftOperand);
196
+ const { value } = resolveOperand(rightOperand);
197
+ if (typeof value !== "string")
198
+ throw new Error(`${operator} operator requires string value`);
199
+ if (relation) {
200
+ const relationOperator = getPrismaRelationOperator(relation);
201
+ return {
202
+ [relation.name]: {
203
+ [relationOperator]: { [relation.field]: { [operator]: value } },
204
+ },
205
+ };
206
+ }
207
+ return path.reduceRight((acc, key, index) => index === path.length - 1
208
+ ? { [key]: { [operator]: value } }
209
+ : { [key]: acc }, {});
210
+ }
211
+ case "isSet": {
212
+ // Check if a field is set (not null) or unset (null)
213
+ const leftOperand = operands.find((o) => "name" in o);
214
+ if (!leftOperand)
215
+ throw new Error("No operand with 'name' found");
216
+ const rightOperand = operands.find((o) => "value" in o);
217
+ if (!rightOperand)
218
+ throw new Error("No operand with 'value' found");
219
+ const { path, relation } = resolveOperand(leftOperand);
220
+ const { value } = resolveOperand(rightOperand);
221
+ if (relation) {
222
+ const relationOperator = getPrismaRelationOperator(relation);
223
+ return {
224
+ [relation.name]: {
225
+ [relationOperator]: {
226
+ [relation.field]: value ? { not: null } : { equals: null },
227
+ },
228
+ },
229
+ };
230
+ }
231
+ return path.reduceRight((acc, key, index) => index === path.length - 1
232
+ ? { [key]: value ? { not: null } : { equals: null } }
233
+ : { [key]: acc }, {});
234
+ }
235
+ case "hasIntersection": {
236
+ // Ensure exactly two operands for intersection check
237
+ if (operands.length !== 2)
238
+ throw new Error("hasIntersection requires exactly two operands");
239
+ const [leftOperand, rightOperand] = operands;
240
+ // Handle map expressions as first operand
241
+ if ("operator" in leftOperand && leftOperand.operator === "map") {
242
+ const mapResult = buildPrismaFilterFromCerbosExpression(leftOperand, mapper);
243
+ const relationKey = Object.keys(mapResult)[0];
244
+ const relation = mapResult[relationKey];
245
+ const selectKey = Object.keys(relation[Object.keys(relation)[0]].select)[0];
246
+ if (!("value" in rightOperand))
247
+ throw new Error("Second operand of hasIntersection must be a value");
248
+ return {
249
+ [relationKey]: { some: { [selectKey]: { in: rightOperand.value } } },
250
+ };
251
+ }
252
+ // Handle direct field reference
253
+ if (!("name" in leftOperand))
254
+ throw new Error("First operand of hasIntersection must be a field reference or map expression");
255
+ if (!("value" in rightOperand))
256
+ throw new Error("Second operand of hasIntersection must be a value");
257
+ const { path, relation } = resolveFieldReference(leftOperand.name, mapper);
258
+ if (!Array.isArray(rightOperand.value))
259
+ throw new Error("hasIntersection requires an array value");
260
+ if (relation) {
261
+ return {
262
+ [relation.name]: {
263
+ some: { [relation.field]: { in: rightOperand.value } },
264
+ },
265
+ };
266
+ }
267
+ return path.reduceRight((acc, key, index) => index === path.length - 1
268
+ ? { [key]: { hasSome: rightOperand.value } }
269
+ : { [key]: acc }, {});
270
+ }
271
+ case "lambda": {
272
+ // Handle lambda expressions by replacing variable prefix on keys
273
+ const [condition, variable] = operands;
274
+ if (!("name" in variable))
275
+ throw new Error("Lambda variable must have a name");
276
+ return buildPrismaFilterFromCerbosExpression(condition, (key) => ({ field: key.replace(`${variable.name}.`, "") }));
277
+ }
278
+ case "exists":
279
+ case "exists_one":
280
+ case "all":
281
+ case "except":
282
+ case "filter": {
283
+ // Operators applying conditions on collections via lambda expressions
284
+ if (operands.length !== 2)
285
+ throw new Error(`${operator} requires exactly two operands`);
286
+ const [collection, lambda] = operands;
287
+ if (!("name" in collection))
288
+ throw new Error("First operand of exists/all/except must be a collection reference");
289
+ if (!("operator" in lambda))
290
+ throw new Error("Second operand of exists/all/except must be a lambda expression");
291
+ const { relation } = resolveFieldReference(collection.name, mapper);
292
+ if (!relation)
293
+ throw new Error(`${operator} operator requires a relation mapping`);
294
+ const lambdaCondition = buildPrismaFilterFromCerbosExpression(lambda, mapper);
295
+ switch (operator) {
296
+ case "exists":
297
+ return { [relation.name]: { some: lambdaCondition } };
298
+ case "exists_one":
299
+ return {
300
+ [relation.name]: { some: lambdaCondition },
301
+ AND: [
302
+ {
303
+ [relation.name]: {
304
+ every: { OR: [lambdaCondition, { NOT: lambdaCondition }] },
305
+ },
306
+ },
307
+ ],
308
+ };
309
+ case "all":
310
+ return { [relation.name]: { every: lambdaCondition } };
311
+ case "filter":
312
+ return { [relation.name]: { some: lambdaCondition } };
313
+ default:
314
+ throw new Error(`Unexpected operator: ${operator}`);
315
+ }
316
+ }
317
+ case "map": {
318
+ // Process map operator to project field values
319
+ if (operands.length !== 2)
320
+ throw new Error("map requires exactly two operands");
321
+ const [collection, lambda] = operands;
322
+ if (!("name" in collection))
323
+ throw new Error("First operand of map must be a collection reference");
324
+ if (!("operator" in lambda) || lambda.operator !== "lambda")
325
+ throw new Error("Second operand of map must be a lambda expression");
326
+ const { relation } = resolveFieldReference(collection.name, mapper);
327
+ if (!relation)
328
+ throw new Error("map operator requires a relation mapping");
329
+ const [projection, variable] = lambda.operands;
330
+ if (!("name" in projection) || !("name" in variable))
331
+ throw new Error("Invalid map lambda expression structure");
332
+ return {
333
+ [relation.name]: {
334
+ [getPrismaRelationOperator(relation)]: {
335
+ select: {
336
+ [projection.name.replace(`${variable.name}.`, "")]: true,
337
+ },
338
+ },
339
+ },
340
+ };
341
+ }
342
+ default:
343
+ throw new Error(`Unsupported operator: ${operator}`);
167
344
  }
168
- return result;
169
- }
345
+ };
package/lib/index.d.ts CHANGED
@@ -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;