@cerbos/orm-prisma 4.0.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 (203) hide show
  1. package/README.md +611 -460
  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 +91 -8
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +34 -2301
  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 +31 -18
  95. package/src/adversarial.test.ts +488 -488
  96. package/src/collections.ts +738 -0
  97. package/src/comparison.ts +1157 -0
  98. package/src/corpus.ts +597 -0
  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 +20 -0
  104. package/src/generated/prisma-adversarial/client.ts +20 -0
  105. package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
  106. package/src/generated/prisma-adversarial/internal/class.ts +44 -4
  107. package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +350 -3
  108. package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +49 -2
  109. package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
  110. package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
  111. package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
  112. package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
  113. package/src/generated/prisma-adversarial/models/AdversarialResource.ts +547 -75
  114. package/src/generated/prisma-adversarial/models.ts +4 -0
  115. package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
  116. package/src/generated/{prisma → prisma-adversarial-mysql}/client.ts +23 -18
  117. package/src/generated/{prisma → prisma-adversarial-mysql}/commonInputTypes.ts +167 -92
  118. package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
  119. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
  120. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
  121. package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
  122. package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
  123. package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
  124. package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
  125. package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
  126. package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
  127. package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
  128. package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
  129. package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
  130. package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
  131. package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
  132. package/src/generated/prisma-adversarial-pg/browser.ts +64 -0
  133. package/src/generated/prisma-adversarial-pg/client.ts +86 -0
  134. package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +345 -0
  135. package/src/generated/prisma-adversarial-pg/enums.ts +15 -0
  136. package/src/generated/prisma-adversarial-pg/internal/class.ts +284 -0
  137. package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +1542 -0
  138. package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +72 -43
  139. package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
  140. package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
  141. package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
  142. package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
  143. package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
  144. package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
  145. package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +2139 -0
  146. package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
  147. package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
  148. package/src/generated/prisma-adversarial-pg/models.ts +20 -0
  149. package/src/generated/prisma-adversarial-pg-v6/client.d.ts +1 -0
  150. package/src/generated/prisma-adversarial-pg-v6/default.d.ts +1 -0
  151. package/src/generated/prisma-adversarial-pg-v6/edge.d.ts +1 -0
  152. package/src/generated/prisma-adversarial-pg-v6/index.d.ts +14918 -0
  153. package/src/generated/prisma-adversarial-pg-v6/runtime/index-browser.d.ts +370 -0
  154. package/src/generated/prisma-adversarial-pg-v6/runtime/library.d.ts +3982 -0
  155. package/src/generated/prisma-adversarial-pg-v6/wasm.d.ts +1 -0
  156. package/src/generated/prisma-adversarial-v6/index.d.ts +6515 -304
  157. package/src/hierarchy.ts +405 -0
  158. package/src/index.ts +134 -3325
  159. package/src/literals.ts +363 -0
  160. package/src/mapping.ts +474 -0
  161. package/src/narrowing.ts +33 -0
  162. package/src/plan.ts +125 -0
  163. package/src/presence.ts +114 -0
  164. package/src/regex.ts +596 -0
  165. package/src/relations.ts +226 -0
  166. package/src/rewrite.ts +524 -0
  167. package/src/solve.ts +96 -0
  168. package/src/strings.ts +152 -0
  169. package/src/ternary.ts +359 -0
  170. package/src/test-setup.adversarial-url.ts +21 -0
  171. package/src/test-setup.adversarial.mysql.v6.ts +13 -0
  172. package/src/test-setup.adversarial.mysql.v7.ts +12 -0
  173. package/src/test-setup.adversarial.pg.v6.ts +13 -0
  174. package/src/test-setup.adversarial.pg.v7.ts +9 -0
  175. package/src/test-setup.adversarial.v7.ts +1 -0
  176. package/src/timestamp.ts +308 -0
  177. package/src/translate.ts +419 -0
  178. package/src/translator.test.ts +766 -0
  179. package/src/types.ts +1310 -0
  180. package/src/value.ts +16 -0
  181. package/src/generated/prisma/browser.ts +0 -59
  182. package/src/generated/prisma/internal/class.ts +0 -274
  183. package/src/generated/prisma/internal/prismaNamespace.ts +0 -1403
  184. package/src/generated/prisma/models/Category.ts +0 -1412
  185. package/src/generated/prisma/models/Label.ts +0 -1276
  186. package/src/generated/prisma/models/NestedResource.ts +0 -1562
  187. package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
  188. package/src/generated/prisma/models/Resource.ts +0 -2196
  189. package/src/generated/prisma/models/SubCategory.ts +0 -1412
  190. package/src/generated/prisma/models/Tag.ts +0 -1276
  191. package/src/generated/prisma/models/User.ts +0 -1503
  192. package/src/generated/prisma/models.ts +0 -19
  193. package/src/generated/prisma-v6/index.d.ts +0 -13726
  194. package/src/index.test.ts +0 -8885
  195. package/src/test-setup.v6.ts +0 -5
  196. package/src/test-setup.v7.ts +0 -7
  197. /package/src/generated/{prisma → prisma-adversarial-mysql}/enums.ts +0 -0
  198. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/client.d.ts +0 -0
  199. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/default.d.ts +0 -0
  200. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/edge.d.ts +0 -0
  201. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/index-browser.d.ts +0 -0
  202. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/library.d.ts +0 -0
  203. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/wasm.d.ts +0 -0
@@ -0,0 +1,548 @@
1
+ // Plan-to-plan rewrites of the CEL functions and constructors that have no Prisma filter of their
2
+ // own into the macros and comparisons the translator already lowers. Each rewrite is exact in CEL,
3
+ // errors included, so it holds under either polarity and needs no negation of its own.
4
+
5
+ import type { PlanExpressionOperand } from "@cerbos/core";
6
+
7
+ import { literalList, literalValue } from "./literals";
8
+ import { lookupElementFields, namesToOneRelation, resolveFieldReference } from "./mapping";
9
+ import type { TranslationContext } from "./mapping";
10
+ import { isNamedOperand, isOperatorOperand, isValueOperand } from "./plan";
11
+ import type { NamedOperand, OperatorOperand } from "./plan";
12
+ import { substituteLambdaVariable } from "./rewrite";
13
+ import { constantPresence } from "./presence";
14
+ import { presence } from "./types";
15
+ import { isString, pair, triple } from "./narrowing";
16
+
17
+ /** The iteration variable of a macro this module builds; no policy can spell the name. */
18
+ const ELEMENT: NamedOperand = { name: "__element" };
19
+
20
+ /** Rewrites, bottom-up, every shape the functions below recognise. */
21
+ export function rewriteFunctionCalls(
22
+ expr: PlanExpressionOperand,
23
+ context: TranslationContext
24
+ ): PlanExpressionOperand {
25
+ if (!isOperatorOperand(expr)) return expr;
26
+
27
+ const node: OperatorOperand = {
28
+ operator: expr.operator,
29
+ operands: expr.operands.map((operand) => rewriteFunctionCalls(operand, context)),
30
+ };
31
+
32
+ return (
33
+ constantPresence(node, context) ??
34
+ memberRead(node, context) ??
35
+ keyTest(node, context) ??
36
+ emptinessComparison(node) ??
37
+ subsetTest(node) ??
38
+ membershipOfDerivedList(node, context) ??
39
+ intersectionWithMappedList(node) ??
40
+ hoistTernaryFromConcatenation(node) ??
41
+ macroOverConstructedList(node, context) ??
42
+ node
43
+ );
44
+ }
45
+
46
+ const not = (operand: PlanExpressionOperand): OperatorOperand => ({
47
+ operator: "not",
48
+ operands: [operand],
49
+ });
50
+
51
+ function macro(
52
+ operator: "exists" | "all",
53
+ collection: PlanExpressionOperand,
54
+ body: PlanExpressionOperand,
55
+ variable: NamedOperand
56
+ ): OperatorOperand {
57
+ return {
58
+ operator,
59
+ operands: [collection, { operator: "lambda", operands: [body, variable] }],
60
+ };
61
+ }
62
+
63
+ /**
64
+ * `x["key"]` is `x.key`: CEL reads a map member the same way through either spelling, and both
65
+ * raise the same error on a missing key. The dotted name then resolves through the mapping as if
66
+ * the policy had written it. A key holding a dot would be read as two segments, so it is left
67
+ * alone, and so is an index into a list-valued mapping, where a string index is an error.
68
+ */
69
+ function memberRead(
70
+ node: OperatorOperand,
71
+ context: TranslationContext
72
+ ): PlanExpressionOperand | undefined {
73
+ if (node.operator !== "index" || node.operands.length !== 2) return undefined;
74
+ const [subject, key] = pair(node.operands);
75
+
76
+ if (
77
+ !isNamedOperand(subject) ||
78
+ !isValueOperand(key) ||
79
+ !isString(key.value) ||
80
+ key.value === "" ||
81
+ key.value.includes(".")
82
+ ) {
83
+ return undefined;
84
+ }
85
+
86
+ const { relations } = resolveFieldReference(subject.name, context);
87
+
88
+ if (relations?.some((relation) => relation.type === "many")) return undefined;
89
+
90
+ return { name: `${subject.name}.${key.value}` };
91
+ }
92
+
93
+ /**
94
+ * `"key" in m` and `m.exists(k, k == "key")` (and `k in ["k1", "k2"]`), with `m` a to-one relation:
95
+ * CEL reads the related row as a map, and both test whether it carries the key, which is
96
+ * `has(m.key)` — the row exists and sends that attribute. Only a key the mapper declares under the
97
+ * relation's `fields` can be tested, since presence follows the declared column's null convention;
98
+ * any other key, and any other body, is left for the translator to refuse.
99
+ */
100
+ function keyTest(
101
+ node: OperatorOperand,
102
+ context: TranslationContext
103
+ ): PlanExpressionOperand | undefined {
104
+ let map: NamedOperand;
105
+ let keys: string[];
106
+
107
+ if (node.operator === "in" && node.operands.length === 2) {
108
+ const [key, container] = pair(node.operands);
109
+
110
+ if (!isNamedOperand(container) || !isValueOperand(key) || !isString(key.value)) {
111
+ return undefined;
112
+ }
113
+
114
+ [map, keys] = [container, [key.value]];
115
+ } else if (node.operator === "exists" && node.operands.length === 2) {
116
+ const [container, lambda] = pair(node.operands);
117
+
118
+ if (
119
+ !isNamedOperand(container) ||
120
+ !isOperatorOperand(lambda) ||
121
+ lambda.operator !== "lambda" ||
122
+ lambda.operands.length !== 2
123
+ ) {
124
+ return undefined;
125
+ }
126
+
127
+ const [body, variable] = pair(lambda.operands);
128
+
129
+ if (!isNamedOperand(variable)) return undefined;
130
+ const tested = keysTested(body, variable.name);
131
+
132
+ if (tested === undefined) return undefined;
133
+ [map, keys] = [container, tested];
134
+ } else {
135
+ return undefined;
136
+ }
137
+
138
+ if (!namesToOneRelation(context.mapper, map.name)) return undefined;
139
+ const declared = lookupElementFields(context.mapper, map.name);
140
+
141
+ if (declared === undefined || !keys.every((key) => key in declared)) return undefined;
142
+
143
+ const tests: PlanExpressionOperand[] = keys.map((key) => ({
144
+ operator: "has",
145
+ operands: [{ name: `${map.name}.${key}` }],
146
+ }));
147
+
148
+ return tests.length === 1 ? tests[0]! : { operator: "or", operands: tests };
149
+ }
150
+
151
+ /** The key literals `body` tests the bare variable against, when it is such a test. */
152
+ function keysTested(body: PlanExpressionOperand, variable: string): string[] | undefined {
153
+ if (!isOperatorOperand(body) || body.operands.length !== 2) return undefined;
154
+ const [first, second] = pair(body.operands);
155
+
156
+ const isVariable = (operand: PlanExpressionOperand): boolean =>
157
+ isNamedOperand(operand) && operand.name === variable;
158
+
159
+ if (body.operator === "eq") {
160
+ const literal = isVariable(first) ? second : isVariable(second) ? first : undefined;
161
+
162
+ return literal !== undefined && isValueOperand(literal) && isString(literal.value)
163
+ ? [literal.value]
164
+ : undefined;
165
+ }
166
+
167
+ if (body.operator === "in" && isVariable(first)) {
168
+ const values = literalList(second);
169
+
170
+ return values?.every(isString) ? values : undefined;
171
+ }
172
+
173
+ return undefined;
174
+ }
175
+
176
+ /**
177
+ * `c.except(L) == []` and `intersect(c, L) == []` (and `!=`), with `L` a literal list. The
178
+ * difference is empty exactly when every element of `c` is in `L`, and the intersection exactly
179
+ * when none is. Cerbos keeps an element by `L`'s membership test, which never raises an error, so
180
+ * the macros below decide every row as the list functions do.
181
+ */
182
+ function emptinessComparison(node: OperatorOperand): PlanExpressionOperand | undefined {
183
+ if ((node.operator !== "eq" && node.operator !== "ne") || node.operands.length !== 2) {
184
+ return undefined;
185
+ }
186
+
187
+ const [first, second] = pair(node.operands);
188
+ const [call, other] = isListFunction(first) ? [first, second] : [second, first];
189
+
190
+ if (!isListFunction(call) || literalList(other)?.length !== 0) return undefined;
191
+
192
+ let [collection, literals] = pair(call.operands);
193
+
194
+ // intersect() is symmetric in which elements it shares, so emptiness is too.
195
+ if (call.operator === "intersect" && !isNamedOperand(collection)) {
196
+ [collection, literals] = [literals, collection];
197
+ }
198
+
199
+ const values = literalList(literals);
200
+
201
+ if (!isNamedOperand(collection) || values === undefined) return undefined;
202
+
203
+ const member: PlanExpressionOperand = { operator: "in", operands: [ELEMENT, { value: values }] };
204
+ const isEmpty = node.operator === "eq";
205
+
206
+ if (call.operator === "except") {
207
+ return isEmpty
208
+ ? macro("all", collection, member, ELEMENT)
209
+ : macro("exists", collection, not(member), ELEMENT);
210
+ }
211
+
212
+ const shared = macro("exists", collection, member, ELEMENT);
213
+
214
+ return isEmpty ? not(shared) : shared;
215
+ }
216
+
217
+ function isListFunction(operand: PlanExpressionOperand): operand is OperatorOperand {
218
+ return (
219
+ isOperatorOperand(operand) &&
220
+ (operand.operator === "except" || operand.operator === "intersect") &&
221
+ operand.operands.length === 2
222
+ );
223
+ }
224
+
225
+ /** `a.isSubset(b)` holds exactly when every element of `a` is in `b`. */
226
+ function subsetTest(node: OperatorOperand): PlanExpressionOperand | undefined {
227
+ if (node.operator !== "isSubset" || node.operands.length !== 2) return undefined;
228
+ const [subset, superset] = pair(node.operands);
229
+ const subsetValues = literalList(subset);
230
+ const supersetValues = literalList(superset);
231
+
232
+ if (isNamedOperand(subset) && supersetValues !== undefined) {
233
+ return macro(
234
+ "all",
235
+ subset,
236
+ { operator: "in", operands: [ELEMENT, { value: supersetValues }] },
237
+ ELEMENT
238
+ );
239
+ }
240
+
241
+ if (subsetValues !== undefined && isNamedOperand(superset)) {
242
+ return macro(
243
+ "all",
244
+ { value: subsetValues },
245
+ { operator: "in", operands: [ELEMENT, superset] },
246
+ ELEMENT
247
+ );
248
+ }
249
+
250
+ return undefined;
251
+ }
252
+
253
+ /**
254
+ * `x in c.map(v, F)` and `x in c.filter(v, P)`, with `x` a literal. The derived list holds `x`
255
+ * exactly when some element maps to `x` (or is `x` and passes `P`), but building it raises an
256
+ * error as soon as ONE element's `F` (or `P`) does, which no witness absorbs. The second macro
257
+ * carries that error: `F == x || !(F == x)` is true for every element whose `F` evaluates, and an
258
+ * error otherwise, so `all` over it holds exactly when the list can be built.
259
+ */
260
+ function membershipOfDerivedList(
261
+ node: OperatorOperand,
262
+ context: TranslationContext
263
+ ): PlanExpressionOperand | undefined {
264
+ if (node.operator !== "in" || node.operands.length !== 2) return undefined;
265
+ const [member, list] = pair(node.operands);
266
+
267
+ if (
268
+ !isValueOperand(member) ||
269
+ !isOperatorOperand(list) ||
270
+ (list.operator !== "map" && list.operator !== "filter") ||
271
+ list.operands.length !== 2
272
+ ) {
273
+ return undefined;
274
+ }
275
+
276
+ const [collection, lambda] = pair(list.operands);
277
+
278
+ if (
279
+ !isNamedOperand(collection) ||
280
+ !isOperatorOperand(lambda) ||
281
+ lambda.operator !== "lambda" ||
282
+ lambda.operands.length !== 2
283
+ ) {
284
+ return undefined;
285
+ }
286
+
287
+ const [body, variable] = pair(lambda.operands);
288
+
289
+ if (!isNamedOperand(variable)) return undefined;
290
+
291
+ const evaluates = (predicate: PlanExpressionOperand): OperatorOperand => ({
292
+ operator: "or",
293
+ operands: [predicate, not(predicate)],
294
+ });
295
+
296
+ if (list.operator === "map") {
297
+ const matches: PlanExpressionOperand = { operator: "eq", operands: [body, member] };
298
+
299
+ return {
300
+ operator: "and",
301
+ operands: [
302
+ macro("exists", collection, matches, variable),
303
+ macro("all", collection, evaluates(matches), variable),
304
+ ],
305
+ };
306
+ }
307
+
308
+ // filter() keeps the element itself, which only a projected scalar list can compare with a
309
+ // literal; the element of a list of related rows is an object.
310
+ const { relations } = resolveFieldReference(collection.name, context);
311
+
312
+ if (relations?.[relations.length - 1]?.field === undefined) return undefined;
313
+
314
+ return {
315
+ operator: "and",
316
+ operands: [
317
+ macro(
318
+ "exists",
319
+ collection,
320
+ { operator: "and", operands: [{ operator: "eq", operands: [variable, member] }, body] },
321
+ variable
322
+ ),
323
+ macro("all", collection, evaluates(body), variable),
324
+ ],
325
+ };
326
+ }
327
+
328
+ /**
329
+ * `hasIntersection(c.map(v, F), L)` (either order), with `L` a literal list: some element maps
330
+ * into `L`, and, as for membership above, building the mapped list raises an error as soon as one
331
+ * element's `F` does. Spelled as `exists` plus an `all` that carries that error, the negation
332
+ * pushes down through both macros, so a row holding an element whose `F` errors stays denied
333
+ * under `!` instead of the guard's own negation selecting it.
334
+ */
335
+ function intersectionWithMappedList(node: OperatorOperand): PlanExpressionOperand | undefined {
336
+ if (node.operator !== "hasIntersection" || node.operands.length !== 2) return undefined;
337
+ const [first, second] = pair(node.operands);
338
+
339
+ const [mapped, literals] =
340
+ isOperatorOperand(first) && first.operator === "map" ? [first, second] : [second, first];
341
+
342
+ const values = literalList(literals);
343
+
344
+ if (
345
+ values === undefined ||
346
+ !isOperatorOperand(mapped) ||
347
+ mapped.operator !== "map" ||
348
+ mapped.operands.length !== 2
349
+ ) {
350
+ return undefined;
351
+ }
352
+
353
+ const [collection, lambda] = pair(mapped.operands);
354
+
355
+ if (
356
+ !isNamedOperand(collection) ||
357
+ !isOperatorOperand(lambda) ||
358
+ lambda.operator !== "lambda" ||
359
+ lambda.operands.length !== 2
360
+ ) {
361
+ return undefined;
362
+ }
363
+
364
+ const [projection, variable] = pair(lambda.operands);
365
+
366
+ if (!isNamedOperand(variable)) return undefined;
367
+
368
+ const shared: PlanExpressionOperand = {
369
+ operator: "in",
370
+ operands: [projection, { value: values }],
371
+ };
372
+
373
+ return {
374
+ operator: "and",
375
+ operands: [
376
+ macro("exists", collection, shared, variable),
377
+ macro("all", collection, { operator: "or", operands: [shared, not(shared)] }, variable),
378
+ ],
379
+ };
380
+ }
381
+
382
+ /**
383
+ * `L1 + (c ? A : B)` (and the mirror), with every list a literal, is `c ? L1 + A : L1 + B`: the
384
+ * ternary evaluates only the branch it selects, and concatenating literals raises no error, so
385
+ * the condition's error is the only one either spelling can raise. The constant folder then
386
+ * concatenates each branch, leaving a ternary over two literal lists.
387
+ */
388
+ function hoistTernaryFromConcatenation(
389
+ node: OperatorOperand
390
+ ): PlanExpressionOperand | undefined {
391
+ if (node.operator !== "add" || node.operands.length !== 2) return undefined;
392
+
393
+ const index = node.operands.findIndex(
394
+ (operand) => isOperatorOperand(operand) && operand.operator === "if"
395
+ );
396
+
397
+ if (index === -1) return undefined;
398
+ const ternary = node.operands[index];
399
+
400
+ if (ternary === undefined || !isOperatorOperand(ternary)) return undefined;
401
+ const other = node.operands[1 - index]!;
402
+
403
+ if (ternary.operands.length !== 3) return undefined;
404
+
405
+ const [condition, thenBranch, elseBranch] = triple(ternary.operands);
406
+
407
+ const otherList = constantList(other);
408
+ const thenList = constantList(thenBranch);
409
+ const elseList = constantList(elseBranch);
410
+
411
+ if (otherList === undefined || thenList === undefined || elseList === undefined) return undefined;
412
+
413
+ const place = (branch: PlanExpressionOperand): OperatorOperand => ({
414
+ operator: "add",
415
+ operands: index === 0 ? [branch, otherList] : [otherList, branch],
416
+ });
417
+
418
+ return { operator: "if", operands: [condition, place(thenList), place(elseList)] };
419
+ }
420
+
421
+ /** A literal list as a value operand, or a concatenation of literal lists left for the folder. */
422
+ function constantList(operand: PlanExpressionOperand): PlanExpressionOperand | undefined {
423
+ const literal = literalList(operand);
424
+
425
+ if (literal !== undefined) return { value: literal };
426
+
427
+ if (
428
+ isOperatorOperand(operand) &&
429
+ operand.operator === "add" &&
430
+ operand.operands.length === 2 &&
431
+ operand.operands.every((part) => constantList(part) !== undefined)
432
+ ) {
433
+ return { operator: "add", operands: operand.operands.map((part) => constantList(part)!) };
434
+ }
435
+
436
+ return undefined;
437
+ }
438
+
439
+ /**
440
+ * `[a, b].exists(v, P)` and `.all(v, P)` over a list BUILT from attributes: the OR (AND) of `P`
441
+ * per element, as CEL's macros are Kleene's disjunction (conjunction) over the elements. Building
442
+ * the list raises an error when an element is a missing attribute, even where another element
443
+ * would decide the macro, so each such element's presence test is ANDed on, and ORed into the
444
+ * body negated: FALSE for a present value and UNKNOWN for a NULL one, so a missing element leaves
445
+ * the whole rewrite UNKNOWN under both polarities. Only elements that are literals or plain
446
+ * columns are rewritten.
447
+ */
448
+ function macroOverConstructedList(
449
+ node: OperatorOperand,
450
+ context: TranslationContext
451
+ ): PlanExpressionOperand | undefined {
452
+ if ((node.operator !== "exists" && node.operator !== "all") || node.operands.length !== 2) {
453
+ return undefined;
454
+ }
455
+
456
+ const [list, lambda] = pair(node.operands);
457
+
458
+ if (!isOperatorOperand(list) || list.operator !== "list" || literalList(list) !== undefined) {
459
+ return undefined;
460
+ }
461
+
462
+ if (!isOperatorOperand(lambda) || lambda.operator !== "lambda" || lambda.operands.length !== 2) {
463
+ return undefined;
464
+ }
465
+
466
+ const [body, variable] = pair(lambda.operands);
467
+
468
+ if (!isNamedOperand(variable)) return undefined;
469
+
470
+ const presences: PlanExpressionOperand[] = [];
471
+
472
+ for (const element of list.operands) {
473
+ if (literalValue(element) !== undefined) continue;
474
+
475
+ if (!isNamedOperand(element)) return undefined;
476
+ const fieldRef = resolveFieldReference(element.name, context);
477
+
478
+ if ((fieldRef.relations?.length ?? 0) > 0) return undefined;
479
+ // An explicit null is a value, which builds the list like any other.
480
+ const convention = fieldRef.nullAttributeRepresentation ?? context.nullRepresentation;
481
+
482
+ if (convention === "explicit") continue;
483
+ const test = presence(element, fieldRef);
484
+
485
+ if (test === undefined) return undefined;
486
+ presences.push(test);
487
+ }
488
+
489
+ const bodies: PlanExpressionOperand[] = [];
490
+
491
+ for (const element of list.operands) {
492
+ const substituted = substituteElement(body, variable.name, element);
493
+
494
+ if (substituted === undefined) return undefined;
495
+ bodies.push(substituted);
496
+ }
497
+
498
+ const core: PlanExpressionOperand =
499
+ bodies.length === 1
500
+ ? bodies[0]!
501
+ : { operator: node.operator === "exists" ? "or" : "and", operands: bodies };
502
+
503
+ if (presences.length === 0) return core;
504
+
505
+ return {
506
+ operator: "and",
507
+ operands: [...presences, { operator: "or", operands: [core, ...presences.map(not)] }],
508
+ };
509
+ }
510
+
511
+ /**
512
+ * `expr` with the lambda variable `name` replaced by `element`, a literal or a column; undefined
513
+ * when a nested lambda rebinds the name.
514
+ */
515
+ function substituteElement(
516
+ expr: PlanExpressionOperand,
517
+ name: string,
518
+ element: PlanExpressionOperand
519
+ ): PlanExpressionOperand | undefined {
520
+ if (isNamedOperand(expr)) {
521
+ if (expr.name !== name && !expr.name.startsWith(`${name}.`)) return expr;
522
+
523
+ if (isNamedOperand(element)) return { name: element.name + expr.name.slice(name.length) };
524
+ const value = literalValue(element);
525
+
526
+ return value === undefined ? undefined : substituteLambdaVariable(expr, name, value);
527
+ }
528
+
529
+ if (!isOperatorOperand(expr)) return expr;
530
+
531
+ if (
532
+ expr.operator === "lambda" &&
533
+ expr.operands.slice(1).some((bound) => isNamedOperand(bound) && bound.name === name)
534
+ ) {
535
+ return undefined;
536
+ }
537
+
538
+ const operands: PlanExpressionOperand[] = [];
539
+
540
+ for (const operand of expr.operands) {
541
+ const substituted = substituteElement(operand, name, element);
542
+
543
+ if (substituted === undefined) return undefined;
544
+ operands.push(substituted);
545
+ }
546
+
547
+ return { operator: expr.operator, operands };
548
+ }
@@ -42,3 +42,23 @@ export type AdversarialSubCategory = Prisma.AdversarialSubCategoryModel
42
42
  *
43
43
  */
44
44
  export type AdversarialLabel = Prisma.AdversarialLabelModel
45
+ /**
46
+ * Model AdversarialParent
47
+ *
48
+ */
49
+ export type AdversarialParent = Prisma.AdversarialParentModel
50
+ /**
51
+ * Model AdversarialInner
52
+ *
53
+ */
54
+ export type AdversarialInner = Prisma.AdversarialInnerModel
55
+ /**
56
+ * Model AdversarialNumberListElement
57
+ *
58
+ */
59
+ export type AdversarialNumberListElement = Prisma.AdversarialNumberListElementModel
60
+ /**
61
+ * Model AdversarialBoolListElement
62
+ *
63
+ */
64
+ export type AdversarialBoolListElement = Prisma.AdversarialBoolListElementModel
@@ -64,3 +64,23 @@ export type AdversarialSubCategory = Prisma.AdversarialSubCategoryModel
64
64
  *
65
65
  */
66
66
  export type AdversarialLabel = Prisma.AdversarialLabelModel
67
+ /**
68
+ * Model AdversarialParent
69
+ *
70
+ */
71
+ export type AdversarialParent = Prisma.AdversarialParentModel
72
+ /**
73
+ * Model AdversarialInner
74
+ *
75
+ */
76
+ export type AdversarialInner = Prisma.AdversarialInnerModel
77
+ /**
78
+ * Model AdversarialNumberListElement
79
+ *
80
+ */
81
+ export type AdversarialNumberListElement = Prisma.AdversarialNumberListElementModel
82
+ /**
83
+ * Model AdversarialBoolListElement
84
+ *
85
+ */
86
+ export type AdversarialBoolListElement = Prisma.AdversarialBoolListElementModel