@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.
- package/README.md +598 -603
- package/lib/collections.d.ts +30 -0
- package/lib/collections.d.ts.map +1 -0
- package/lib/collections.js +421 -0
- package/lib/collections.js.map +1 -0
- package/lib/comparison.d.ts +24 -0
- package/lib/comparison.d.ts.map +1 -0
- package/lib/comparison.js +731 -0
- package/lib/comparison.js.map +1 -0
- package/lib/errors.d.ts +13 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +17 -0
- package/lib/errors.js.map +1 -0
- package/lib/evaluate.d.ts +5 -0
- package/lib/evaluate.d.ts.map +1 -0
- package/lib/evaluate.js +118 -0
- package/lib/evaluate.js.map +1 -0
- package/lib/fields.d.ts +34 -0
- package/lib/fields.d.ts.map +1 -0
- package/lib/fields.js +253 -0
- package/lib/fields.js.map +1 -0
- package/lib/functions.d.ts +5 -0
- package/lib/functions.d.ts.map +1 -0
- package/lib/functions.js +408 -0
- package/lib/functions.js.map +1 -0
- package/lib/hierarchy.d.ts +6 -0
- package/lib/hierarchy.d.ts.map +1 -0
- package/lib/hierarchy.js +266 -0
- package/lib/hierarchy.js.map +1 -0
- package/lib/index.d.ts +23 -13
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +34 -2555
- package/lib/index.js.map +1 -1
- package/lib/literals.d.ts +20 -0
- package/lib/literals.d.ts.map +1 -0
- package/lib/literals.js +290 -0
- package/lib/literals.js.map +1 -0
- package/lib/mapping.d.ts +95 -0
- package/lib/mapping.d.ts.map +1 -0
- package/lib/mapping.js +316 -0
- package/lib/mapping.js.map +1 -0
- package/lib/narrowing.d.ts +12 -0
- package/lib/narrowing.d.ts.map +1 -0
- package/lib/narrowing.js +32 -0
- package/lib/narrowing.js.map +1 -0
- package/lib/plan.d.ts +49 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +86 -0
- package/lib/plan.js.map +1 -0
- package/lib/presence.d.ts +25 -0
- package/lib/presence.d.ts.map +1 -0
- package/lib/presence.js +82 -0
- package/lib/presence.js.map +1 -0
- package/lib/regex.d.ts +33 -0
- package/lib/regex.d.ts.map +1 -0
- package/lib/regex.js +480 -0
- package/lib/regex.js.map +1 -0
- package/lib/relations.d.ts +58 -0
- package/lib/relations.d.ts.map +1 -0
- package/lib/relations.js +175 -0
- package/lib/relations.js.map +1 -0
- package/lib/rewrite.d.ts +36 -0
- package/lib/rewrite.d.ts.map +1 -0
- package/lib/rewrite.js +366 -0
- package/lib/rewrite.js.map +1 -0
- package/lib/solve.d.ts +20 -0
- package/lib/solve.d.ts.map +1 -0
- package/lib/solve.js +77 -0
- package/lib/solve.js.map +1 -0
- package/lib/strings.d.ts +12 -0
- package/lib/strings.d.ts.map +1 -0
- package/lib/strings.js +110 -0
- package/lib/strings.js.map +1 -0
- package/lib/ternary.d.ts +12 -0
- package/lib/ternary.d.ts.map +1 -0
- package/lib/ternary.js +192 -0
- package/lib/ternary.js.map +1 -0
- package/lib/timestamp.d.ts +45 -0
- package/lib/timestamp.d.ts.map +1 -0
- package/lib/timestamp.js +225 -0
- package/lib/timestamp.js.map +1 -0
- package/lib/translate.d.ts +20 -0
- package/lib/translate.d.ts.map +1 -0
- package/lib/translate.js +287 -0
- package/lib/translate.js.map +1 -0
- package/lib/types.d.ts +29 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +980 -0
- package/lib/types.js.map +1 -0
- package/lib/value.d.ts +8 -0
- package/lib/value.d.ts.map +1 -0
- package/lib/value.js +7 -0
- package/lib/value.js.map +1 -0
- package/package.json +27 -17
- package/src/adversarial.test.ts +376 -1071
- package/src/collections.ts +738 -0
- package/src/comparison.ts +1157 -0
- package/src/corpus.ts +433 -250
- package/src/errors.ts +12 -0
- package/src/evaluate.ts +170 -0
- package/src/fields.ts +373 -0
- package/src/functions.ts +548 -0
- package/src/generated/prisma-adversarial/browser.ts +10 -0
- package/src/generated/prisma-adversarial/client.ts +10 -0
- package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
- package/src/generated/prisma-adversarial/internal/class.ts +24 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +174 -3
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +23 -2
- package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +54 -54
- package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +66 -66
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +435 -87
- package/src/generated/prisma-adversarial/models.ts +2 -0
- package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
- package/src/generated/prisma-adversarial-mysql/client.ts +86 -0
- package/src/generated/prisma-adversarial-mysql/commonInputTypes.ts +349 -0
- package/src/generated/prisma-adversarial-mysql/enums.ts +15 -0
- package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
- package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
- package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
- package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
- package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
- package/src/generated/prisma-adversarial-mysql-v6/client.d.ts +1 -0
- package/src/generated/prisma-adversarial-mysql-v6/default.d.ts +1 -0
- package/src/generated/prisma-adversarial-mysql-v6/edge.d.ts +1 -0
- package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
- package/src/generated/prisma-adversarial-mysql-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-adversarial-mysql-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-adversarial-mysql-v6/wasm.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg/browser.ts +10 -0
- package/src/generated/prisma-adversarial-pg/client.ts +10 -0
- package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +118 -129
- package/src/generated/prisma-adversarial-pg/internal/class.ts +24 -4
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +174 -3
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespaceBrowser.ts +23 -2
- package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +54 -54
- package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +66 -66
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +435 -87
- package/src/generated/prisma-adversarial-pg/models.ts +2 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +3476 -388
- package/src/generated/prisma-adversarial-v6/index.d.ts +3465 -383
- package/src/hierarchy.ts +405 -0
- package/src/index.ts +66 -3646
- package/src/literals.ts +363 -0
- package/src/mapping.ts +474 -0
- package/src/narrowing.ts +33 -0
- package/src/plan.ts +125 -0
- package/src/presence.ts +114 -0
- package/src/regex.ts +596 -0
- package/src/relations.ts +226 -0
- package/src/rewrite.ts +524 -0
- package/src/solve.ts +96 -0
- package/src/strings.ts +152 -0
- package/src/ternary.ts +359 -0
- package/src/test-setup.adversarial-url.ts +6 -3
- package/src/test-setup.adversarial.mysql.v6.ts +13 -0
- package/src/test-setup.adversarial.mysql.v7.ts +12 -0
- package/src/test-setup.adversarial.v7.ts +1 -0
- package/src/timestamp.ts +308 -0
- package/src/translate.ts +419 -0
- package/src/translator.test.ts +497 -1317
- package/src/types.ts +1310 -0
- package/src/value.ts +16 -0
package/src/functions.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -52,3 +52,13 @@ export type AdversarialParent = Prisma.AdversarialParentModel
|
|
|
52
52
|
*
|
|
53
53
|
*/
|
|
54
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
|
|
@@ -74,3 +74,13 @@ export type AdversarialParent = Prisma.AdversarialParentModel
|
|
|
74
74
|
*
|
|
75
75
|
*/
|
|
76
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
|