@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,363 @@
1
+ // A plan-to-plan rewrite for collection macros over a LITERAL list — typically a folded principal
2
+ // attribute, which the planner leaves unrolled above ten elements. Each macro becomes the
3
+ // comparisons it performs, so no relation is involved and nothing has to be counted in the store.
4
+
5
+ import type { PlanExpressionOperand, Value } from "@cerbos/core";
6
+
7
+ import { isNamedOperand, isOperatorOperand, isValueOperand } from "./plan";
8
+ import type { NamedOperand, OperatorOperand } from "./plan";
9
+ import { substituteLambdaVariable } from "./rewrite";
10
+ import { isNumber, isObject, isString, lookup, pair } from "./narrowing";
11
+
12
+ /** Past this many elements, `exists`/`all` are left to the translator's own fold. */
13
+ const MAX_UNROLLED_ELEMENTS = 100;
14
+
15
+ /**
16
+ * Rewrites, bottom-up:
17
+ *
18
+ * - `exists`/`all` over a literal list into the OR/AND of the body per element. CEL's macros are
19
+ * Kleene's disjunction and conjunction over the elements, so this is exact under any polarity.
20
+ * - `exists_one`, `size(filter(...))` and `size(except(list, [x]))` over a literal list whose
21
+ * body is `t == x` or `t != x`, with `x` not mentioning `t`, into membership of `x`. Every
22
+ * element then compares against the same `x`: all of them error together when `x` is missing,
23
+ * and otherwise the count of matches is the multiplicity of `x` in the list, so the macro holds
24
+ * exactly for the values whose multiplicity satisfies it.
25
+ * - `size(except(collection, literals))` against an emptiness threshold into `exists`/`all` over
26
+ * the collection, which the translator lowers as a relation.
27
+ */
28
+ export function expandLiteralCollections(expr: PlanExpressionOperand): PlanExpressionOperand {
29
+ if (!isOperatorOperand(expr)) return expr;
30
+
31
+ const node: OperatorOperand = {
32
+ operator: expr.operator,
33
+ operands: expr.operands.map(expandLiteralCollections),
34
+ };
35
+
36
+ return expandMacro(node) ?? expandSizeComparison(node) ?? node;
37
+ }
38
+
39
+ function expandMacro(expr: OperatorOperand): PlanExpressionOperand | undefined {
40
+ const { operator } = expr;
41
+
42
+ if (operator !== "exists" && operator !== "all" && operator !== "exists_one") return undefined;
43
+ const parts = literalMacro(expr);
44
+
45
+ if (parts === undefined) return undefined;
46
+ const { elements, body, variable } = parts;
47
+
48
+ if (operator === "exists_one") {
49
+ return countMembership(elements, body, variable, (count) => count === 1);
50
+ }
51
+
52
+ if (elements.length > MAX_UNROLLED_ELEMENTS) return undefined;
53
+
54
+ if (elements.length === 0) return { value: operator === "all" };
55
+
56
+ const substituted = elements.map((element) =>
57
+ substituteLambdaVariable(body, variable.name, element)
58
+ );
59
+
60
+ if (substituted.length === 1) return substituted[0]!;
61
+
62
+ return { operator: operator === "exists" ? "or" : "and", operands: substituted };
63
+ }
64
+
65
+ /** `size(filter(...)) CMP n`, `size(except(...)) CMP n`. */
66
+ function expandSizeComparison(expr: OperatorOperand): PlanExpressionOperand | undefined {
67
+ const comparison = sizeComparison(expr);
68
+
69
+ if (comparison === undefined) return undefined;
70
+ const { counted, holds } = comparison;
71
+
72
+ if (counted.operator === "filter") {
73
+ const parts = literalMacro(counted);
74
+
75
+ return parts && countMembership(parts.elements, parts.body, parts.variable, holds);
76
+ }
77
+
78
+ if (counted.operator !== "except" || counted.operands.length !== 2) return undefined;
79
+ const [minuend, subtrahend] = pair(counted.operands);
80
+
81
+ if (isOperatorOperand(subtrahend) && subtrahend.operator === "lambda") return undefined;
82
+
83
+ // except(literals, [x]) keeps every element that is not x.
84
+ const elements = literalList(minuend);
85
+
86
+ if (elements !== undefined) {
87
+ const removed = isOperatorOperand(subtrahend) && subtrahend.operator === "list"
88
+ ? subtrahend.operands
89
+ : undefined;
90
+
91
+ if (removed?.length !== 1 || literalValue(removed[0]!) !== undefined) return undefined;
92
+ const variable: NamedOperand = { name: "__except" };
93
+ const body: PlanExpressionOperand = { operator: "ne", operands: [variable, removed[0]!] };
94
+
95
+ return countMembership(elements, body, variable, holds);
96
+ }
97
+
98
+ // except(collection, literals) is non-empty exactly when some element is not among them.
99
+ if (isNamedOperand(minuend) && literalList(subtrahend) !== undefined) {
100
+ const nonEmpty = holds(1) && !holds(0);
101
+ const empty = holds(0) && !holds(1);
102
+
103
+ if (!nonEmpty && !empty) return undefined;
104
+
105
+ // Only counts 0 and 1 are distinguished, so every count >= 1 must agree with 1.
106
+ if ([2, 3].some((count) => holds(count) !== holds(1))) return undefined;
107
+ const variable: NamedOperand = { name: "__except" };
108
+ const member: PlanExpressionOperand = { operator: "in", operands: [variable, subtrahend] };
109
+
110
+ return {
111
+ operator: nonEmpty ? "exists" : "all",
112
+ operands: [
113
+ minuend,
114
+ {
115
+ operator: "lambda",
116
+ operands: [nonEmpty ? { operator: "not", operands: [member] } : member, variable],
117
+ },
118
+ ],
119
+ };
120
+ }
121
+
122
+ return undefined;
123
+ }
124
+
125
+ /**
126
+ * The membership test a count over `t == x` (or `t != x`) reduces to, or undefined when the body
127
+ * has another shape.
128
+ */
129
+ function countMembership(
130
+ elements: Value[],
131
+ body: PlanExpressionOperand,
132
+ variable: NamedOperand,
133
+ holds: (count: number) => boolean
134
+ ): PlanExpressionOperand | undefined {
135
+ // Over no elements `x` is never evaluated, so its error cannot surface.
136
+ if (elements.length === 0) return undefined;
137
+ const comparison = uniformComparison(body, variable);
138
+
139
+ if (comparison === undefined) return undefined;
140
+ const { operator, path, other } = comparison;
141
+
142
+ const projected: Value[] = [];
143
+
144
+ for (const element of elements) {
145
+ const value = project(element, path);
146
+
147
+ // An element without the field errors on its own; a non-scalar compares structurally.
148
+ if (value === undefined || (isObject(value))) return undefined;
149
+ projected.push(value);
150
+ }
151
+
152
+ const distinct = [...new Set(projected)];
153
+ const multiplicity = (value: Value) => projected.filter((v) => v === value).length;
154
+
155
+ const countFor = (matches: number) =>
156
+ operator === "eq" ? matches : projected.length - matches;
157
+
158
+ const accepted = distinct.filter((value) => holds(countFor(multiplicity(value))));
159
+ const absentHolds = holds(countFor(0));
160
+
161
+ // Decided for every x: only the error of a missing x is left, which an empty IN cannot carry
162
+ // under negation.
163
+ if (absentHolds ? accepted.length === distinct.length : accepted.length === 0) {
164
+ return undefined;
165
+ }
166
+
167
+ const membership = (values: Value[]): PlanExpressionOperand =>
168
+ values.length === 1
169
+ ? { operator: "eq", operands: [other, { value: values[0]! }] }
170
+ : { operator: "in", operands: [other, { value: values }] };
171
+
172
+ if (!absentHolds) return membership(accepted);
173
+
174
+ return {
175
+ operator: "not",
176
+ operands: [membership(distinct.filter((value) => !accepted.includes(value)))],
177
+ };
178
+ }
179
+
180
+ /**
181
+ * `t == x`, `t.path != x` and their mirrors, where `x` does not mention `t`: every element is
182
+ * compared against the same `x`, and equality never raises an error of its own.
183
+ */
184
+ function uniformComparison(
185
+ body: PlanExpressionOperand,
186
+ variable: NamedOperand
187
+ ):
188
+ | { operator: "eq" | "ne"; path: string[]; other: PlanExpressionOperand }
189
+ | undefined {
190
+ if (!isOperatorOperand(body) || (body.operator !== "eq" && body.operator !== "ne")) {
191
+ return undefined;
192
+ }
193
+
194
+ if (body.operands.length !== 2) return undefined;
195
+
196
+ for (const [element, other] of [
197
+ [body.operands[0]!, body.operands[1]!],
198
+ [body.operands[1]!, body.operands[0]!],
199
+ ] as const) {
200
+ if (!isNamedOperand(element) || mentions(other, variable.name)) continue;
201
+
202
+ if (element.name === variable.name) return { operator: body.operator, path: [], other };
203
+
204
+ if (element.name.startsWith(`${variable.name}.`)) {
205
+ return {
206
+ operator: body.operator,
207
+ path: element.name.slice(variable.name.length + 1).split("."),
208
+ other,
209
+ };
210
+ }
211
+ }
212
+
213
+ return undefined;
214
+ }
215
+
216
+ function mentions(expr: PlanExpressionOperand, name: string): boolean {
217
+ if (isNamedOperand(expr)) return expr.name === name || expr.name.startsWith(`${name}.`);
218
+
219
+ return isOperatorOperand(expr) && expr.operands.some((operand) => mentions(operand, name));
220
+ }
221
+
222
+ function project(element: Value, path: string[]): Value | undefined {
223
+ let current: Value | undefined = element;
224
+
225
+ for (const segment of path) {
226
+ if (!isObject(current) || Array.isArray(current)) {
227
+ return undefined;
228
+ }
229
+
230
+ current = current[segment];
231
+ }
232
+
233
+ return current;
234
+ }
235
+
236
+ // -- reading the plan ----------------------------------------------------------------------------
237
+
238
+ /** `size(counted) CMP n`, with `holds` deciding the comparison for a count. */
239
+ function sizeComparison(
240
+ expr: OperatorOperand
241
+ ): { counted: OperatorOperand; holds: (count: number) => boolean } | undefined {
242
+ const mirrored = { lt: "gt", gt: "lt", le: "ge", ge: "le" } satisfies Record<string, string>;
243
+
244
+ if (!["eq", "ne", "lt", "le", "gt", "ge"].includes(expr.operator)) return undefined;
245
+
246
+ if (expr.operands.length !== 2) return undefined;
247
+ const [first, second] = pair(expr.operands);
248
+
249
+ const [size, threshold, operator] = isValueOperand(first)
250
+ ? [second, first, lookup(mirrored, expr.operator) ?? expr.operator]
251
+ : [first, second, expr.operator];
252
+
253
+ if (
254
+ !isOperatorOperand(size) ||
255
+ size.operator !== "size" ||
256
+ size.operands.length !== 1 ||
257
+ !isValueOperand(threshold) ||
258
+ !isNumber(threshold.value) ||
259
+ !Number.isFinite(threshold.value)
260
+ ) {
261
+ return undefined;
262
+ }
263
+
264
+ const counted = size.operands[0]!;
265
+
266
+ if (!isOperatorOperand(counted)) return undefined;
267
+ const n = threshold.value;
268
+
269
+ const holds = (count: number): boolean => {
270
+ switch (operator) {
271
+ case "eq":
272
+ return count === n;
273
+ case "ne":
274
+ return count !== n;
275
+ case "lt":
276
+ return count < n;
277
+ case "le":
278
+ return count <= n;
279
+ case "gt":
280
+ return count > n;
281
+ default:
282
+ return count >= n;
283
+ }
284
+ };
285
+
286
+ return { counted, holds };
287
+ }
288
+
289
+ /** The elements, lambda body and variable of `macro(literals, lambda(body, variable))`. */
290
+ function literalMacro(
291
+ expr: OperatorOperand
292
+ ): { elements: Value[]; body: PlanExpressionOperand; variable: NamedOperand } | undefined {
293
+ const [collection, lambda] = expr.operands;
294
+
295
+ if (
296
+ expr.operands.length !== 2 ||
297
+ collection === undefined ||
298
+ lambda === undefined ||
299
+ !isOperatorOperand(lambda) ||
300
+ lambda.operator !== "lambda" ||
301
+ lambda.operands.length !== 2
302
+ ) {
303
+ return undefined;
304
+ }
305
+
306
+ const elements = literalList(collection);
307
+ const [body, variable] = pair(lambda.operands);
308
+
309
+ if (elements === undefined || !isNamedOperand(variable)) return undefined;
310
+
311
+ return { elements, body, variable };
312
+ }
313
+
314
+ /** A list literal: a value list, or a `list(...)` of literals. */
315
+ export function literalList(operand: PlanExpressionOperand): Value[] | undefined {
316
+ if (isValueOperand(operand)) {
317
+ return Array.isArray(operand.value) ? operand.value : undefined;
318
+ }
319
+
320
+ if (isOperatorOperand(operand) && operand.operator === "list") {
321
+ const elements: Value[] = [];
322
+
323
+ for (const element of operand.operands) {
324
+ const value = literalValue(element);
325
+
326
+ if (value === undefined) return undefined;
327
+ elements.push(value);
328
+ }
329
+
330
+ return elements;
331
+ }
332
+
333
+ return undefined;
334
+ }
335
+
336
+ /** A literal operand's value: a value, or a `struct(set-field(...), ...)` of literals. */
337
+ export function literalValue(operand: PlanExpressionOperand): Value | undefined {
338
+ if (isValueOperand(operand)) return operand.value;
339
+
340
+ if (!isOperatorOperand(operand)) return undefined;
341
+
342
+ if (operand.operator === "list") return literalList(operand);
343
+
344
+ if (operand.operator !== "struct") return undefined;
345
+ const fields: Record<string, Value> = {};
346
+
347
+ for (const field of operand.operands) {
348
+ if (!isOperatorOperand(field) || field.operator !== "set-field" || field.operands.length !== 2) {
349
+ return undefined;
350
+ }
351
+
352
+ const [key, value] = pair(field.operands);
353
+ const literal = literalValue(value);
354
+
355
+ if (!isValueOperand(key) || !isString(key.value) || literal === undefined) {
356
+ return undefined;
357
+ }
358
+
359
+ fields[key.value] = literal;
360
+ }
361
+
362
+ return fields;
363
+ }