@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 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
@@ -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;AAGpB,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;AA2FN;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,MAAW,GACZ,EAAE,qBAAqB,GAAG,uBAAuB,CAiBjD"}
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 { path, relations } = resolveFieldReference(expression.name, mapper);
246
- const fieldName = getLeafField(path);
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 { path, relations } = resolveFieldReference(operand.name, mapper);
342
+ const { relations, ...fieldRef } = resolveFieldReference(operand.name, mapper);
274
343
  if (!relations || relations.length === 0) {
275
- const fieldName = getLeafField(path);
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
- const { path, relations } = left;
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.find(isNamedOperand);
405
- if (!nameOperand)
406
- throw new Error("Name operand is undefined");
407
- const valueOperand = operands.find(isValueOperand);
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
- const fieldName = getLeafField(path);
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.find(isNamedOperand);
437
- if (!nameOperand)
438
- throw new Error("Name operand is undefined");
439
- const resolved = resolveOperand(nameOperand, mapper);
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
- const fieldFilter = { [fieldName]: { [operator]: value } };
457
- if (relations && relations.length > 0) {
458
- return buildNestedRelationFilter(relations, fieldFilter);
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.find(isNamedOperand);
467
- if (!nameOperand)
468
- throw new Error("Name operand is undefined");
469
- const resolved = resolveOperand(nameOperand, mapper);
470
- if (!isResolvedFieldReference(resolved)) {
471
- throw new Error("Name operand must resolve to a field reference");
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