@cerbos/orm-prisma 3.2.1 → 3.3.0
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 +11 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +400 -103
- package/lib/index.js.map +1 -1
- package/package.json +13 -6
- package/src/generated/prisma/client.ts +3 -1
- package/src/generated/prisma/internal/class.ts +17 -5
- package/src/generated/prisma/internal/prismaNamespace.ts +4 -4
- package/src/generated/prisma/models/Category.ts +5 -0
- package/src/generated/prisma/models/Label.ts +5 -0
- package/src/generated/prisma/models/NestedResource.ts +5 -0
- package/src/generated/prisma/models/NextLevelNestedResource.ts +5 -0
- package/src/generated/prisma/models/Resource.ts +5 -0
- package/src/generated/prisma/models/SubCategory.ts +5 -0
- package/src/generated/prisma/models/Tag.ts +5 -0
- package/src/generated/prisma/models/User.ts +5 -0
- package/src/index.test.ts +807 -0
- package/src/index.ts +586 -123
package/README.md
CHANGED
|
@@ -19,6 +19,17 @@ An adapter library that takes a [Cerbos](https://cerbos.dev) Query Plan ([PlanRe
|
|
|
19
19
|
- Collection operators: `exists`, `exists_one`, `all`, `filter`, `except`
|
|
20
20
|
- Set operations: `hasIntersection`
|
|
21
21
|
|
|
22
|
+
#### String Concatenation
|
|
23
|
+
|
|
24
|
+
- `add` operator for string/numeric concatenation in conditions
|
|
25
|
+
- Algebraic solving: `P.attr.context == "projects:" + R.attr.id` → `{ id: { equals: "123" } }`
|
|
26
|
+
|
|
27
|
+
#### Hierarchy Operators
|
|
28
|
+
|
|
29
|
+
- `hierarchy(string)`, `hierarchy(string, delimiter)`, `hierarchy([segments])`
|
|
30
|
+
- `overlaps`: segment-wise prefix comparison between two hierarchies
|
|
31
|
+
- `ancestorOf` / `descendentOf`: strict prefix relationship between hierarchies
|
|
32
|
+
|
|
22
33
|
#### Advanced Features
|
|
23
34
|
|
|
24
35
|
- Deep nested relations support
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EAErB,QAAQ,EAET,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EAErB,QAAQ,EAET,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,QAAQ,EAAE,CAAC;AAYpB,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE/C,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;KACvC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAC5B,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,YAAY,CAAC,CAAC;AAEpC,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,qBAAqB,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,uBAAuB,GAC/B;IACE,IAAI,EAAE,QAAQ,CAAC,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC;CACxD,GACD;IACE,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC;IAC3B,OAAO,EAAE,YAAY,CAAC;CACvB,CAAC;AAmKN;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,MAAW,GACZ,EAAE,qBAAqB,GAAG,uBAAuB,CAiBjD"}
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,14 @@ exports.PlanKind = void 0;
|
|
|
4
4
|
exports.queryPlanToPrisma = queryPlanToPrisma;
|
|
5
5
|
const core_1 = require("@cerbos/core");
|
|
6
6
|
Object.defineProperty(exports, "PlanKind", { enumerable: true, get: function () { return core_1.PlanKind; } });
|
|
7
|
+
const CERBOS_TO_PRISMA_OPERATOR = {
|
|
8
|
+
eq: "equals",
|
|
9
|
+
ne: "not",
|
|
10
|
+
lt: "lt",
|
|
11
|
+
le: "lte",
|
|
12
|
+
gt: "gt",
|
|
13
|
+
ge: "gte",
|
|
14
|
+
};
|
|
7
15
|
function isNamedOperand(operand) {
|
|
8
16
|
return "name" in operand && typeof operand.name === "string";
|
|
9
17
|
}
|
|
@@ -42,6 +50,49 @@ function isResolvedFieldReference(operand) {
|
|
|
42
50
|
function isResolvedValue(operand) {
|
|
43
51
|
return "value" in operand;
|
|
44
52
|
}
|
|
53
|
+
function getNamedOperand(operands, message) {
|
|
54
|
+
const operand = operands.find(isNamedOperand);
|
|
55
|
+
if (!operand) {
|
|
56
|
+
throw new Error(message);
|
|
57
|
+
}
|
|
58
|
+
return operand;
|
|
59
|
+
}
|
|
60
|
+
function getValueOperand(operands, message) {
|
|
61
|
+
const operand = operands.find(isValueOperand);
|
|
62
|
+
if (!operand) {
|
|
63
|
+
throw new Error(message);
|
|
64
|
+
}
|
|
65
|
+
return operand;
|
|
66
|
+
}
|
|
67
|
+
function requireResolvedFieldReference(operand, message) {
|
|
68
|
+
if (!isResolvedFieldReference(operand)) {
|
|
69
|
+
throw new Error(message);
|
|
70
|
+
}
|
|
71
|
+
return operand;
|
|
72
|
+
}
|
|
73
|
+
function requireResolvedValue(operand, message) {
|
|
74
|
+
if (!isResolvedValue(operand)) {
|
|
75
|
+
throw new Error(message);
|
|
76
|
+
}
|
|
77
|
+
return operand;
|
|
78
|
+
}
|
|
79
|
+
function wrapRelations(relations, filter) {
|
|
80
|
+
if (!relations || relations.length === 0) {
|
|
81
|
+
return filter;
|
|
82
|
+
}
|
|
83
|
+
return buildNestedRelationFilter(relations, filter);
|
|
84
|
+
}
|
|
85
|
+
function buildFieldEqualsFilter(fieldRef, value) {
|
|
86
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
87
|
+
return wrapRelations(fieldRef.relations, { [fieldName]: { equals: value } });
|
|
88
|
+
}
|
|
89
|
+
function buildFieldDirectOrInFilter(fieldRef, values) {
|
|
90
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
91
|
+
const baseFilter = values.length === 1
|
|
92
|
+
? { [fieldName]: values[0] }
|
|
93
|
+
: { [fieldName]: { in: values } };
|
|
94
|
+
return wrapRelations(fieldRef.relations, baseFilter);
|
|
95
|
+
}
|
|
45
96
|
/**
|
|
46
97
|
* Converts a Cerbos query plan to a Prisma filter.
|
|
47
98
|
*/
|
|
@@ -180,11 +231,34 @@ function resolveOperand(operand, mapper) {
|
|
|
180
231
|
return { value: operand.value };
|
|
181
232
|
}
|
|
182
233
|
else if (isOperatorOperand(operand)) {
|
|
234
|
+
const folded = tryFoldValueExpression(operand, mapper);
|
|
235
|
+
if (folded !== null)
|
|
236
|
+
return { value: folded };
|
|
183
237
|
const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
|
|
184
238
|
return { value: nestedResult };
|
|
185
239
|
}
|
|
186
240
|
throw new Error("Operand must have name, value, or be an expression");
|
|
187
241
|
}
|
|
242
|
+
function tryFoldValueExpression(expr, mapper) {
|
|
243
|
+
if (expr.operator !== "add")
|
|
244
|
+
return null;
|
|
245
|
+
const leftOp = expr.operands[0];
|
|
246
|
+
const rightOp = expr.operands[1];
|
|
247
|
+
if (!leftOp || !rightOp)
|
|
248
|
+
return null;
|
|
249
|
+
const left = resolveOperand(leftOp, mapper);
|
|
250
|
+
if (!isResolvedValue(left))
|
|
251
|
+
return null;
|
|
252
|
+
const right = resolveOperand(rightOp, mapper);
|
|
253
|
+
if (!isResolvedValue(right))
|
|
254
|
+
return null;
|
|
255
|
+
try {
|
|
256
|
+
return foldAdd(left.value, right.value);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
188
262
|
/**
|
|
189
263
|
* Creates a scoped mapper for collection operations
|
|
190
264
|
*/
|
|
@@ -242,13 +316,8 @@ function createScopedMapper(collectionPath, variableName, fullMapper) {
|
|
|
242
316
|
function buildPrismaFilterFromCerbosExpression(expression, mapper) {
|
|
243
317
|
// A bare named operand represents a boolean field reference (e.g. `R.attr.booleanAttr`)
|
|
244
318
|
if (isNamedOperand(expression)) {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
const fieldFilter = { [fieldName]: { equals: true } };
|
|
248
|
-
if (relations && relations.length > 0) {
|
|
249
|
-
return buildNestedRelationFilter(relations, fieldFilter);
|
|
250
|
-
}
|
|
251
|
-
return fieldFilter;
|
|
319
|
+
const fieldRef = resolveFieldReference(expression.name, mapper);
|
|
320
|
+
return buildFieldEqualsFilter(fieldRef, true);
|
|
252
321
|
}
|
|
253
322
|
if (!isOperatorOperand(expression)) {
|
|
254
323
|
throw new Error("Invalid Cerbos expression structure");
|
|
@@ -270,10 +339,9 @@ function buildPrismaFilterFromCerbosExpression(expression, mapper) {
|
|
|
270
339
|
throw new Error("not operator requires an operand");
|
|
271
340
|
}
|
|
272
341
|
if (isNamedOperand(operand)) {
|
|
273
|
-
const {
|
|
342
|
+
const { relations, ...fieldRef } = resolveFieldReference(operand.name, mapper);
|
|
274
343
|
if (!relations || relations.length === 0) {
|
|
275
|
-
|
|
276
|
-
return { [fieldName]: { equals: false } };
|
|
344
|
+
return buildFieldEqualsFilter(fieldRef, false);
|
|
277
345
|
}
|
|
278
346
|
}
|
|
279
347
|
return {
|
|
@@ -315,6 +383,15 @@ function buildPrismaFilterFromCerbosExpression(expression, mapper) {
|
|
|
315
383
|
case "map": {
|
|
316
384
|
return handleMapOperator(operands, mapper);
|
|
317
385
|
}
|
|
386
|
+
case "overlaps": {
|
|
387
|
+
return handleOverlapsOperator(operands, mapper);
|
|
388
|
+
}
|
|
389
|
+
case "ancestorOf": {
|
|
390
|
+
return handleAncestorDescendantOperator(operands, mapper, "ancestor");
|
|
391
|
+
}
|
|
392
|
+
case "descendentOf": {
|
|
393
|
+
return handleAncestorDescendantOperator(operands, mapper, "descendant");
|
|
394
|
+
}
|
|
318
395
|
default:
|
|
319
396
|
throw new Error(`Unsupported operator: ${operator}`);
|
|
320
397
|
}
|
|
@@ -357,14 +434,7 @@ function handleSizeComparison(operator, sizeOperand, valueOperand, mapper) {
|
|
|
357
434
|
* Helper function to process relational operators (eq, ne, lt, etc.)
|
|
358
435
|
*/
|
|
359
436
|
function handleRelationalOperator(operator, operands, mapper) {
|
|
360
|
-
const prismaOperator =
|
|
361
|
-
eq: "equals",
|
|
362
|
-
ne: "not",
|
|
363
|
-
lt: "lt",
|
|
364
|
-
le: "lte",
|
|
365
|
-
gt: "gt",
|
|
366
|
-
ge: "gte",
|
|
367
|
-
}[operator];
|
|
437
|
+
const prismaOperator = CERBOS_TO_PRISMA_OPERATOR[operator];
|
|
368
438
|
if (!prismaOperator) {
|
|
369
439
|
throw new Error(`Unsupported operator: ${operator}`);
|
|
370
440
|
}
|
|
@@ -377,23 +447,15 @@ function handleRelationalOperator(operator, operands, mapper) {
|
|
|
377
447
|
if (isOperatorOperand(leftOperand) && leftOperand.operator === "size") {
|
|
378
448
|
return handleSizeComparison(operator, leftOperand, rightOperand, mapper);
|
|
379
449
|
}
|
|
450
|
+
const addOperand = [leftOperand, rightOperand].find((o) => isOperatorOperand(o) && o.operator === "add");
|
|
451
|
+
if (addOperand) {
|
|
452
|
+
const otherOperand = addOperand === leftOperand ? rightOperand : leftOperand;
|
|
453
|
+
return handleAddComparison(operator, addOperand, otherOperand, mapper);
|
|
454
|
+
}
|
|
380
455
|
const left = resolveOperand(leftOperand, mapper);
|
|
381
|
-
const right = resolveOperand(rightOperand, mapper);
|
|
456
|
+
const right = requireResolvedValue(resolveOperand(rightOperand, mapper), "Right operand must be a value");
|
|
382
457
|
if (isResolvedFieldReference(left)) {
|
|
383
|
-
|
|
384
|
-
if (!isResolvedValue(right)) {
|
|
385
|
-
throw new Error("Right operand must be a value");
|
|
386
|
-
}
|
|
387
|
-
const filterValue = { [prismaOperator]: right.value };
|
|
388
|
-
const fieldName = getLeafField(path);
|
|
389
|
-
const fieldFilter = { [fieldName]: filterValue };
|
|
390
|
-
if (relations && relations.length > 0) {
|
|
391
|
-
return buildNestedRelationFilter(relations, fieldFilter);
|
|
392
|
-
}
|
|
393
|
-
return fieldFilter;
|
|
394
|
-
}
|
|
395
|
-
if (!isResolvedValue(right)) {
|
|
396
|
-
throw new Error("Right operand must be a value");
|
|
458
|
+
return buildFieldFilter(left, prismaOperator, right.value);
|
|
397
459
|
}
|
|
398
460
|
return { [prismaOperator]: right.value };
|
|
399
461
|
}
|
|
@@ -401,91 +463,41 @@ function handleRelationalOperator(operator, operands, mapper) {
|
|
|
401
463
|
* Helper function to handle "in" operator
|
|
402
464
|
*/
|
|
403
465
|
function handleInOperator(operands, mapper) {
|
|
404
|
-
const nameOperand = operands
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
if (!valueOperand)
|
|
409
|
-
throw new Error("Value operand is undefined");
|
|
410
|
-
const resolved = resolveOperand(nameOperand, mapper);
|
|
411
|
-
if (!isResolvedFieldReference(resolved)) {
|
|
412
|
-
throw new Error("Name operand must resolve to a field reference");
|
|
413
|
-
}
|
|
414
|
-
const { path, relations } = resolved;
|
|
415
|
-
const resolvedValue = resolveOperand(valueOperand, mapper);
|
|
416
|
-
if (!isResolvedValue(resolvedValue)) {
|
|
417
|
-
throw new Error("Value operand must resolve to a value");
|
|
418
|
-
}
|
|
419
|
-
const { value } = resolvedValue;
|
|
466
|
+
const nameOperand = getNamedOperand(operands, "Name operand is undefined");
|
|
467
|
+
const valueOperand = getValueOperand(operands, "Value operand is undefined");
|
|
468
|
+
const fieldRef = requireResolvedFieldReference(resolveOperand(nameOperand, mapper), "Name operand must resolve to a field reference");
|
|
469
|
+
const { value } = requireResolvedValue(resolveOperand(valueOperand, mapper), "Value operand must resolve to a value");
|
|
420
470
|
const values = Array.isArray(value) ? value : [value];
|
|
421
|
-
|
|
422
|
-
if (relations && relations.length > 0) {
|
|
423
|
-
const fieldFilter = values.length === 1
|
|
424
|
-
? { [fieldName]: values[0] }
|
|
425
|
-
: { [fieldName]: { in: values } };
|
|
426
|
-
return buildNestedRelationFilter(relations, fieldFilter);
|
|
427
|
-
}
|
|
428
|
-
return values.length === 1
|
|
429
|
-
? { [fieldName]: values[0] }
|
|
430
|
-
: { [fieldName]: { in: values } };
|
|
471
|
+
return buildFieldDirectOrInFilter(fieldRef, values);
|
|
431
472
|
}
|
|
432
473
|
/**
|
|
433
474
|
* Helper function to handle string operators (contains, startsWith, endsWith)
|
|
434
475
|
*/
|
|
435
476
|
function handleStringOperator(operator, operands, mapper) {
|
|
436
|
-
const nameOperand = operands
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
if (!isResolvedFieldReference(resolved)) {
|
|
441
|
-
throw new Error("Name operand must resolve to a field reference");
|
|
442
|
-
}
|
|
443
|
-
const { path, relations } = resolved;
|
|
444
|
-
const valueOperand = operands.find(isValueOperand);
|
|
445
|
-
if (!valueOperand)
|
|
446
|
-
throw new Error("Value operand is undefined");
|
|
447
|
-
const resolvedValue = resolveOperand(valueOperand, mapper);
|
|
448
|
-
if (!isResolvedValue(resolvedValue)) {
|
|
449
|
-
throw new Error("Value operand must resolve to a value");
|
|
450
|
-
}
|
|
451
|
-
const { value } = resolvedValue;
|
|
477
|
+
const nameOperand = getNamedOperand(operands, "Name operand is undefined");
|
|
478
|
+
const valueOperand = getValueOperand(operands, "Value operand is undefined");
|
|
479
|
+
const fieldRef = requireResolvedFieldReference(resolveOperand(nameOperand, mapper), "Name operand must resolve to a field reference");
|
|
480
|
+
const { value } = requireResolvedValue(resolveOperand(valueOperand, mapper), "Value operand must resolve to a value");
|
|
452
481
|
if (typeof value !== "string") {
|
|
453
482
|
throw new Error(`${operator} operator requires string value`);
|
|
454
483
|
}
|
|
455
|
-
const fieldName = getLeafField(path);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
return fieldFilter;
|
|
484
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
485
|
+
return wrapRelations(fieldRef.relations, {
|
|
486
|
+
[fieldName]: { [operator]: value },
|
|
487
|
+
});
|
|
461
488
|
}
|
|
462
489
|
/**
|
|
463
490
|
* Helper function to handle "isSet" operator
|
|
464
491
|
*/
|
|
465
492
|
function handleIsSetOperator(operands, mapper) {
|
|
466
|
-
const nameOperand = operands
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
const { path, relations } = resolved;
|
|
474
|
-
const valueOperand = operands.find(isValueOperand);
|
|
475
|
-
if (!valueOperand)
|
|
476
|
-
throw new Error("Value operand is undefined");
|
|
477
|
-
const resolvedValue = resolveOperand(valueOperand, mapper);
|
|
478
|
-
if (!isResolvedValue(resolvedValue)) {
|
|
479
|
-
throw new Error("Value operand must resolve to a value");
|
|
480
|
-
}
|
|
481
|
-
const fieldName = getLeafField(path);
|
|
482
|
-
const fieldFilter = {
|
|
493
|
+
const nameOperand = getNamedOperand(operands, "Name operand is undefined");
|
|
494
|
+
const valueOperand = getValueOperand(operands, "Value operand is undefined");
|
|
495
|
+
const fieldRef = requireResolvedFieldReference(resolveOperand(nameOperand, mapper), "Name operand must resolve to a field reference");
|
|
496
|
+
const resolvedValue = requireResolvedValue(resolveOperand(valueOperand, mapper), "Value operand must resolve to a value");
|
|
497
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
498
|
+
return wrapRelations(fieldRef.relations, {
|
|
483
499
|
[fieldName]: resolvedValue.value ? { not: null } : { equals: null },
|
|
484
|
-
};
|
|
485
|
-
if (relations && relations.length > 0) {
|
|
486
|
-
return buildNestedRelationFilter(relations, fieldFilter);
|
|
487
|
-
}
|
|
488
|
-
return fieldFilter;
|
|
500
|
+
});
|
|
489
501
|
}
|
|
490
502
|
/**
|
|
491
503
|
* Helper function to handle "hasIntersection" operator
|
|
@@ -682,4 +694,289 @@ function handleMapOperator(operands, mapper) {
|
|
|
682
694
|
},
|
|
683
695
|
});
|
|
684
696
|
}
|
|
697
|
+
function buildImpossibleFilter(fieldRef) {
|
|
698
|
+
return buildFieldFilter(fieldRef, "in", []);
|
|
699
|
+
}
|
|
700
|
+
function handleAddComparison(operator, addExpr, otherOperand, mapper) {
|
|
701
|
+
const addLeftOp = assertDefined(addExpr.operands[0], "add operator requires a left operand");
|
|
702
|
+
const addRightOp = assertDefined(addExpr.operands[1], "add operator requires a right operand");
|
|
703
|
+
const addLeft = resolveOperand(addLeftOp, mapper);
|
|
704
|
+
const addRight = resolveOperand(addRightOp, mapper);
|
|
705
|
+
const other = resolveOperand(otherOperand, mapper);
|
|
706
|
+
if (isResolvedValue(addLeft) && isResolvedValue(addRight)) {
|
|
707
|
+
const folded = foldAdd(addLeft.value, addRight.value);
|
|
708
|
+
const prismaOp = CERBOS_TO_PRISMA_OPERATOR[operator];
|
|
709
|
+
if (!prismaOp)
|
|
710
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
711
|
+
if (isResolvedFieldReference(other)) {
|
|
712
|
+
return buildFieldFilter(other, prismaOp, folded);
|
|
713
|
+
}
|
|
714
|
+
throw new Error("add with two values requires a field reference on the other side");
|
|
715
|
+
}
|
|
716
|
+
if (!isResolvedValue(other)) {
|
|
717
|
+
throw new Error("add operator with field references requires a value on the other side of the comparison");
|
|
718
|
+
}
|
|
719
|
+
let fieldRef;
|
|
720
|
+
let addValue;
|
|
721
|
+
let fieldIsLeft;
|
|
722
|
+
if (isResolvedFieldReference(addLeft) && isResolvedValue(addRight)) {
|
|
723
|
+
fieldRef = addLeft;
|
|
724
|
+
addValue = addRight.value;
|
|
725
|
+
fieldIsLeft = true;
|
|
726
|
+
}
|
|
727
|
+
else if (isResolvedValue(addLeft) && isResolvedFieldReference(addRight)) {
|
|
728
|
+
fieldRef = addRight;
|
|
729
|
+
addValue = addLeft.value;
|
|
730
|
+
fieldIsLeft = false;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
throw new Error("add operator requires exactly one field reference and one value, or two values");
|
|
734
|
+
}
|
|
735
|
+
if (operator !== "eq" && operator !== "ne") {
|
|
736
|
+
throw new Error(`Operator ${operator} is not supported with add and field references`);
|
|
737
|
+
}
|
|
738
|
+
const solvedValue = solveAdd(other.value, addValue, fieldIsLeft);
|
|
739
|
+
if (solvedValue === null) {
|
|
740
|
+
if (operator === "eq")
|
|
741
|
+
return buildImpossibleFilter(fieldRef);
|
|
742
|
+
return {};
|
|
743
|
+
}
|
|
744
|
+
return buildFieldFilter(fieldRef, CERBOS_TO_PRISMA_OPERATOR[operator], solvedValue);
|
|
745
|
+
}
|
|
746
|
+
function foldAdd(left, right) {
|
|
747
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
748
|
+
return String(left) + String(right);
|
|
749
|
+
}
|
|
750
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
751
|
+
return left + right;
|
|
752
|
+
}
|
|
753
|
+
throw new Error("add operator requires string or number operands");
|
|
754
|
+
}
|
|
755
|
+
function solveAdd(comparisonValue, addConstant, fieldIsLeft) {
|
|
756
|
+
if (typeof comparisonValue === "string" && typeof addConstant === "string") {
|
|
757
|
+
if (fieldIsLeft) {
|
|
758
|
+
if (!comparisonValue.endsWith(addConstant))
|
|
759
|
+
return null;
|
|
760
|
+
return comparisonValue.slice(0, comparisonValue.length - addConstant.length);
|
|
761
|
+
}
|
|
762
|
+
if (!comparisonValue.startsWith(addConstant))
|
|
763
|
+
return null;
|
|
764
|
+
return comparisonValue.slice(addConstant.length);
|
|
765
|
+
}
|
|
766
|
+
if (typeof comparisonValue === "number" && typeof addConstant === "number") {
|
|
767
|
+
return comparisonValue - addConstant;
|
|
768
|
+
}
|
|
769
|
+
throw new Error("Type mismatch in add comparison");
|
|
770
|
+
}
|
|
771
|
+
function resolveHierarchy(expr, mapper) {
|
|
772
|
+
const operands = expr.operands;
|
|
773
|
+
if (operands.length === 2) {
|
|
774
|
+
const strOperand = assertDefined(operands[0], "hierarchy requires operands");
|
|
775
|
+
const delimOperand = assertDefined(operands[1], "hierarchy requires a delimiter");
|
|
776
|
+
if (!isValueOperand(delimOperand)) {
|
|
777
|
+
throw new Error("hierarchy delimiter must be a value");
|
|
778
|
+
}
|
|
779
|
+
const delimiter = String(delimOperand.value);
|
|
780
|
+
if (isValueOperand(strOperand)) {
|
|
781
|
+
const raw = String(strOperand.value);
|
|
782
|
+
return { type: "constant", segments: raw.split(delimiter), raw, delimiter };
|
|
783
|
+
}
|
|
784
|
+
if (isNamedOperand(strOperand)) {
|
|
785
|
+
return {
|
|
786
|
+
type: "field",
|
|
787
|
+
fieldRef: resolveFieldReference(strOperand.name, mapper),
|
|
788
|
+
delimiter,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
throw new Error("hierarchy(string, delimiter) requires a value or field operand");
|
|
792
|
+
}
|
|
793
|
+
if (operands.length === 1) {
|
|
794
|
+
const inner = assertDefined(operands[0], "hierarchy requires an operand");
|
|
795
|
+
if (isValueOperand(inner)) {
|
|
796
|
+
const raw = String(inner.value);
|
|
797
|
+
return { type: "constant", segments: raw.split("."), raw, delimiter: "." };
|
|
798
|
+
}
|
|
799
|
+
if (isNamedOperand(inner)) {
|
|
800
|
+
return {
|
|
801
|
+
type: "field",
|
|
802
|
+
fieldRef: resolveFieldReference(inner.name, mapper),
|
|
803
|
+
delimiter: ".",
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (isOperatorOperand(inner) && inner.operator === "list") {
|
|
807
|
+
const segments = inner.operands.map((op) => {
|
|
808
|
+
const resolved = resolveOperand(op, mapper);
|
|
809
|
+
if (isResolvedValue(resolved)) {
|
|
810
|
+
return { type: "constant", value: String(resolved.value) };
|
|
811
|
+
}
|
|
812
|
+
return { type: "field", fieldRef: resolved };
|
|
813
|
+
});
|
|
814
|
+
return { type: "segmented", segments };
|
|
815
|
+
}
|
|
816
|
+
throw new Error("hierarchy requires a value, field, or list operand");
|
|
817
|
+
}
|
|
818
|
+
throw new Error("hierarchy requires 1 or 2 operands");
|
|
819
|
+
}
|
|
820
|
+
function toSegments(resolved) {
|
|
821
|
+
switch (resolved.type) {
|
|
822
|
+
case "constant":
|
|
823
|
+
return resolved.segments.map((s) => ({ type: "constant", value: s }));
|
|
824
|
+
case "segmented":
|
|
825
|
+
return resolved.segments;
|
|
826
|
+
case "field":
|
|
827
|
+
throw new Error("Cannot get segments from a field-reference hierarchy");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function normalizeHierarchy(h, defaultDelimiter = ".") {
|
|
831
|
+
if (h.type !== "segmented")
|
|
832
|
+
return h;
|
|
833
|
+
const allConstant = h.segments.every((s) => s.type === "constant");
|
|
834
|
+
if (!allConstant)
|
|
835
|
+
return h;
|
|
836
|
+
const segments = h.segments.map((s) => s.value);
|
|
837
|
+
return {
|
|
838
|
+
type: "constant",
|
|
839
|
+
segments,
|
|
840
|
+
raw: segments.join(defaultDelimiter),
|
|
841
|
+
delimiter: defaultDelimiter,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function checkPrefixConditions(shorter, longer) {
|
|
845
|
+
if (shorter.length > longer.length)
|
|
846
|
+
return null;
|
|
847
|
+
const conditions = [];
|
|
848
|
+
for (let i = 0; i < shorter.length; i++) {
|
|
849
|
+
const s = shorter[i];
|
|
850
|
+
const l = longer[i];
|
|
851
|
+
if (s.type === "constant" && l.type === "constant") {
|
|
852
|
+
if (s.value !== l.value)
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
else if (s.type === "field" && l.type === "constant") {
|
|
856
|
+
conditions.push(buildFieldFilter(s.fieldRef, "equals", l.value));
|
|
857
|
+
}
|
|
858
|
+
else if (s.type === "constant" && l.type === "field") {
|
|
859
|
+
conditions.push(buildFieldFilter(l.fieldRef, "equals", s.value));
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
throw new Error("Cannot compare two field references in hierarchy overlap");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (conditions.length === 0)
|
|
866
|
+
return {};
|
|
867
|
+
if (conditions.length === 1)
|
|
868
|
+
return conditions[0];
|
|
869
|
+
return { AND: conditions };
|
|
870
|
+
}
|
|
871
|
+
function handleOverlapsOperator(operands, mapper) {
|
|
872
|
+
const [left, right] = extractHierarchyOperands("overlaps", operands, mapper);
|
|
873
|
+
if (left.type === "field" || right.type === "field") {
|
|
874
|
+
return handleFieldOverlaps(left, right);
|
|
875
|
+
}
|
|
876
|
+
const leftSegs = toSegments(left);
|
|
877
|
+
const rightSegs = toSegments(right);
|
|
878
|
+
const leftPrefixOfRight = checkPrefixConditions(leftSegs, rightSegs);
|
|
879
|
+
const rightPrefixOfLeft = checkPrefixConditions(rightSegs, leftSegs);
|
|
880
|
+
const validConditions = [leftPrefixOfRight, rightPrefixOfLeft].filter((c) => c !== null);
|
|
881
|
+
if (validConditions.length === 0) {
|
|
882
|
+
const allSegs = [...leftSegs, ...rightSegs];
|
|
883
|
+
const fieldSeg = allSegs.find((s) => s.type === "field");
|
|
884
|
+
if (fieldSeg)
|
|
885
|
+
return buildImpossibleFilter(fieldSeg.fieldRef);
|
|
886
|
+
throw new Error("Cannot determine overlap: no field references found");
|
|
887
|
+
}
|
|
888
|
+
if (validConditions.some((c) => Object.keys(c).length === 0))
|
|
889
|
+
return {};
|
|
890
|
+
// When both directions are valid (equal-length hierarchies), they produce
|
|
891
|
+
// identical conditions since the same segment pairs are compared in both.
|
|
892
|
+
return validConditions[0];
|
|
893
|
+
}
|
|
894
|
+
function handleFieldOverlaps(left, right) {
|
|
895
|
+
if (left.type === "field" && right.type === "field") {
|
|
896
|
+
throw new Error("overlaps: cannot compare two field-reference hierarchies");
|
|
897
|
+
}
|
|
898
|
+
const field = (left.type === "field" ? left : right);
|
|
899
|
+
const other = left.type === "field" ? right : left;
|
|
900
|
+
if (other.type !== "constant") {
|
|
901
|
+
throw new Error("overlaps: segmented hierarchies with field hierarchies are not supported");
|
|
902
|
+
}
|
|
903
|
+
const delimiter = field.delimiter;
|
|
904
|
+
const otherRaw = other.segments.join(delimiter);
|
|
905
|
+
const strictPrefixes = getStrictPrefixes(other.segments, delimiter);
|
|
906
|
+
const conditions = [];
|
|
907
|
+
if (strictPrefixes.length > 0) {
|
|
908
|
+
conditions.push(buildFieldFilter(field.fieldRef, "in", strictPrefixes));
|
|
909
|
+
}
|
|
910
|
+
conditions.push(buildFieldFilter(field.fieldRef, "equals", otherRaw));
|
|
911
|
+
conditions.push(buildFieldFilter(field.fieldRef, "startsWith", otherRaw + delimiter));
|
|
912
|
+
if (conditions.length === 1)
|
|
913
|
+
return conditions[0];
|
|
914
|
+
return { OR: conditions };
|
|
915
|
+
}
|
|
916
|
+
function extractHierarchyOperands(operatorName, operands, mapper) {
|
|
917
|
+
if (operands.length !== 2) {
|
|
918
|
+
throw new Error(`${operatorName} requires exactly two operands`);
|
|
919
|
+
}
|
|
920
|
+
const leftOp = assertDefined(operands[0], `${operatorName} requires a left operand`);
|
|
921
|
+
const rightOp = assertDefined(operands[1], `${operatorName} requires a right operand`);
|
|
922
|
+
if (!isOperatorOperand(leftOp) || leftOp.operator !== "hierarchy" ||
|
|
923
|
+
!isOperatorOperand(rightOp) || rightOp.operator !== "hierarchy") {
|
|
924
|
+
throw new Error(`${operatorName} requires two hierarchy operands`);
|
|
925
|
+
}
|
|
926
|
+
return [
|
|
927
|
+
normalizeHierarchy(resolveHierarchy(leftOp, mapper)),
|
|
928
|
+
normalizeHierarchy(resolveHierarchy(rightOp, mapper)),
|
|
929
|
+
];
|
|
930
|
+
}
|
|
931
|
+
function getStrictPrefixes(segments, delimiter) {
|
|
932
|
+
if (segments.length <= 1)
|
|
933
|
+
return [];
|
|
934
|
+
const prefixes = [];
|
|
935
|
+
let current = segments[0];
|
|
936
|
+
prefixes.push(current);
|
|
937
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
938
|
+
current = current + delimiter + segments[i];
|
|
939
|
+
prefixes.push(current);
|
|
940
|
+
}
|
|
941
|
+
return prefixes;
|
|
942
|
+
}
|
|
943
|
+
function handleAncestorDescendantOperator(operands, mapper, direction) {
|
|
944
|
+
const operatorName = direction === "ancestor" ? "ancestorOf" : "descendentOf";
|
|
945
|
+
const [left, right] = extractHierarchyOperands(operatorName, operands, mapper);
|
|
946
|
+
// ancestorOf(A, B) = A is strict prefix of B
|
|
947
|
+
// descendentOf(A, B) = B is strict prefix of A
|
|
948
|
+
const ancestor = direction === "ancestor" ? left : right;
|
|
949
|
+
const descendant = direction === "ancestor" ? right : left;
|
|
950
|
+
if (ancestor.type === "constant" && descendant.type === "field") {
|
|
951
|
+
const prefix = ancestor.segments.join(descendant.delimiter) + descendant.delimiter;
|
|
952
|
+
return buildFieldFilter(descendant.fieldRef, "startsWith", prefix);
|
|
953
|
+
}
|
|
954
|
+
if (ancestor.type === "field" && descendant.type === "constant") {
|
|
955
|
+
const delimiter = ancestor.delimiter;
|
|
956
|
+
const prefixes = getStrictPrefixes(descendant.segments, delimiter);
|
|
957
|
+
if (prefixes.length === 0) {
|
|
958
|
+
return buildImpossibleFilter(ancestor.fieldRef);
|
|
959
|
+
}
|
|
960
|
+
if (prefixes.length === 1) {
|
|
961
|
+
return buildFieldFilter(ancestor.fieldRef, "equals", prefixes[0]);
|
|
962
|
+
}
|
|
963
|
+
return buildFieldFilter(ancestor.fieldRef, "in", prefixes);
|
|
964
|
+
}
|
|
965
|
+
if (ancestor.type === "constant" && descendant.type === "constant") {
|
|
966
|
+
const ancestorSegs = ancestor.segments;
|
|
967
|
+
const descendantSegs = descendant.segments;
|
|
968
|
+
if (descendantSegs.length > ancestorSegs.length &&
|
|
969
|
+
ancestorSegs.every((seg, i) => seg === descendantSegs[i])) {
|
|
970
|
+
return {};
|
|
971
|
+
}
|
|
972
|
+
throw new Error(`${operatorName}: constants do not satisfy ${direction} relationship`);
|
|
973
|
+
}
|
|
974
|
+
throw new Error(`${operatorName}: unsupported hierarchy type combination`);
|
|
975
|
+
}
|
|
976
|
+
function buildFieldFilter(fieldRef, prismaOp, value) {
|
|
977
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
978
|
+
return wrapRelations(fieldRef.relations, {
|
|
979
|
+
[fieldName]: { [prismaOp]: value },
|
|
980
|
+
});
|
|
981
|
+
}
|
|
685
982
|
//# sourceMappingURL=index.js.map
|