@cerbos/orm-prisma 4.1.0 → 5.0.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.
Files changed (174) hide show
  1. package/README.md +598 -603
  2. package/lib/collections.d.ts +30 -0
  3. package/lib/collections.d.ts.map +1 -0
  4. package/lib/collections.js +421 -0
  5. package/lib/collections.js.map +1 -0
  6. package/lib/comparison.d.ts +24 -0
  7. package/lib/comparison.d.ts.map +1 -0
  8. package/lib/comparison.js +731 -0
  9. package/lib/comparison.js.map +1 -0
  10. package/lib/errors.d.ts +13 -0
  11. package/lib/errors.d.ts.map +1 -0
  12. package/lib/errors.js +17 -0
  13. package/lib/errors.js.map +1 -0
  14. package/lib/evaluate.d.ts +5 -0
  15. package/lib/evaluate.d.ts.map +1 -0
  16. package/lib/evaluate.js +118 -0
  17. package/lib/evaluate.js.map +1 -0
  18. package/lib/fields.d.ts +34 -0
  19. package/lib/fields.d.ts.map +1 -0
  20. package/lib/fields.js +253 -0
  21. package/lib/fields.js.map +1 -0
  22. package/lib/functions.d.ts +5 -0
  23. package/lib/functions.d.ts.map +1 -0
  24. package/lib/functions.js +408 -0
  25. package/lib/functions.js.map +1 -0
  26. package/lib/hierarchy.d.ts +6 -0
  27. package/lib/hierarchy.d.ts.map +1 -0
  28. package/lib/hierarchy.js +266 -0
  29. package/lib/hierarchy.js.map +1 -0
  30. package/lib/index.d.ts +23 -13
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +34 -2555
  33. package/lib/index.js.map +1 -1
  34. package/lib/literals.d.ts +20 -0
  35. package/lib/literals.d.ts.map +1 -0
  36. package/lib/literals.js +290 -0
  37. package/lib/literals.js.map +1 -0
  38. package/lib/mapping.d.ts +95 -0
  39. package/lib/mapping.d.ts.map +1 -0
  40. package/lib/mapping.js +316 -0
  41. package/lib/mapping.js.map +1 -0
  42. package/lib/narrowing.d.ts +12 -0
  43. package/lib/narrowing.d.ts.map +1 -0
  44. package/lib/narrowing.js +32 -0
  45. package/lib/narrowing.js.map +1 -0
  46. package/lib/plan.d.ts +49 -0
  47. package/lib/plan.d.ts.map +1 -0
  48. package/lib/plan.js +86 -0
  49. package/lib/plan.js.map +1 -0
  50. package/lib/presence.d.ts +25 -0
  51. package/lib/presence.d.ts.map +1 -0
  52. package/lib/presence.js +82 -0
  53. package/lib/presence.js.map +1 -0
  54. package/lib/regex.d.ts +33 -0
  55. package/lib/regex.d.ts.map +1 -0
  56. package/lib/regex.js +480 -0
  57. package/lib/regex.js.map +1 -0
  58. package/lib/relations.d.ts +58 -0
  59. package/lib/relations.d.ts.map +1 -0
  60. package/lib/relations.js +175 -0
  61. package/lib/relations.js.map +1 -0
  62. package/lib/rewrite.d.ts +36 -0
  63. package/lib/rewrite.d.ts.map +1 -0
  64. package/lib/rewrite.js +366 -0
  65. package/lib/rewrite.js.map +1 -0
  66. package/lib/solve.d.ts +20 -0
  67. package/lib/solve.d.ts.map +1 -0
  68. package/lib/solve.js +77 -0
  69. package/lib/solve.js.map +1 -0
  70. package/lib/strings.d.ts +12 -0
  71. package/lib/strings.d.ts.map +1 -0
  72. package/lib/strings.js +110 -0
  73. package/lib/strings.js.map +1 -0
  74. package/lib/ternary.d.ts +12 -0
  75. package/lib/ternary.d.ts.map +1 -0
  76. package/lib/ternary.js +192 -0
  77. package/lib/ternary.js.map +1 -0
  78. package/lib/timestamp.d.ts +45 -0
  79. package/lib/timestamp.d.ts.map +1 -0
  80. package/lib/timestamp.js +225 -0
  81. package/lib/timestamp.js.map +1 -0
  82. package/lib/translate.d.ts +20 -0
  83. package/lib/translate.d.ts.map +1 -0
  84. package/lib/translate.js +287 -0
  85. package/lib/translate.js.map +1 -0
  86. package/lib/types.d.ts +29 -0
  87. package/lib/types.d.ts.map +1 -0
  88. package/lib/types.js +980 -0
  89. package/lib/types.js.map +1 -0
  90. package/lib/value.d.ts +8 -0
  91. package/lib/value.d.ts.map +1 -0
  92. package/lib/value.js +7 -0
  93. package/lib/value.js.map +1 -0
  94. package/package.json +27 -17
  95. package/src/adversarial.test.ts +376 -1071
  96. package/src/collections.ts +738 -0
  97. package/src/comparison.ts +1157 -0
  98. package/src/corpus.ts +433 -250
  99. package/src/errors.ts +12 -0
  100. package/src/evaluate.ts +170 -0
  101. package/src/fields.ts +373 -0
  102. package/src/functions.ts +548 -0
  103. package/src/generated/prisma-adversarial/browser.ts +10 -0
  104. package/src/generated/prisma-adversarial/client.ts +10 -0
  105. package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
  106. package/src/generated/prisma-adversarial/internal/class.ts +24 -4
  107. package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +174 -3
  108. package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +23 -2
  109. package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
  110. package/src/generated/prisma-adversarial/models/AdversarialInner.ts +54 -54
  111. package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
  112. package/src/generated/prisma-adversarial/models/AdversarialParent.ts +66 -66
  113. package/src/generated/prisma-adversarial/models/AdversarialResource.ts +435 -87
  114. package/src/generated/prisma-adversarial/models.ts +2 -0
  115. package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
  116. package/src/generated/prisma-adversarial-mysql/client.ts +86 -0
  117. package/src/generated/prisma-adversarial-mysql/commonInputTypes.ts +349 -0
  118. package/src/generated/prisma-adversarial-mysql/enums.ts +15 -0
  119. package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
  120. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
  121. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
  122. package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
  123. package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
  124. package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
  125. package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
  126. package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
  127. package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
  128. package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
  129. package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
  130. package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
  131. package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
  132. package/src/generated/prisma-adversarial-mysql-v6/client.d.ts +1 -0
  133. package/src/generated/prisma-adversarial-mysql-v6/default.d.ts +1 -0
  134. package/src/generated/prisma-adversarial-mysql-v6/edge.d.ts +1 -0
  135. package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
  136. package/src/generated/prisma-adversarial-mysql-v6/runtime/index-browser.d.ts +370 -0
  137. package/src/generated/prisma-adversarial-mysql-v6/runtime/library.d.ts +3982 -0
  138. package/src/generated/prisma-adversarial-mysql-v6/wasm.d.ts +1 -0
  139. package/src/generated/prisma-adversarial-pg/browser.ts +10 -0
  140. package/src/generated/prisma-adversarial-pg/client.ts +10 -0
  141. package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +118 -129
  142. package/src/generated/prisma-adversarial-pg/internal/class.ts +24 -4
  143. package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +174 -3
  144. package/src/generated/prisma-adversarial-pg/internal/prismaNamespaceBrowser.ts +23 -2
  145. package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
  146. package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +54 -54
  147. package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
  148. package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +66 -66
  149. package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +435 -87
  150. package/src/generated/prisma-adversarial-pg/models.ts +2 -0
  151. package/src/generated/prisma-adversarial-pg-v6/index.d.ts +3476 -388
  152. package/src/generated/prisma-adversarial-v6/index.d.ts +3465 -383
  153. package/src/hierarchy.ts +405 -0
  154. package/src/index.ts +66 -3646
  155. package/src/literals.ts +363 -0
  156. package/src/mapping.ts +474 -0
  157. package/src/narrowing.ts +33 -0
  158. package/src/plan.ts +125 -0
  159. package/src/presence.ts +114 -0
  160. package/src/regex.ts +596 -0
  161. package/src/relations.ts +226 -0
  162. package/src/rewrite.ts +524 -0
  163. package/src/solve.ts +96 -0
  164. package/src/strings.ts +152 -0
  165. package/src/ternary.ts +359 -0
  166. package/src/test-setup.adversarial-url.ts +6 -3
  167. package/src/test-setup.adversarial.mysql.v6.ts +13 -0
  168. package/src/test-setup.adversarial.mysql.v7.ts +12 -0
  169. package/src/test-setup.adversarial.v7.ts +1 -0
  170. package/src/timestamp.ts +308 -0
  171. package/src/translate.ts +419 -0
  172. package/src/translator.test.ts +497 -1317
  173. package/src/types.ts +1310 -0
  174. package/src/value.ts +16 -0
@@ -0,0 +1,58 @@
1
+ import type { PlanExpressionOperand } from "@cerbos/core";
2
+ import type { PrismaFilter } from "./index";
3
+ import type { RelationConfig, TranslationContext } from "./mapping";
4
+ /** The Prisma operator that reaches into a relation for an existence test. */
5
+ export declare function relationOperator(relation: RelationConfig): string;
6
+ /**
7
+ * One relation-scoped filter (`{ tags: { some: … } }`), narrowed by the store-side predicate the
8
+ * caller declared on the mapping.
9
+ *
10
+ * Every nested relation filter the adapter emits goes through here, so a declared
11
+ * `subqueryFilter` reaches the collection macros, the membership shapes, the emptiness checks and
12
+ * the hop-existence guard alike. Adding a new relation shape means routing it through here, or
13
+ * the declaration silently stops applying to it.
14
+ *
15
+ * `every` is the one operator that cannot simply absorb the predicate. `{ every: AND(W, P) }`
16
+ * would REQUIRE every record to satisfy W, which is the opposite of "ignore the records W hides",
17
+ * so it is rewritten to `{ none: AND(W, NOT P) }` — no visible record violates P. Both agree with
18
+ * `every` when nothing is declared, and both stay vacuously true over an empty relation.
19
+ */
20
+ export declare function relationFilter(relation: RelationConfig, operator: string, inner: PrismaFilter): PrismaFilter;
21
+ /**
22
+ * `filter` nested through every relation in `relations`, outermost first, each reached with its
23
+ * existence operator (`is` for to-one, `some` for to-many). No relations leaves it unchanged.
24
+ */
25
+ export declare function wrapInRelations(relations: RelationConfig[] | undefined, filter: PrismaFilter): PrismaFilter;
26
+ /**
27
+ * "Every intermediate hop of a dotted path exists", as a Prisma filter.
28
+ *
29
+ * CEL cannot dot through a list, so each intermediate segment of `a.b.c` is a to-ONE
30
+ * parent: absent, the caller sends no attribute and CEL raises a missing-path error, which
31
+ * denies. `NOT { categories: { some: { subCategories: { some: P } } } }` is TRUE for a row
32
+ * with no category at all, so the negation returns rows the PDP denies — the parent must be
33
+ * required separately (cerbos/query-plan-adapters#309).
34
+ *
35
+ * Returns undefined when the reference has no intermediate hop, so a direct relation keeps
36
+ * its empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
37
+ */
38
+ export declare function buildLeadingHopsExistFilter(head: RelationConfig, restRelations: RelationConfig[]): PrismaFilter | undefined;
39
+ /**
40
+ * `{ NOT: filter }` for a filter translated from a plan sub-expression.
41
+ *
42
+ * An unconditional filter (`{}`, which the hierarchy operators return for a relationship that
43
+ * holds between two constants) cannot be negated this way: Prisma drops an empty condition, so
44
+ * `{ NOT: {} }` is true and matches every row, where the negation of an unconditional condition
45
+ * matches none. Prisma has no field-free false condition to emit instead, so the shape is refused.
46
+ */
47
+ export declare function negateFilter(filter: PrismaFilter): PrismaFilter;
48
+ /**
49
+ * `{ NOT: filter }`, with every to-one hop `operand` dots through required OUTSIDE the
50
+ * negation so an absent parent stays denied (see buildExpressionHopsExistFilter).
51
+ */
52
+ export declare function negateRequiringHops(operand: PlanExpressionOperand, filter: PrismaFilter, context: TranslationContext): PrismaFilter;
53
+ /**
54
+ * Whether negating `expr` needs a hop requirement (see buildExpressionHopsExistFilter): it dots
55
+ * through at least one to-one hop, which is absent on a parentless row.
56
+ */
57
+ export declare function referencesRequiredHop(expr: PlanExpressionOperand, context: TranslationContext): boolean;
58
+ //# sourceMappingURL=relations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../src/relations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAIpE,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAEjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,GAClB,YAAY,CAoBd;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,cAAc,EAAE,GAAG,SAAS,EACvC,MAAM,EAAE,YAAY,GACnB,YAAY,CAQd;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,cAAc,EAAE,GAC9B,YAAY,GAAG,SAAS,CAO1B;AA6ED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAS/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,kBAAkB,GAC1B,YAAY,CAKd;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,qBAAqB,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAET"}
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ // Nesting a filter through the relations a reference crosses, and the hop-existence guards that
3
+ // keep an absent to-one parent denied under negation.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.relationOperator = relationOperator;
6
+ exports.relationFilter = relationFilter;
7
+ exports.wrapInRelations = wrapInRelations;
8
+ exports.buildLeadingHopsExistFilter = buildLeadingHopsExistFilter;
9
+ exports.negateFilter = negateFilter;
10
+ exports.negateRequiringHops = negateRequiringHops;
11
+ exports.referencesRequiredHop = referencesRequiredHop;
12
+ const mapping_1 = require("./mapping");
13
+ const plan_1 = require("./plan");
14
+ const errors_1 = require("./errors");
15
+ /** The Prisma operator that reaches into a relation for an existence test. */
16
+ function relationOperator(relation) {
17
+ return relation.type === "one" ? "is" : "some";
18
+ }
19
+ /**
20
+ * One relation-scoped filter (`{ tags: { some: … } }`), narrowed by the store-side predicate the
21
+ * caller declared on the mapping.
22
+ *
23
+ * Every nested relation filter the adapter emits goes through here, so a declared
24
+ * `subqueryFilter` reaches the collection macros, the membership shapes, the emptiness checks and
25
+ * the hop-existence guard alike. Adding a new relation shape means routing it through here, or
26
+ * the declaration silently stops applying to it.
27
+ *
28
+ * `every` is the one operator that cannot simply absorb the predicate. `{ every: AND(W, P) }`
29
+ * would REQUIRE every record to satisfy W, which is the opposite of "ignore the records W hides",
30
+ * so it is rewritten to `{ none: AND(W, NOT P) }` — no visible record violates P. Both agree with
31
+ * `every` when nothing is declared, and both stay vacuously true over an empty relation.
32
+ */
33
+ function relationFilter(relation, operator, inner) {
34
+ const declared = relation.subqueryFilter;
35
+ if (declared === undefined) {
36
+ return { [relation.name]: { [operator]: inner } };
37
+ }
38
+ const isEmpty = Object.keys(inner).length === 0;
39
+ if (operator === "every") {
40
+ return isEmpty
41
+ ? { [relation.name]: { every: inner } }
42
+ : { [relation.name]: { none: { AND: [declared, { NOT: inner }] } } };
43
+ }
44
+ return {
45
+ [relation.name]: {
46
+ [operator]: isEmpty ? declared : { AND: [declared, inner] },
47
+ },
48
+ };
49
+ }
50
+ /**
51
+ * `filter` nested through every relation in `relations`, outermost first, each reached with its
52
+ * existence operator (`is` for to-one, `some` for to-many). No relations leaves it unchanged.
53
+ */
54
+ function wrapInRelations(relations, filter) {
55
+ let current = filter;
56
+ for (const relation of [...(relations ?? [])].reverse()) {
57
+ current = relationFilter(relation, relationOperator(relation), current);
58
+ }
59
+ return current;
60
+ }
61
+ /**
62
+ * "Every one of these relations exists", as a nested Prisma filter with an empty innermost
63
+ * predicate — `{ parent: { is: { inner: { is: {} } } } }`.
64
+ */
65
+ function buildHopsExistFilter(hops) {
66
+ return hops.length === 0 ? undefined : wrapInRelations(hops, {});
67
+ }
68
+ /**
69
+ * "Every intermediate hop of a dotted path exists", as a Prisma filter.
70
+ *
71
+ * CEL cannot dot through a list, so each intermediate segment of `a.b.c` is a to-ONE
72
+ * parent: absent, the caller sends no attribute and CEL raises a missing-path error, which
73
+ * denies. `NOT { categories: { some: { subCategories: { some: P } } } }` is TRUE for a row
74
+ * with no category at all, so the negation returns rows the PDP denies — the parent must be
75
+ * required separately (cerbos/query-plan-adapters#309).
76
+ *
77
+ * Returns undefined when the reference has no intermediate hop, so a direct relation keeps
78
+ * its empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
79
+ */
80
+ function buildLeadingHopsExistFilter(head, restRelations) {
81
+ if (restRelations.length === 0) {
82
+ return undefined;
83
+ }
84
+ // The last entry is the collection being iterated; everything before it is a hop.
85
+ return buildHopsExistFilter([head, ...restRelations.slice(0, -1)]);
86
+ }
87
+ /**
88
+ * "Every to-one hop this expression dots through exists", as a Prisma filter, or undefined
89
+ * when it dots through none.
90
+ *
91
+ * Prisma has no UNKNOWN: a chained relation filter is exact under the POSITIVE polarity —
92
+ * `categories: { some: { subCategories: { some: P } } }` cannot hold without the category —
93
+ * but `NOT` inverts the hop requirement along with the predicate, so every operator built on
94
+ * a chain readmits the parentless rows once negated. #309 fixed that inside the collection
95
+ * macros, which left the sibling operators reached through the same chain unguarded:
96
+ * membership and `hasIntersection` (cerbos/query-plan-adapters#315), and the negated count
97
+ * spelling `!(size(chain) > 0)` (#316), all of which the macros' guard never saw.
98
+ *
99
+ * Requiring the hops OUTSIDE the negation is what fixes all of them at once, and it is
100
+ * unconditionally faithful to CEL rather than a per-operator patch: a missing hop is a
101
+ * missing-path error, which denies under BOTH polarities, so the requirement never depends
102
+ * on which operator sits above the chain.
103
+ *
104
+ * Lambda bodies are deliberately not walked — their references resolve against a scoped
105
+ * mapper, and a negated macro carries its own guard through the negated collection operators.
106
+ */
107
+ function buildExpressionHopsExistFilter(operand, context) {
108
+ const hops = [];
109
+ const seen = new Set();
110
+ const visit = (expr) => {
111
+ if ((0, plan_1.isNamedOperand)(expr)) {
112
+ if (seen.has(expr.name)) {
113
+ return;
114
+ }
115
+ seen.add(expr.name);
116
+ const { relations } = (0, mapping_1.resolveFieldReference)(expr.name, context);
117
+ if (!relations || relations.length === 0) {
118
+ return;
119
+ }
120
+ const last = relations[relations.length - 1];
121
+ // Every to-ONE relation on the path is a hop that must EXIST — including a TRAILING one,
122
+ // which is the case a chain of to-many relations never produces. `parent.aBool` resolves
123
+ // to a single to-one relation, and `NOT { parent: { is: { aBool: true } } }` is TRUE for
124
+ // a row with no parent at all, so the negation returns rows the PDP denies. A trailing
125
+ // to-MANY relation is the collection being iterated rather than a hop, and keeps its
126
+ // empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
127
+ const hopFilter = buildHopsExistFilter(last.type === "one" ? relations : relations.slice(0, -1));
128
+ if (hopFilter !== undefined) {
129
+ hops.push(hopFilter);
130
+ }
131
+ return;
132
+ }
133
+ if (!(0, plan_1.isOperatorOperand)(expr) || expr.operator === "lambda") {
134
+ return;
135
+ }
136
+ expr.operands.forEach(visit);
137
+ };
138
+ visit(operand);
139
+ if (hops.length === 0) {
140
+ return undefined;
141
+ }
142
+ return hops.length === 1 ? hops[0] : { AND: hops };
143
+ }
144
+ /**
145
+ * `{ NOT: filter }` for a filter translated from a plan sub-expression.
146
+ *
147
+ * An unconditional filter (`{}`, which the hierarchy operators return for a relationship that
148
+ * holds between two constants) cannot be negated this way: Prisma drops an empty condition, so
149
+ * `{ NOT: {} }` is true and matches every row, where the negation of an unconditional condition
150
+ * matches none. Prisma has no field-free false condition to emit instead, so the shape is refused.
151
+ */
152
+ function negateFilter(filter) {
153
+ if (Object.keys(filter).length === 0) {
154
+ throw new errors_1.UnsupportedQueryPlanError("Cannot negate an unconditional filter: Prisma evaluates { NOT: {} } as true and matches " +
155
+ "every row, where the negated condition matches none");
156
+ }
157
+ return { NOT: filter };
158
+ }
159
+ /**
160
+ * `{ NOT: filter }`, with every to-one hop `operand` dots through required OUTSIDE the
161
+ * negation so an absent parent stays denied (see buildExpressionHopsExistFilter).
162
+ */
163
+ function negateRequiringHops(operand, filter, context) {
164
+ const hops = buildExpressionHopsExistFilter(operand, context);
165
+ const negated = negateFilter(filter);
166
+ return hops === undefined ? negated : { AND: [hops, negated] };
167
+ }
168
+ /**
169
+ * Whether negating `expr` needs a hop requirement (see buildExpressionHopsExistFilter): it dots
170
+ * through at least one to-one hop, which is absent on a parentless row.
171
+ */
172
+ function referencesRequiredHop(expr, context) {
173
+ return buildExpressionHopsExistFilter(expr, context) !== undefined;
174
+ }
175
+ //# sourceMappingURL=relations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.js","sourceRoot":"","sources":["../src/relations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { PlanExpressionOperand, Value } from "@cerbos/core";
2
+ /** The boolean collection macros: each takes a collection and a lambda. */
3
+ export declare const COLLECTION_OPERATORS: Set<string>;
4
+ export declare function containsCollectionOperator(expr: PlanExpressionOperand): boolean;
5
+ /**
6
+ * Substitute a lambda iteration variable with a concrete collection element
7
+ * inside a lambda body. A bare reference to the variable becomes the element
8
+ * itself; a `variable.path.to.field` reference drills into the element. A
9
+ * nested collection macro whose lambda rebinds the same variable name shadows
10
+ * the outer variable, so substitution only descends into its collection
11
+ * operand.
12
+ */
13
+ export declare function substituteLambdaVariable(operand: PlanExpressionOperand, variableName: string, element: Value): PlanExpressionOperand;
14
+ /**
15
+ * Rewrites lambda bodies that reference columns of an ENCLOSING scope (the root row, or an
16
+ * outer lambda's element) so that every filter lands on the model it belongs to. Without this,
17
+ * an outer column referenced inside `tags.exists(t, ...)` would be emitted as a field of the
18
+ * tag model. Two sound transforms, applied bottom-up:
19
+ *
20
+ * - Conjunct hoisting (exists only): `exists(c, P(c) && Q)` with row-constant Q becomes
21
+ * `exists(c, P(c)) && Q`. Valid in three-valued logic (AND distributes over the per-element
22
+ * OR), including the empty-collection case (both sides are FALSE).
23
+ * - Case split (any collection op): a bare outer boolean reference Q in a boolean position
24
+ * becomes `(Q && op[Q:=true]) || (!Q && op[Q:=false]) || (Q && !Q && exists(c, true))`. The
25
+ * contradiction arm keeps the whole expression UNKNOWN (not FALSE) when Q is NULL-derived and
26
+ * the collection has elements, mirroring the guarded-ternary encoding.
27
+ */
28
+ export declare function hoistOuterScopeReferences(expr: PlanExpressionOperand, enclosingVariables: string[]): PlanExpressionOperand;
29
+ /**
30
+ * Bottom-up constant folding over the plan AST. Only shapes the case-split transform can
31
+ * produce need folding (constant ternary conditions, comparisons between two constants,
32
+ * logical operators with constant operands, collection macros with constant bodies); real
33
+ * planner output arrives pre-folded.
34
+ */
35
+ export declare function constantFoldExpression(expr: PlanExpressionOperand): PlanExpressionOperand;
36
+ //# sourceMappingURL=rewrite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rewrite.d.ts","sourceRoot":"","sources":["../src/rewrite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAkBjE,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,aAM/B,CAAC;AAKH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAU/E;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,KAAK,GACb,qBAAqB,CAsEvB;AA4HD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,qBAAqB,EAC3B,kBAAkB,EAAE,MAAM,EAAE,GAC3B,qBAAqB,CAoGvB;AAyBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,qBAAqB,GAC1B,qBAAqB,CA0HvB"}
package/lib/rewrite.js ADDED
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+ // Plan-to-plan rewrites that run before translation. None of them knows about Prisma: each takes
3
+ // an expression tree and returns an equivalent one that the translator can lower more directly.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.COLLECTION_OPERATORS = void 0;
6
+ exports.containsCollectionOperator = containsCollectionOperator;
7
+ exports.substituteLambdaVariable = substituteLambdaVariable;
8
+ exports.hoistOuterScopeReferences = hoistOuterScopeReferences;
9
+ exports.constantFoldExpression = constantFoldExpression;
10
+ const evaluate_1 = require("./evaluate");
11
+ const plan_1 = require("./plan");
12
+ const errors_1 = require("./errors");
13
+ const narrowing_1 = require("./narrowing");
14
+ /** The boolean collection macros: each takes a collection and a lambda. */
15
+ exports.COLLECTION_OPERATORS = new Set([
16
+ "exists",
17
+ "exists_one",
18
+ "all",
19
+ "except",
20
+ "filter",
21
+ ]);
22
+ // Operators whose second operand is a lambda that binds an iteration variable.
23
+ const LAMBDA_BINDING_OPERATORS = new Set([...exports.COLLECTION_OPERATORS, "map"]);
24
+ function containsCollectionOperator(expr) {
25
+ if (!(0, plan_1.isOperatorOperand)(expr)) {
26
+ return false;
27
+ }
28
+ if (exports.COLLECTION_OPERATORS.has(expr.operator)) {
29
+ return true;
30
+ }
31
+ return expr.operands.some(containsCollectionOperator);
32
+ }
33
+ /**
34
+ * Substitute a lambda iteration variable with a concrete collection element
35
+ * inside a lambda body. A bare reference to the variable becomes the element
36
+ * itself; a `variable.path.to.field` reference drills into the element. A
37
+ * nested collection macro whose lambda rebinds the same variable name shadows
38
+ * the outer variable, so substitution only descends into its collection
39
+ * operand.
40
+ */
41
+ function substituteLambdaVariable(operand, variableName, element) {
42
+ if ((0, plan_1.isNamedOperand)(operand)) {
43
+ if (operand.name === variableName) {
44
+ return { value: element };
45
+ }
46
+ if (operand.name.startsWith(`${variableName}.`)) {
47
+ let current = element;
48
+ for (const segment of operand.name
49
+ .slice(variableName.length + 1)
50
+ .split(".")) {
51
+ if (!(0, narrowing_1.isObject)(current) ||
52
+ Array.isArray(current) ||
53
+ !(segment in current) ||
54
+ current[segment] === undefined) {
55
+ throw new errors_1.UnsupportedQueryPlanError(`Cannot resolve "${operand.name}": collection element has no field "${segment}"`);
56
+ }
57
+ current = current[segment];
58
+ }
59
+ return { value: current };
60
+ }
61
+ return operand;
62
+ }
63
+ if (!(0, plan_1.isOperatorOperand)(operand)) {
64
+ return operand;
65
+ }
66
+ const [nestedCollection, nestedLambda] = operand.operands;
67
+ if (LAMBDA_BINDING_OPERATORS.has(operand.operator) &&
68
+ operand.operands.length === 2 &&
69
+ nestedCollection !== undefined &&
70
+ nestedLambda !== undefined &&
71
+ (0, plan_1.isOperatorOperand)(nestedLambda) &&
72
+ nestedLambda.operator === "lambda") {
73
+ const nestedVariable = nestedLambda.operands[1];
74
+ if (nestedVariable !== undefined &&
75
+ (0, plan_1.isNamedOperand)(nestedVariable) &&
76
+ nestedVariable.name === variableName) {
77
+ // The nested lambda shadows our variable: substitute only in the collection operand.
78
+ return {
79
+ operator: operand.operator,
80
+ operands: [
81
+ substituteLambdaVariable(nestedCollection, variableName, element),
82
+ nestedLambda,
83
+ ],
84
+ };
85
+ }
86
+ }
87
+ return {
88
+ operator: operand.operator,
89
+ operands: operand.operands.map((o) => substituteLambdaVariable(o, variableName, element)),
90
+ };
91
+ }
92
+ function mentionsVariable(expr, variableName) {
93
+ if ((0, plan_1.isNamedOperand)(expr)) {
94
+ return (expr.name === variableName || expr.name.startsWith(variableName + "."));
95
+ }
96
+ if ((0, plan_1.isOperatorOperand)(expr)) {
97
+ return expr.operands.some((operand) => mentionsVariable(operand, variableName));
98
+ }
99
+ return false;
100
+ }
101
+ function isOuterScopeName(name, enclosingVariables) {
102
+ return !enclosingVariables.some((variable) => name === variable || name.startsWith(variable + "."));
103
+ }
104
+ /** Replaces every occurrence of the bare named operand `name` with a boolean constant. */
105
+ function substituteNamedOperand(expr, name, value) {
106
+ if ((0, plan_1.isNamedOperand)(expr)) {
107
+ return expr.name === name ? { value } : expr;
108
+ }
109
+ if ((0, plan_1.isOperatorOperand)(expr)) {
110
+ return {
111
+ operator: expr.operator,
112
+ operands: expr.operands.map((operand) => substituteNamedOperand(operand, name, value)),
113
+ };
114
+ }
115
+ return expr;
116
+ }
117
+ const BOOLEAN_POSITION_OPERATORS = new Set(["and", "or", "not", "if"]);
118
+ /**
119
+ * Finds a bare boolean reference to an OUTER column (root or enclosing lambda scope) in a
120
+ * boolean position of a lambda body: a direct operand of and/or/not, a ternary condition, or
121
+ * the body itself.
122
+ */
123
+ function findOuterBooleanReference(expr, enclosingVariables) {
124
+ if ((0, plan_1.isNamedOperand)(expr)) {
125
+ return isOuterScopeName(expr.name, enclosingVariables)
126
+ ? expr.name
127
+ : undefined;
128
+ }
129
+ if (!(0, plan_1.isOperatorOperand)(expr)) {
130
+ return undefined;
131
+ }
132
+ const booleanPositions = BOOLEAN_POSITION_OPERATORS.has(expr.operator)
133
+ ? expr.operands
134
+ : // Comparisons wrapping a ternary keep the outer reference in the ternary condition.
135
+ expr.operands.filter((operand) => (0, plan_1.isOperatorOperand)(operand) &&
136
+ BOOLEAN_POSITION_OPERATORS.has(operand.operator));
137
+ for (const operand of booleanPositions) {
138
+ const found = findOuterBooleanReference(operand, enclosingVariables);
139
+ if (found !== undefined) {
140
+ return found;
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * The parts of `macro(collection, lambda(body, variable))` when `expr` is a boolean collection
147
+ * macro over a single-variable lambda, or undefined for any other shape.
148
+ */
149
+ function parseLambdaMacro(expr) {
150
+ const [collection, lambda] = expr.operands;
151
+ if (!exports.COLLECTION_OPERATORS.has(expr.operator) ||
152
+ expr.operands.length !== 2 ||
153
+ collection === undefined ||
154
+ lambda === undefined ||
155
+ !(0, plan_1.isOperatorOperand)(lambda) ||
156
+ lambda.operator !== "lambda" ||
157
+ lambda.operands.length !== 2) {
158
+ return undefined;
159
+ }
160
+ const variable = lambda.operands[1];
161
+ if (variable === undefined || !(0, plan_1.isNamedOperand)(variable)) {
162
+ return undefined;
163
+ }
164
+ return { collection, lambda, variable };
165
+ }
166
+ /**
167
+ * Rewrites lambda bodies that reference columns of an ENCLOSING scope (the root row, or an
168
+ * outer lambda's element) so that every filter lands on the model it belongs to. Without this,
169
+ * an outer column referenced inside `tags.exists(t, ...)` would be emitted as a field of the
170
+ * tag model. Two sound transforms, applied bottom-up:
171
+ *
172
+ * - Conjunct hoisting (exists only): `exists(c, P(c) && Q)` with row-constant Q becomes
173
+ * `exists(c, P(c)) && Q`. Valid in three-valued logic (AND distributes over the per-element
174
+ * OR), including the empty-collection case (both sides are FALSE).
175
+ * - Case split (any collection op): a bare outer boolean reference Q in a boolean position
176
+ * becomes `(Q && op[Q:=true]) || (!Q && op[Q:=false]) || (Q && !Q && exists(c, true))`. The
177
+ * contradiction arm keeps the whole expression UNKNOWN (not FALSE) when Q is NULL-derived and
178
+ * the collection has elements, mirroring the guarded-ternary encoding.
179
+ */
180
+ function hoistOuterScopeReferences(expr, enclosingVariables) {
181
+ if (!(0, plan_1.isOperatorOperand)(expr)) {
182
+ return expr;
183
+ }
184
+ const macro = parseLambdaMacro(expr);
185
+ if (macro === undefined) {
186
+ return {
187
+ operator: expr.operator,
188
+ operands: expr.operands.map((operand) => hoistOuterScopeReferences(operand, enclosingVariables)),
189
+ };
190
+ }
191
+ const { collection, lambda, variable } = macro;
192
+ const scopeVariables = [...enclosingVariables, variable.name];
193
+ const body = hoistOuterScopeReferences((0, plan_1.assertDefined)(lambda.operands[0], "Lambda requires a condition"), scopeVariables);
194
+ const rebuild = (newBody) => ({
195
+ operator: expr.operator,
196
+ operands: [collection, { operator: "lambda", operands: [newBody, variable] }],
197
+ });
198
+ // Conjunct hoisting out of exists().
199
+ if (expr.operator === "exists" &&
200
+ (0, plan_1.isOperatorOperand)(body) &&
201
+ body.operator === "and") {
202
+ const elementConjuncts = body.operands.filter((operand) => mentionsVariable(operand, variable.name));
203
+ const outerConjuncts = body.operands.filter((operand) => !mentionsVariable(operand, variable.name));
204
+ if (outerConjuncts.length > 0 && elementConjuncts.length > 0) {
205
+ const innerBody = elementConjuncts.length === 1
206
+ ? elementConjuncts[0]
207
+ : { operator: "and", operands: elementConjuncts };
208
+ return {
209
+ operator: "and",
210
+ operands: [rebuild(innerBody), ...outerConjuncts],
211
+ };
212
+ }
213
+ }
214
+ // Case split on a bare outer boolean reference.
215
+ const outerRef = findOuterBooleanReference(body, scopeVariables);
216
+ if (outerRef !== undefined) {
217
+ const q = { name: outerRef };
218
+ const notQ = { operator: "not", operands: [q] };
219
+ const arms = [
220
+ {
221
+ operator: "and",
222
+ operands: [q, rebuild(substituteNamedOperand(body, outerRef, true))],
223
+ },
224
+ {
225
+ operator: "and",
226
+ operands: [notQ, rebuild(substituteNamedOperand(body, outerRef, false))],
227
+ },
228
+ // Contradiction arm: UNKNOWN when the reference is NULL-derived, never TRUE. CEL never
229
+ // evaluates the body over an empty collection, so there the arm is FALSE, not UNKNOWN.
230
+ {
231
+ operator: "and",
232
+ operands: [
233
+ q,
234
+ notQ,
235
+ {
236
+ operator: "exists",
237
+ operands: [collection, { operator: "lambda", operands: [{ value: true }, variable] }],
238
+ },
239
+ ],
240
+ },
241
+ ];
242
+ // all() over an empty collection never evaluates its body, so CEL holds it TRUE even when
243
+ // the reference is missing; the arms above are all UNKNOWN on that row. `all(c, false)` is
244
+ // TRUE exactly when the collection is empty, and definite, so it restores that row without
245
+ // touching any other (#488).
246
+ if (expr.operator === "all") {
247
+ arms.push(rebuild({ value: false }));
248
+ }
249
+ return { operator: "or", operands: arms };
250
+ }
251
+ return rebuild(body);
252
+ }
253
+ /**
254
+ * `and`/`or` over already-folded operands: a dominating constant decides the whole expression,
255
+ * and the other constants drop out.
256
+ */
257
+ function foldLogical(operator, operands) {
258
+ const dominant = operator === "or";
259
+ if (operands.some((o) => (0, plan_1.isValueOperand)(o) && o.value === dominant)) {
260
+ return { value: dominant };
261
+ }
262
+ const remaining = operands.filter((o) => !(0, plan_1.isValueOperand)(o));
263
+ if (remaining.length === 0)
264
+ return { value: !dominant };
265
+ if (remaining.length === 1)
266
+ return remaining[0];
267
+ return { operator, operands: remaining };
268
+ }
269
+ /**
270
+ * Bottom-up constant folding over the plan AST. Only shapes the case-split transform can
271
+ * produce need folding (constant ternary conditions, comparisons between two constants,
272
+ * logical operators with constant operands, collection macros with constant bodies); real
273
+ * planner output arrives pre-folded.
274
+ */
275
+ function constantFoldExpression(expr) {
276
+ if (!(0, plan_1.isOperatorOperand)(expr)) {
277
+ return expr;
278
+ }
279
+ (0, plan_1.assertLogicalOperands)(expr.operator, expr.operands);
280
+ const operands = expr.operands.map(constantFoldExpression);
281
+ const folded = { operator: expr.operator, operands };
282
+ const [first, second] = operands;
283
+ const bothConstant = first !== undefined &&
284
+ second !== undefined &&
285
+ (0, plan_1.isValueOperand)(first) &&
286
+ (0, plan_1.isValueOperand)(second);
287
+ switch (expr.operator) {
288
+ case "if": {
289
+ const [condition, thenBranch, elseBranch] = operands;
290
+ if (condition !== undefined && (0, plan_1.isValueOperand)(condition)) {
291
+ if (!(0, narrowing_1.isBoolean)(condition.value)) {
292
+ throw new errors_1.UnsupportedQueryPlanError("if (ternary) condition must be a boolean expression");
293
+ }
294
+ return (0, plan_1.assertDefined)(condition.value ? thenBranch : elseBranch, "if (ternary) requires branch operands");
295
+ }
296
+ return folded;
297
+ }
298
+ case "and":
299
+ case "or":
300
+ return foldLogical(expr.operator, operands);
301
+ case "not":
302
+ if (first !== undefined && (0, plan_1.isValueOperand)(first)) {
303
+ return { value: first.value !== true };
304
+ }
305
+ return folded;
306
+ case "add":
307
+ case "sub":
308
+ case "mult":
309
+ case "div":
310
+ return bothConstant
311
+ ? { value: (0, evaluate_1.foldArithmetic)(expr.operator, first.value, second.value) }
312
+ : folded;
313
+ case "contains":
314
+ case "startsWith":
315
+ case "endsWith": {
316
+ if (bothConstant &&
317
+ (0, narrowing_1.isString)(first.value) &&
318
+ (0, narrowing_1.isString)(second.value)) {
319
+ const receiver = first.value;
320
+ const needle = second.value;
321
+ return {
322
+ value: expr.operator === "contains"
323
+ ? receiver.includes(needle)
324
+ : expr.operator === "startsWith"
325
+ ? receiver.startsWith(needle)
326
+ : receiver.endsWith(needle),
327
+ };
328
+ }
329
+ return folded;
330
+ }
331
+ case "exists":
332
+ case "all": {
333
+ const lambda = second;
334
+ if (first !== undefined &&
335
+ lambda !== undefined &&
336
+ (0, plan_1.isOperatorOperand)(lambda) &&
337
+ lambda.operator === "lambda" &&
338
+ lambda.operands[0] !== undefined &&
339
+ (0, plan_1.isValueOperand)(lambda.operands[0])) {
340
+ const bodyValue = lambda.operands[0].value === true;
341
+ if (expr.operator === "exists" && !bodyValue)
342
+ return { value: false };
343
+ if (expr.operator === "all" && bodyValue)
344
+ return { value: true };
345
+ const sizeExpr = {
346
+ operator: "size",
347
+ operands: [first],
348
+ };
349
+ // exists(c, true) is "collection is non-empty"; all(c, false) is "collection is empty".
350
+ return expr.operator === "exists"
351
+ ? { operator: "gt", operands: [sizeExpr, { value: 0 }] }
352
+ : { operator: "eq", operands: [sizeExpr, { value: 0 }] };
353
+ }
354
+ return folded;
355
+ }
356
+ default:
357
+ if (plan_1.COMPARISON_OPERATORS.has(expr.operator) &&
358
+ bothConstant) {
359
+ return {
360
+ value: (0, evaluate_1.evaluateConstantComparison)(expr.operator, first.value, second.value),
361
+ };
362
+ }
363
+ return folded;
364
+ }
365
+ }
366
+ //# sourceMappingURL=rewrite.js.map