@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
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Comparisons (`eq`, `ne`, `lt`, `le`, `gt`, `ge`) and membership (`in`): against a constant,
|
|
3
|
+
// between two columns, over size(), and over linear arithmetic solved for the column.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.handleRelationalOperator = handleRelationalOperator;
|
|
6
|
+
exports.containsMapLiteralLookup = containsMapLiteralLookup;
|
|
7
|
+
exports.tryHandleMapLiteralLookup = tryHandleMapLiteralLookup;
|
|
8
|
+
exports.handleInOperator = handleInOperator;
|
|
9
|
+
const evaluate_1 = require("./evaluate");
|
|
10
|
+
const fields_1 = require("./fields");
|
|
11
|
+
const literals_1 = require("./literals");
|
|
12
|
+
const mapping_1 = require("./mapping");
|
|
13
|
+
const plan_1 = require("./plan");
|
|
14
|
+
const relations_1 = require("./relations");
|
|
15
|
+
const solve_1 = require("./solve");
|
|
16
|
+
const ternary_1 = require("./ternary");
|
|
17
|
+
const timestamp_1 = require("./timestamp");
|
|
18
|
+
const translate_1 = require("./translate");
|
|
19
|
+
const types_1 = require("./types");
|
|
20
|
+
const errors_1 = require("./errors");
|
|
21
|
+
const narrowing_1 = require("./narrowing");
|
|
22
|
+
const value_1 = require("./value");
|
|
23
|
+
/**
|
|
24
|
+
* Helper function to process relational operators (eq, ne, lt, etc.)
|
|
25
|
+
*/
|
|
26
|
+
function handleRelationalOperator(operator, operands, context) {
|
|
27
|
+
const ternaryFilter = (0, ternary_1.tryHandleTernaryComparison)(operator, operands, context);
|
|
28
|
+
if (ternaryFilter) {
|
|
29
|
+
return ternaryFilter;
|
|
30
|
+
}
|
|
31
|
+
const lookupFilter = tryHandleMapLiteralLookup(operator, operands, context) ??
|
|
32
|
+
tryHandleUpperAsciiComparison(operator, operands, context);
|
|
33
|
+
if (lookupFilter) {
|
|
34
|
+
return lookupFilter;
|
|
35
|
+
}
|
|
36
|
+
({ operator, operands } = (0, plan_1.normalizeBinaryOperands)(operator, operands));
|
|
37
|
+
if (!(0, narrowing_1.lookup)(plan_1.CERBOS_TO_PRISMA_OPERATOR, operator)) {
|
|
38
|
+
throw new errors_1.UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
39
|
+
}
|
|
40
|
+
const leftOperand = operands.find((o) => (0, plan_1.isNamedOperand)(o) || (0, plan_1.isOperatorOperand)(o));
|
|
41
|
+
if (!leftOperand)
|
|
42
|
+
throw new errors_1.UnsupportedQueryPlanError("No valid left operand found");
|
|
43
|
+
const rightOperand = operands.find((o) => o !== leftOperand);
|
|
44
|
+
if (!rightOperand)
|
|
45
|
+
throw new errors_1.UnsupportedQueryPlanError("No valid right operand found");
|
|
46
|
+
if ((0, plan_1.isOperatorOperand)(leftOperand) && leftOperand.operator === "size") {
|
|
47
|
+
return handleSizeComparison(operator, leftOperand, rightOperand, context);
|
|
48
|
+
}
|
|
49
|
+
const arithOperand = [leftOperand, rightOperand].find((o) => (0, plan_1.isOperatorOperand)(o) && evaluate_1.ARITHMETIC_OPERATORS.has(o.operator));
|
|
50
|
+
if (arithOperand) {
|
|
51
|
+
const arithIsLeft = arithOperand === leftOperand;
|
|
52
|
+
return handleArithmeticComparison(operator, arithOperand, arithIsLeft ? rightOperand : leftOperand, arithIsLeft, context);
|
|
53
|
+
}
|
|
54
|
+
if ([leftOperand, rightOperand].some((o) => (0, plan_1.isOperatorOperand)(o) && o.operator === "map")) {
|
|
55
|
+
throw new errors_1.UnsupportedQueryPlanError(`Direct comparison of map(...) to a value is not supported (operator: ${operator}). ` +
|
|
56
|
+
`Wrap the map() expression in hasIntersection(map(...), [...]) instead.`);
|
|
57
|
+
}
|
|
58
|
+
assertNoRawTemporalAgainstTimestamp(leftOperand, rightOperand, context);
|
|
59
|
+
// Only a comparison between a column and one constant can absorb a sub-millisecond instant.
|
|
60
|
+
const oneColumn = isColumnOperand(leftOperand) !== isColumnOperand(rightOperand);
|
|
61
|
+
const left = (0, translate_1.resolveOperand)(leftOperand, context, oneColumn);
|
|
62
|
+
const right = (0, translate_1.resolveOperand)(rightOperand, context, oneColumn);
|
|
63
|
+
if ((0, mapping_1.isResolvedValue)(left)) {
|
|
64
|
+
if ((0, mapping_1.isResolvedFieldReference)(right)) {
|
|
65
|
+
return buildValueComparisonFilter(context, right, (0, plan_1.mirrorOperator)(operator), left);
|
|
66
|
+
}
|
|
67
|
+
return (0, evaluate_1.evaluateConstantComparison)(operator, left.value, right.value)
|
|
68
|
+
? {}
|
|
69
|
+
: (0, fields_1.rejectConstantFalse)();
|
|
70
|
+
}
|
|
71
|
+
if ((0, mapping_1.isResolvedFieldReference)(right)) {
|
|
72
|
+
if (left.valueType === "dateTime" &&
|
|
73
|
+
right.valueType === "dateTime" &&
|
|
74
|
+
(!left.timestampWrapped || !right.timestampWrapped)) {
|
|
75
|
+
throw new errors_1.UnsupportedQueryPlanError("Raw temporal column comparison loses RFC-3339 string spelling; wrap both operands in timestamp()");
|
|
76
|
+
}
|
|
77
|
+
if (left.valueType !== undefined &&
|
|
78
|
+
right.valueType !== undefined &&
|
|
79
|
+
left.valueType !== right.valueType) {
|
|
80
|
+
throw new errors_1.UnsupportedQueryPlanError("Cannot compare fields with different mapped value types");
|
|
81
|
+
}
|
|
82
|
+
return buildFieldToFieldFilter(context, operator, leftOperand, left, rightOperand, right);
|
|
83
|
+
}
|
|
84
|
+
return buildValueComparisonFilter(context, left, operator, right);
|
|
85
|
+
}
|
|
86
|
+
/** `{"k": v, ...}[x]`: an index into a map literal. */
|
|
87
|
+
function isMapLiteralLookup(operand) {
|
|
88
|
+
if (!(0, plan_1.isOperatorOperand)(operand) || operand.operator !== "index" || operand.operands.length !== 2) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const [map] = operand.operands;
|
|
92
|
+
return (0, plan_1.isOperatorOperand)(map) && map.operator === "struct";
|
|
93
|
+
}
|
|
94
|
+
/** Whether a map-literal lookup appears anywhere in `operand`. */
|
|
95
|
+
function containsMapLiteralLookup(operand) {
|
|
96
|
+
if (!(0, plan_1.isOperatorOperand)(operand))
|
|
97
|
+
return false;
|
|
98
|
+
return operand.operands.some(containsMapLiteralLookup) || isMapLiteralLookup(operand);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `{"k1": v1, "k2": v2}[x] == v` (and `!=`, either side first), with `x` a string column: the keys
|
|
102
|
+
* whose value equals `v` are exactly the values of `x` that make it true, and every other key the
|
|
103
|
+
* values that make it false. Any `x` that is not a key is an error, true under neither polarity,
|
|
104
|
+
* so the comparison and its negation are two membership tests, never one test and its NOT:
|
|
105
|
+
* `negated` asks for the negation's filter, and buildNegatedFilter pushes every `not` above a
|
|
106
|
+
* lookup down to it. Inside a lambda body a macro negates its body with a plain NOT, so the
|
|
107
|
+
* lookup is refused there.
|
|
108
|
+
*/
|
|
109
|
+
function tryHandleMapLiteralLookup(operator, operands, context, negated = false) {
|
|
110
|
+
if ((operator !== "eq" && operator !== "ne") || operands.length !== 2)
|
|
111
|
+
return null;
|
|
112
|
+
const lookupIndex = operands.findIndex(isMapLiteralLookup);
|
|
113
|
+
if (lookupIndex === -1)
|
|
114
|
+
return null;
|
|
115
|
+
const lookup = operands[lookupIndex];
|
|
116
|
+
if (lookup === undefined || !(0, plan_1.isOperatorOperand)(lookup))
|
|
117
|
+
return null;
|
|
118
|
+
const other = operands[1 - lookupIndex];
|
|
119
|
+
const [mapOperand, key] = (0, narrowing_1.pair)(lookup.operands);
|
|
120
|
+
const map = (0, literals_1.literalValue)(mapOperand);
|
|
121
|
+
if (map === undefined ||
|
|
122
|
+
!(0, narrowing_1.isObject)(map) ||
|
|
123
|
+
Array.isArray(map) ||
|
|
124
|
+
!(0, plan_1.isValueOperand)(other) ||
|
|
125
|
+
!(0, plan_1.isNamedOperand)(key)) {
|
|
126
|
+
throw new errors_1.UnsupportedQueryPlanError("An index into a map literal is translated only as a literal map indexed by a column " +
|
|
127
|
+
"and compared with a literal: a Prisma filter has no map lookup");
|
|
128
|
+
}
|
|
129
|
+
if (context.scopes.length > 0) {
|
|
130
|
+
throw new errors_1.UnsupportedQueryPlanError("An index into a map literal inside a lambda body is not supported: a missing key is an " +
|
|
131
|
+
"error under both polarities, and the macro negates its body with a plain NOT");
|
|
132
|
+
}
|
|
133
|
+
const fieldRef = (0, mapping_1.resolveFieldReference)(key.name, context);
|
|
134
|
+
if (fieldRef.valueType !== "string") {
|
|
135
|
+
throw new errors_1.UnsupportedQueryPlanError(`An index into a map literal requires a string key column, and ${key.name} is not mapped ` +
|
|
136
|
+
'with valueType: "string"');
|
|
137
|
+
}
|
|
138
|
+
const keys = Object.keys(map);
|
|
139
|
+
const matching = keys.filter((k) => (0, evaluate_1.evaluateConstantComparison)("eq", map[k], other.value));
|
|
140
|
+
const holds = (operator === "eq") !== negated;
|
|
141
|
+
return (0, fields_1.buildFieldFilter)(fieldRef, "in", holds ? matching : keys.filter((k) => !matching.includes(k)));
|
|
142
|
+
}
|
|
143
|
+
/** Past this many spellings, `upperAscii(x) == "LIT"` is refused rather than enumerated. */
|
|
144
|
+
const MAX_UPPER_ASCII_SPELLINGS = 1024;
|
|
145
|
+
/**
|
|
146
|
+
* `x.upperAscii() == "LIT"` (and `!=`, either side first), with `x` a string column: upperAscii
|
|
147
|
+
* maps exactly the ASCII letters a-z to A-Z and leaves every other code point alone, so the
|
|
148
|
+
* strings it maps to "LIT" are its spellings with each ASCII capital in either case — none at all
|
|
149
|
+
* when the literal holds a lowercase ASCII letter. An IN over those spellings compares the bytes
|
|
150
|
+
* exactly, where a store's UPPER would also fold non-ASCII letters. A NULL column stays UNKNOWN,
|
|
151
|
+
* as upperAscii of a missing (or null) value is an error.
|
|
152
|
+
*/
|
|
153
|
+
function tryHandleUpperAsciiComparison(operator, operands, context) {
|
|
154
|
+
if ((operator !== "eq" && operator !== "ne") || operands.length !== 2)
|
|
155
|
+
return null;
|
|
156
|
+
const callIndex = operands.findIndex((operand) => (0, plan_1.isOperatorOperand)(operand) && operand.operator === "upperAscii");
|
|
157
|
+
if (callIndex === -1)
|
|
158
|
+
return null;
|
|
159
|
+
const call = operands[callIndex];
|
|
160
|
+
if (call === undefined || !(0, plan_1.isOperatorOperand)(call))
|
|
161
|
+
return null;
|
|
162
|
+
const other = operands[1 - callIndex];
|
|
163
|
+
const [subject] = call.operands;
|
|
164
|
+
if (call.operands.length !== 1 ||
|
|
165
|
+
subject === undefined ||
|
|
166
|
+
!(0, plan_1.isNamedOperand)(subject) ||
|
|
167
|
+
!(0, plan_1.isValueOperand)(other) ||
|
|
168
|
+
!(0, narrowing_1.isString)(other.value)) {
|
|
169
|
+
throw new errors_1.UnsupportedQueryPlanError("upperAscii() is translated only as a column compared with a string literal: a Prisma " +
|
|
170
|
+
"filter has no ASCII-only case mapping");
|
|
171
|
+
}
|
|
172
|
+
const fieldRef = (0, mapping_1.resolveFieldReference)(subject.name, context);
|
|
173
|
+
(0, fields_1.assertStringField)(fieldRef, "upperAscii");
|
|
174
|
+
(0, mapping_1.recordErroringNullElement)(context, subject, fieldRef);
|
|
175
|
+
let spellings = [""];
|
|
176
|
+
for (const character of other.value) {
|
|
177
|
+
if (character >= "a" && character <= "z") {
|
|
178
|
+
spellings = [];
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
const cases = character >= "A" && character <= "Z" ? [character, character.toLowerCase()] : [character];
|
|
182
|
+
spellings = spellings.flatMap((prefix) => cases.map((c) => prefix + c));
|
|
183
|
+
if (spellings.length > MAX_UPPER_ASCII_SPELLINGS) {
|
|
184
|
+
throw new errors_1.UnsupportedQueryPlanError(`Cannot translate upperAscii() == ${JSON.stringify(other.value)}: its spellings exceed ` +
|
|
185
|
+
`the ${MAX_UPPER_ASCII_SPELLINGS}-entry limit`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const filter = (0, fields_1.buildFieldFilter)(fieldRef, "in", spellings);
|
|
189
|
+
return operator === "eq" ? filter : { NOT: filter };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Refuses a bare `dateTime` column compared with a `timestamp(...)` operand. The attribute the
|
|
193
|
+
* application sends for that column is its RFC 3339 STRING (a Cerbos attribute has no timestamp
|
|
194
|
+
* type), and CEL has no overload ordering a string against a timestamp: `check()` raises an error,
|
|
195
|
+
* which decides the row under neither polarity the way any column filter would. Wrapping the
|
|
196
|
+
* attribute in timestamp() is what the policy means.
|
|
197
|
+
*/
|
|
198
|
+
function assertNoRawTemporalAgainstTimestamp(first, second, context) {
|
|
199
|
+
for (const [column, other] of [
|
|
200
|
+
[first, second],
|
|
201
|
+
[second, first],
|
|
202
|
+
]) {
|
|
203
|
+
if ((0, plan_1.isNamedOperand)(column) &&
|
|
204
|
+
(0, plan_1.isOperatorOperand)(other) &&
|
|
205
|
+
other.operator === "timestamp" &&
|
|
206
|
+
(0, mapping_1.resolveFieldReference)(column.name, context).valueType === "dateTime") {
|
|
207
|
+
throw new errors_1.UnsupportedQueryPlanError(`Cannot compare ${column.name} with a timestamp: the attribute holds an RFC 3339 ` +
|
|
208
|
+
"string, and CEL has no overload comparing a string with a timestamp, so check() " +
|
|
209
|
+
"raises an error that no column filter reproduces under both polarities; wrap the " +
|
|
210
|
+
"attribute in timestamp()");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** A column reference, bare or wrapped in timestamp(). */
|
|
215
|
+
function isColumnOperand(operand) {
|
|
216
|
+
if ((0, plan_1.isNamedOperand)(operand))
|
|
217
|
+
return true;
|
|
218
|
+
return ((0, plan_1.isOperatorOperand)(operand) &&
|
|
219
|
+
operand.operator === "timestamp" &&
|
|
220
|
+
operand.operands.length === 1 &&
|
|
221
|
+
(0, plan_1.isNamedOperand)(operand.operands[0]));
|
|
222
|
+
}
|
|
223
|
+
/** buildComparisonFilter, for a resolved constant that may be a sub-millisecond timestamp. */
|
|
224
|
+
function buildValueComparisonFilter(context, fieldRef, operator, constant) {
|
|
225
|
+
if (!constant.subMillisecond) {
|
|
226
|
+
return (0, fields_1.buildComparisonFilter)(context, fieldRef, operator, constant.value);
|
|
227
|
+
}
|
|
228
|
+
const rounded = (0, timestamp_1.roundSubMillisecond)(operator);
|
|
229
|
+
if (rounded !== null) {
|
|
230
|
+
return (0, fields_1.buildComparisonFilter)(context, fieldRef, rounded, constant.value);
|
|
231
|
+
}
|
|
232
|
+
// No whole millisecond equals the instant. Spelled as a contradiction on the column, so a NULL
|
|
233
|
+
// stays UNKNOWN under both polarities — timestamp() of a missing attribute is an error.
|
|
234
|
+
const never = {
|
|
235
|
+
AND: [
|
|
236
|
+
(0, fields_1.buildComparisonFilter)(context, fieldRef, "lt", constant.value),
|
|
237
|
+
(0, fields_1.buildComparisonFilter)(context, fieldRef, "ge", constant.value),
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
return operator === "eq" ? never : { NOT: never };
|
|
241
|
+
}
|
|
242
|
+
function countPredicate(operator, n) {
|
|
243
|
+
const atLeast = (count, negated = false) => ({
|
|
244
|
+
kind: "atLeast",
|
|
245
|
+
count: Math.max(count, 0),
|
|
246
|
+
negated,
|
|
247
|
+
});
|
|
248
|
+
switch (operator) {
|
|
249
|
+
case "gt":
|
|
250
|
+
return atLeast(Math.floor(n) + 1);
|
|
251
|
+
case "ge":
|
|
252
|
+
return atLeast(Math.ceil(n));
|
|
253
|
+
case "lt":
|
|
254
|
+
return atLeast(Math.ceil(n), true);
|
|
255
|
+
case "le":
|
|
256
|
+
return atLeast(Math.floor(n) + 1, true);
|
|
257
|
+
case "eq":
|
|
258
|
+
case "ne": {
|
|
259
|
+
const negated = operator === "ne";
|
|
260
|
+
// A fractional size never equals anything; a negative one never exists.
|
|
261
|
+
return Number.isInteger(n) && n >= 0
|
|
262
|
+
? { kind: "exactly", count: n, negated }
|
|
263
|
+
: atLeast(0, !negated);
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
throw new errors_1.UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* No store holds a string of 2^32 characters: PostgreSQL caps a value at 1 GB, SQLite at
|
|
271
|
+
* 2^31 - 1 bytes and MySQL's LONGTEXT at 2^32 - 1 bytes. A length threshold at or past it is
|
|
272
|
+
* decided without a pattern.
|
|
273
|
+
*/
|
|
274
|
+
const STRING_LENGTH_CEILING = 2 ** 32;
|
|
275
|
+
/** Past this, a length threshold short of the ceiling is refused rather than spelled out. */
|
|
276
|
+
const MAX_LENGTH_PATTERN = 1024;
|
|
277
|
+
/**
|
|
278
|
+
* `size(column) CMP n` over a string column, as `LIKE` patterns of `_`: Prisma does not escape
|
|
279
|
+
* the wildcards in a `startsWith` needle, so `startsWith: "_____"` is `LIKE '_____%'`, true exactly
|
|
280
|
+
* when the value holds at least five characters. Each store's `_` matches one character — a code
|
|
281
|
+
* point in a UTF-8 database — which is what CEL's size() counts. `startsWith: ""` is `LIKE '%'`,
|
|
282
|
+
* true for every non-NULL value. Every filter built here is therefore UNKNOWN on a NULL column
|
|
283
|
+
* under both polarities, as size() of a missing attribute is an error in CEL.
|
|
284
|
+
*/
|
|
285
|
+
function buildStringLengthFilter(fieldRef, predicate) {
|
|
286
|
+
const atLeast = (count) => {
|
|
287
|
+
if (count >= STRING_LENGTH_CEILING) {
|
|
288
|
+
return { NOT: (0, fields_1.buildFieldFilter)(fieldRef, "startsWith", "") };
|
|
289
|
+
}
|
|
290
|
+
if (count > MAX_LENGTH_PATTERN) {
|
|
291
|
+
throw new errors_1.UnsupportedQueryPlanError(`Cannot translate a string length threshold of ${count}: the LIKE pattern it needs ` +
|
|
292
|
+
`exceeds the ${MAX_LENGTH_PATTERN}-character limit`);
|
|
293
|
+
}
|
|
294
|
+
return (0, fields_1.buildFieldFilter)(fieldRef, "startsWith", "_".repeat(count));
|
|
295
|
+
};
|
|
296
|
+
const filter = predicate.kind === "atLeast"
|
|
297
|
+
? atLeast(predicate.count)
|
|
298
|
+
: { AND: [atLeast(predicate.count), { NOT: atLeast(predicate.count + 1) }] };
|
|
299
|
+
return predicate.negated ? { NOT: filter } : filter;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* `size(collection) CMP n`. Over a relation, only the thresholds that mean "is empty", "is
|
|
303
|
+
* non-empty" or "the chain reaching it exists" — the only counts a relation filter can express.
|
|
304
|
+
* Over a string column, any threshold (see buildStringLengthFilter).
|
|
305
|
+
*/
|
|
306
|
+
function handleSizeComparison(operator, sizeOperand, valueOperand, context) {
|
|
307
|
+
const collectionOperand = sizeOperand.operands[0];
|
|
308
|
+
if (collectionOperand && (0, plan_1.isOperatorOperand)(collectionOperand)) {
|
|
309
|
+
throw new errors_1.UnsupportedQueryPlanError(`size() of ${collectionOperand.operator}(...) counts the elements of a derived list, and a ` +
|
|
310
|
+
"Prisma relation filter can only test whether some, every or no related row matches, " +
|
|
311
|
+
"never count them");
|
|
312
|
+
}
|
|
313
|
+
if (!collectionOperand || !(0, plan_1.isNamedOperand)(collectionOperand)) {
|
|
314
|
+
throw new errors_1.UnsupportedQueryPlanError("size operator requires a named collection operand");
|
|
315
|
+
}
|
|
316
|
+
if (!(0, plan_1.isValueOperand)(valueOperand)) {
|
|
317
|
+
throw new errors_1.UnsupportedQueryPlanError("size comparison requires a numeric value operand");
|
|
318
|
+
}
|
|
319
|
+
const count = valueOperand.value;
|
|
320
|
+
if (!(0, narrowing_1.isNumber)(count) || !Number.isFinite(count)) {
|
|
321
|
+
throw new errors_1.UnsupportedQueryPlanError("size comparison requires a numeric value");
|
|
322
|
+
}
|
|
323
|
+
const predicate = countPredicate(operator, count);
|
|
324
|
+
const fieldRef = (0, mapping_1.resolveFieldReference)(collectionOperand.name, context);
|
|
325
|
+
const { relations } = fieldRef;
|
|
326
|
+
// A string column reached through to-one hops is measured on the hop's row, never counted as
|
|
327
|
+
// one: `size(parent.aString) >= 1` read as "has a parent" returned a parent whose aString is
|
|
328
|
+
// empty. The length's own negation sits inside the hops, so a missing hop is unmatched under
|
|
329
|
+
// both polarities, the missing-path error CEL denies.
|
|
330
|
+
if (fieldRef.valueType === "string") {
|
|
331
|
+
return (0, relations_1.wrapInRelations)(relations, buildStringLengthFilter({ ...fieldRef, relations: [] }, predicate));
|
|
332
|
+
}
|
|
333
|
+
if (!relations || relations.length === 0) {
|
|
334
|
+
throw new errors_1.UnsupportedQueryPlanError("size operator requires a relation mapping");
|
|
335
|
+
}
|
|
336
|
+
// The count applies to the deepest relation; every relation before it is a hop to reach it.
|
|
337
|
+
const hops = relations.slice(0, -1);
|
|
338
|
+
const deepest = relations[relations.length - 1];
|
|
339
|
+
const { kind, count: k, negated } = predicate;
|
|
340
|
+
// `size >= 0` holds whenever the collection can be reached. Only a chain can fail to reach it;
|
|
341
|
+
// a direct relation would be an unconditional `{}`, which no negation could then express.
|
|
342
|
+
if (kind === "atLeast" && k === 0 && !negated && hops.length > 0) {
|
|
343
|
+
return (0, relations_1.wrapInRelations)(hops, {});
|
|
344
|
+
}
|
|
345
|
+
const isNonEmpty = (kind === "atLeast" && k === 1 && !negated) ||
|
|
346
|
+
(kind === "exactly" && k === 0 && negated);
|
|
347
|
+
const isEmpty = (kind === "atLeast" && k === 1 && negated) ||
|
|
348
|
+
(kind === "exactly" && k === 0 && !negated);
|
|
349
|
+
if (!isNonEmpty && !isEmpty) {
|
|
350
|
+
throw new errors_1.UnsupportedQueryPlanError(`Unsupported size comparison: size(...) ${operator} ${count}`);
|
|
351
|
+
}
|
|
352
|
+
return (0, relations_1.wrapInRelations)(hops, (0, relations_1.relationFilter)(deepest, isNonEmpty ? "some" : "none", {}));
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Builds a column-vs-column comparison as a Prisma field reference. Prisma only supports
|
|
356
|
+
* references between fields of the SAME model, so the container is the root model for
|
|
357
|
+
* root-level comparisons and the relation's model inside a lambda; mixed-scope comparisons
|
|
358
|
+
* (an element column against an outer column) are cross-model and must fail loudly.
|
|
359
|
+
*/
|
|
360
|
+
function buildFieldToFieldFilter(context, operator, leftOperand, left, rightOperand, right) {
|
|
361
|
+
const prismaOperator = (0, plan_1.assertDefined)((0, narrowing_1.lookup)(plan_1.CERBOS_TO_PRISMA_OPERATOR, operator), `Unsupported operator: ${operator}`);
|
|
362
|
+
if (!(0, plan_1.isNamedOperand)(leftOperand) || !(0, plan_1.isNamedOperand)(rightOperand)) {
|
|
363
|
+
throw new errors_1.UnsupportedQueryPlanError("Field-to-field comparison requires two named operands");
|
|
364
|
+
}
|
|
365
|
+
const container = fieldReferenceContainer(context, leftOperand.name, left, rightOperand.name, right);
|
|
366
|
+
const leftField = (0, mapping_1.getLeafField)(left.path);
|
|
367
|
+
const rightField = (0, mapping_1.getLeafField)(right.path);
|
|
368
|
+
const fieldReference = { _ref: rightField, _container: container };
|
|
369
|
+
// Two explicit nulls are EQUAL in CEL, and a null against a value is definitely NOT equal.
|
|
370
|
+
// A bare field reference gives UNKNOWN for both, which drops the row under either polarity.
|
|
371
|
+
const leftExplicit = (0, mapping_1.isExplicitNullReference)(left);
|
|
372
|
+
const rightExplicit = (0, mapping_1.isExplicitNullReference)(right);
|
|
373
|
+
if (operator === "eq" || operator === "ne") {
|
|
374
|
+
// Mixing the two conventions across one comparison has no faithful rendering. The declared
|
|
375
|
+
// side needs a definite answer for its NULL (CEL holds a null VALUE); the undeclared side
|
|
376
|
+
// needs UNKNOWN for its NULL (a missing attribute, which CEL denies under both polarities). A
|
|
377
|
+
// definite predicate returns rows the PDP refuses; a plain one drops rows the PDP allows.
|
|
378
|
+
// Refuse it rather than pick a direction — declare both attributes, or neither.
|
|
379
|
+
// Mixing the two conventions across one comparison: the explicit side's NULL is a null VALUE,
|
|
380
|
+
// which `!=` answers TRUE against any present value, while the other side's NULL is a missing
|
|
381
|
+
// attribute, an error under both polarities. `other = other` is TRUE for a present value and
|
|
382
|
+
// UNKNOWN for a NULL one, so it carries exactly that error into the explicit-null arm:
|
|
383
|
+
//
|
|
384
|
+
// explicit NULL, other present -> TRUE explicit NULL, other NULL -> UNKNOWN
|
|
385
|
+
// explicit present, other NULL -> UNKNOWN both present -> the plain comparison
|
|
386
|
+
//
|
|
387
|
+
// and `==` is its negation.
|
|
388
|
+
if (leftExplicit !== rightExplicit) {
|
|
389
|
+
const [explicitField, otherField] = leftExplicit
|
|
390
|
+
? [leftField, rightField]
|
|
391
|
+
: [rightField, leftField];
|
|
392
|
+
const otherReference = { _ref: otherField, _container: container };
|
|
393
|
+
const inequality = {
|
|
394
|
+
OR: [
|
|
395
|
+
{
|
|
396
|
+
AND: [
|
|
397
|
+
{ [explicitField]: null },
|
|
398
|
+
{ [otherField]: { equals: otherReference } },
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
{ NOT: { [explicitField]: { equals: otherReference } } },
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
return operator === "ne" ? inequality : { NOT: inequality };
|
|
405
|
+
}
|
|
406
|
+
if (leftExplicit) {
|
|
407
|
+
const equality = {
|
|
408
|
+
OR: [
|
|
409
|
+
{ AND: [{ [leftField]: null }, { [rightField]: null }] },
|
|
410
|
+
{
|
|
411
|
+
AND: [
|
|
412
|
+
{ [leftField]: { not: null } },
|
|
413
|
+
{ [rightField]: { not: null } },
|
|
414
|
+
{ [leftField]: { equals: fieldReference } },
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
return operator === "eq" ? equality : { NOT: equality };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// `ne` is the one operator whose Prisma spelling cannot carry a field reference: `not` nests a
|
|
423
|
+
// NestedStringFilter, which has no `_ref` form, so `{ col: { not: { _ref } } }` is rejected by
|
|
424
|
+
// the client rather than executed. Hoisting the negation to the top-level `NOT` combinator says
|
|
425
|
+
// the same thing with a shape that does accept one — the same rewrite the explicit-null branch
|
|
426
|
+
// above already performs. Three-valued logic is unchanged by the hoist: a NULL on either side
|
|
427
|
+
// makes the inner equality UNKNOWN, and `NOT UNKNOWN` is still UNKNOWN, so the row stays out
|
|
428
|
+
// under both polarities, which is what a missing attribute does on the check side.
|
|
429
|
+
if (operator === "ne") {
|
|
430
|
+
return { NOT: { [leftField]: { equals: fieldReference } } };
|
|
431
|
+
}
|
|
432
|
+
return { [leftField]: { [prismaOperator]: fieldReference } };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* The Prisma model both columns of a field-to-field comparison belong to: the relation model
|
|
436
|
+
* inside a lambda when both are element columns, otherwise the root model.
|
|
437
|
+
*/
|
|
438
|
+
function fieldReferenceContainer(context, leftName, left, rightName, right) {
|
|
439
|
+
const scope = (0, mapping_1.currentScope)(context);
|
|
440
|
+
if (scope) {
|
|
441
|
+
const prefix = scope.variableName + ".";
|
|
442
|
+
const leftInScope = leftName.startsWith(prefix);
|
|
443
|
+
const rightInScope = rightName.startsWith(prefix);
|
|
444
|
+
if (leftInScope !== rightInScope) {
|
|
445
|
+
throw new errors_1.UnsupportedQueryPlanError(`Cannot compare a collection element column with an outer column (${leftName} vs ${rightName}): ` +
|
|
446
|
+
"Prisma field references only work between fields of the same model");
|
|
447
|
+
}
|
|
448
|
+
if (leftInScope) {
|
|
449
|
+
if (!scope.relationModel) {
|
|
450
|
+
throw new Error("Field-to-field comparison inside a collection requires `relation.model` in the mapper");
|
|
451
|
+
}
|
|
452
|
+
return scope.relationModel;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if ((left.relations && left.relations.length > 0) ||
|
|
456
|
+
(right.relations && right.relations.length > 0)) {
|
|
457
|
+
throw new errors_1.UnsupportedQueryPlanError("Cannot compare columns across relations: Prisma field references only work between fields of the same model");
|
|
458
|
+
}
|
|
459
|
+
if (!context.rootModel) {
|
|
460
|
+
throw new Error("Field-to-field comparison requires the `model` option (the Prisma model name) to build a field reference");
|
|
461
|
+
}
|
|
462
|
+
return context.rootModel;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Translates `column ⊕ constant CMP value` (and its mirrored forms) by solving for the
|
|
466
|
+
* column: Prisma where-filters cannot express column arithmetic, but linear arithmetic with
|
|
467
|
+
* one constant side always rewrites to a plain comparison. Multiplying/dividing by a
|
|
468
|
+
* negative constant mirrors directional operators. The rewrite happens in IEEE double space,
|
|
469
|
+
* matching CEL: Cerbos attribute numbers are doubles, so this preserves check-time semantics
|
|
470
|
+
* (e.g. `n * 0.1 == 0.3` solves to an unrepresentable fraction and matches no integer row,
|
|
471
|
+
* exactly as CEL evaluates it).
|
|
472
|
+
*/
|
|
473
|
+
function handleArithmeticComparison(operator, arithExpr, otherOperand, arithIsLeft, context) {
|
|
474
|
+
const arithOp = arithExpr.operator;
|
|
475
|
+
const arithLeftOp = (0, plan_1.assertDefined)(arithExpr.operands[0], `${arithOp} operator requires a left operand`);
|
|
476
|
+
const arithRightOp = (0, plan_1.assertDefined)(arithExpr.operands[1], `${arithOp} operator requires a right operand`);
|
|
477
|
+
if ((0, plan_1.isOperatorOperand)(otherOperand) &&
|
|
478
|
+
evaluate_1.ARITHMETIC_OPERATORS.has(otherOperand.operator) &&
|
|
479
|
+
(0, translate_1.tryFoldValueExpression)(otherOperand, context) === null) {
|
|
480
|
+
throw new errors_1.UnsupportedQueryPlanError("Arithmetic on both sides of a comparison is not supported: the expression cannot be solved to a plain column filter");
|
|
481
|
+
}
|
|
482
|
+
const arithLeft = (0, translate_1.resolveOperand)(arithLeftOp, context);
|
|
483
|
+
const arithRight = (0, translate_1.resolveOperand)(arithRightOp, context);
|
|
484
|
+
const other = (0, translate_1.resolveOperand)(otherOperand, context);
|
|
485
|
+
// Fully constant arithmetic: fold and compare the other side against the result. The
|
|
486
|
+
// arithmetic side keeps its source position, so when the folded constant was written
|
|
487
|
+
// FIRST (`1 + 2 < f`), directional operators mirror to keep the field on the left.
|
|
488
|
+
if ((0, mapping_1.isResolvedValue)(arithLeft) && (0, mapping_1.isResolvedValue)(arithRight)) {
|
|
489
|
+
const folded = (0, evaluate_1.foldArithmetic)(arithOp, arithLeft.value, arithRight.value);
|
|
490
|
+
if (!(0, mapping_1.isResolvedFieldReference)(other)) {
|
|
491
|
+
throw new errors_1.UnsupportedQueryPlanError(`${arithOp} with two values requires a field reference on the other side`);
|
|
492
|
+
}
|
|
493
|
+
return (0, fields_1.buildComparisonFilter)(context, other, arithIsLeft ? (0, plan_1.mirrorOperator)(operator) : operator, folded);
|
|
494
|
+
}
|
|
495
|
+
if (!(0, mapping_1.isResolvedValue)(other)) {
|
|
496
|
+
throw new errors_1.UnsupportedQueryPlanError(`${arithOp} operator with field references requires a value on the other side of the comparison`);
|
|
497
|
+
}
|
|
498
|
+
let fieldRef;
|
|
499
|
+
let constant;
|
|
500
|
+
let fieldIsLeft;
|
|
501
|
+
if ((0, mapping_1.isResolvedFieldReference)(arithLeft) && (0, mapping_1.isResolvedValue)(arithRight)) {
|
|
502
|
+
fieldRef = arithLeft;
|
|
503
|
+
constant = arithRight.value;
|
|
504
|
+
fieldIsLeft = true;
|
|
505
|
+
}
|
|
506
|
+
else if ((0, mapping_1.isResolvedValue)(arithLeft) &&
|
|
507
|
+
(0, mapping_1.isResolvedFieldReference)(arithRight)) {
|
|
508
|
+
fieldRef = arithRight;
|
|
509
|
+
constant = arithLeft.value;
|
|
510
|
+
fieldIsLeft = false;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
throw new errors_1.UnsupportedQueryPlanError(`${arithOp} operator requires exactly one field reference and one value, or two values`);
|
|
514
|
+
}
|
|
515
|
+
// The comparison operator was normalized arithmetic-side-first by the caller; if the
|
|
516
|
+
// arithmetic was written on the RIGHT of the comparison, direction was already mirrored.
|
|
517
|
+
let effectiveOperator = arithIsLeft ? operator : (0, plan_1.mirrorOperator)(operator);
|
|
518
|
+
const declared = fieldRef.valueType;
|
|
519
|
+
const declaredString = declared === "string";
|
|
520
|
+
if (((0, narrowing_1.isString)(constant) && declared !== undefined && !declaredString) ||
|
|
521
|
+
((0, narrowing_1.isNumber)(constant) &&
|
|
522
|
+
(declaredString || declared === "boolean" || declared === "dateTime"))) {
|
|
523
|
+
throw new errors_1.UnsupportedQueryPlanError(`${arithOp} over a ${declared} field and a ${(0, narrowing_1.typeofValue)(constant)} is a CEL no-overload error`);
|
|
524
|
+
}
|
|
525
|
+
// String concatenation solving (eq/ne only).
|
|
526
|
+
if (arithOp === "add" && (0, narrowing_1.isString)(constant)) {
|
|
527
|
+
if (effectiveOperator !== "eq" && effectiveOperator !== "ne") {
|
|
528
|
+
throw new errors_1.UnsupportedQueryPlanError(`Operator ${effectiveOperator} is not supported with string concatenation`);
|
|
529
|
+
}
|
|
530
|
+
const solvedValue = solveAdd(other.value, constant, fieldIsLeft);
|
|
531
|
+
if (solvedValue === null) {
|
|
532
|
+
// A contradictory / exhaustive pair remains SQL UNKNOWN for a missing field.
|
|
533
|
+
// An empty IN or an empty filter loses that error when an outer NOT is applied.
|
|
534
|
+
const equality = (0, fields_1.buildFieldFilter)(fieldRef, "equals", other.value);
|
|
535
|
+
const inequality = (0, fields_1.buildFieldFilter)(fieldRef, "not", other.value);
|
|
536
|
+
return effectiveOperator === "eq"
|
|
537
|
+
? { AND: [equality, inequality] }
|
|
538
|
+
: { OR: [equality, inequality] };
|
|
539
|
+
}
|
|
540
|
+
// null + "x" is a CEL error, so the solved comparison stays UNKNOWN for a NULL column even
|
|
541
|
+
// where the field is sent as an explicit null.
|
|
542
|
+
const erroring = { ...fieldRef, nullAttributeRepresentation: "omitted" };
|
|
543
|
+
return (0, fields_1.buildComparisonFilter)(context, erroring, effectiveOperator, solvedValue);
|
|
544
|
+
}
|
|
545
|
+
if (!(0, narrowing_1.isNumber)(constant) || !(0, narrowing_1.isNumber)(other.value)) {
|
|
546
|
+
throw new errors_1.UnsupportedQueryPlanError(`${arithOp} comparison requires numeric operands`);
|
|
547
|
+
}
|
|
548
|
+
if (arithOp === "mult" && constant === 0) {
|
|
549
|
+
throw new errors_1.UnsupportedQueryPlanError("Multiplication by a constant zero must be folded by the Cerbos planner");
|
|
550
|
+
}
|
|
551
|
+
if (arithOp === "div" && !fieldIsLeft) {
|
|
552
|
+
throw new errors_1.UnsupportedQueryPlanError("Division by a column is not supported: the comparison cannot be solved to a plain column filter");
|
|
553
|
+
}
|
|
554
|
+
if (arithOp === "div" && constant === 0) {
|
|
555
|
+
throw new errors_1.UnsupportedQueryPlanError("Division by a constant zero is not supported");
|
|
556
|
+
}
|
|
557
|
+
if (!evaluate_1.ARITHMETIC_OPERATORS.has(arithOp)) {
|
|
558
|
+
throw new errors_1.UnsupportedQueryPlanError(`Unsupported operator: ${arithOp}`);
|
|
559
|
+
}
|
|
560
|
+
// The column side as a non-decreasing function of `y`, where `y` is the column or, when the
|
|
561
|
+
// arithmetic reverses the order (`c - x`, a negative multiplier or divisor), its negation —
|
|
562
|
+
// negating a double is exact, so `c - x` is `c + y` and `x * c` is `y * -c`.
|
|
563
|
+
const c = constant;
|
|
564
|
+
const decreasing = (arithOp === "sub" && !fieldIsLeft) || ((arithOp === "mult" || arithOp === "div") && c < 0);
|
|
565
|
+
const f = (y) => {
|
|
566
|
+
switch (arithOp) {
|
|
567
|
+
case "add":
|
|
568
|
+
return y + c;
|
|
569
|
+
case "sub":
|
|
570
|
+
return fieldIsLeft ? y - c : c + y;
|
|
571
|
+
case "mult":
|
|
572
|
+
return decreasing ? y * -c : y * c;
|
|
573
|
+
default:
|
|
574
|
+
return decreasing ? y / -c : y / c;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const solvedOperator = effectiveOperator === "ne" ? "eq" : effectiveOperator;
|
|
578
|
+
if (solvedOperator !== "eq" &&
|
|
579
|
+
solvedOperator !== "lt" &&
|
|
580
|
+
solvedOperator !== "le" &&
|
|
581
|
+
solvedOperator !== "gt" &&
|
|
582
|
+
solvedOperator !== "ge") {
|
|
583
|
+
throw new errors_1.UnsupportedQueryPlanError(`Operator ${effectiveOperator} is not supported with arithmetic`);
|
|
584
|
+
}
|
|
585
|
+
const solved = (0, solve_1.solveMonotone)(f, solvedOperator, other.value);
|
|
586
|
+
const interval = decreasing ? (0, solve_1.negateInterval)(solved) : solved;
|
|
587
|
+
const filter = buildIntervalFilter(context, fieldRef, interval);
|
|
588
|
+
return effectiveOperator === "ne" ? { NOT: filter } : filter;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* `lo <= column <= hi`. An empty interval is a contradiction and an unbounded one a tautology,
|
|
592
|
+
* both spelled on the column, so a NULL keeps them UNKNOWN as the arithmetic's error requires.
|
|
593
|
+
*/
|
|
594
|
+
function buildIntervalFilter(context, fieldRef, interval) {
|
|
595
|
+
if (interval.empty) {
|
|
596
|
+
return {
|
|
597
|
+
AND: [
|
|
598
|
+
(0, fields_1.buildComparisonFilter)(context, fieldRef, "gt", 0),
|
|
599
|
+
(0, fields_1.buildComparisonFilter)(context, fieldRef, "le", 0),
|
|
600
|
+
],
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const bounds = [];
|
|
604
|
+
if (interval.lo !== undefined) {
|
|
605
|
+
bounds.push((0, fields_1.buildComparisonFilter)(context, fieldRef, "ge", interval.lo));
|
|
606
|
+
}
|
|
607
|
+
if (interval.hi !== undefined) {
|
|
608
|
+
bounds.push((0, fields_1.buildComparisonFilter)(context, fieldRef, "le", interval.hi));
|
|
609
|
+
}
|
|
610
|
+
if (bounds.length === 0) {
|
|
611
|
+
return {
|
|
612
|
+
OR: [
|
|
613
|
+
(0, fields_1.buildComparisonFilter)(context, fieldRef, "gt", 0),
|
|
614
|
+
(0, fields_1.buildComparisonFilter)(context, fieldRef, "le", 0),
|
|
615
|
+
],
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
// Each bound may itself be a bracket (see fractionalBracket); one flat AND reads better.
|
|
619
|
+
const flat = bounds.flatMap((bound) => {
|
|
620
|
+
const conjuncts = bound["AND"];
|
|
621
|
+
return Object.keys(bound).length === 1 && Array.isArray(conjuncts) ? conjuncts.filter(value_1.isPrismaFilter) : [bound];
|
|
622
|
+
});
|
|
623
|
+
return flat.length === 1 ? flat[0] : { AND: flat };
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The column value that makes `column + addConstant` (or `addConstant + column`) equal
|
|
627
|
+
* `comparisonValue`, or null when no string does.
|
|
628
|
+
*/
|
|
629
|
+
function solveAdd(comparisonValue, addConstant, fieldIsLeft) {
|
|
630
|
+
if ((0, narrowing_1.isString)(comparisonValue) && (0, narrowing_1.isString)(addConstant)) {
|
|
631
|
+
if (fieldIsLeft) {
|
|
632
|
+
if (!comparisonValue.endsWith(addConstant))
|
|
633
|
+
return null;
|
|
634
|
+
return comparisonValue.slice(0, comparisonValue.length - addConstant.length);
|
|
635
|
+
}
|
|
636
|
+
if (!comparisonValue.startsWith(addConstant))
|
|
637
|
+
return null;
|
|
638
|
+
return comparisonValue.slice(addConstant.length);
|
|
639
|
+
}
|
|
640
|
+
if ((0, narrowing_1.isNumber)(comparisonValue) && (0, narrowing_1.isNumber)(addConstant)) {
|
|
641
|
+
return comparisonValue - addConstant;
|
|
642
|
+
}
|
|
643
|
+
throw new errors_1.UnsupportedQueryPlanError("Type mismatch in add comparison");
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Helper function to handle "in" operator
|
|
647
|
+
*/
|
|
648
|
+
function handleInOperator(operands, context) {
|
|
649
|
+
if (operands.length !== 2) {
|
|
650
|
+
throw new errors_1.UnsupportedQueryPlanError("in requires exactly two operands");
|
|
651
|
+
}
|
|
652
|
+
const ternaryFilter = (0, ternary_1.tryHandleTernaryComparison)("in", operands, context);
|
|
653
|
+
if (ternaryFilter) {
|
|
654
|
+
return ternaryFilter;
|
|
655
|
+
}
|
|
656
|
+
const member = (0, plan_1.assertDefined)(operands[0], "in requires a member operand");
|
|
657
|
+
const collection = (0, plan_1.assertDefined)(operands[1], "in requires a collection operand");
|
|
658
|
+
if ((0, plan_1.isNamedOperand)(member) && (0, plan_1.isValueOperand)(collection)) {
|
|
659
|
+
if (!Array.isArray(collection.value))
|
|
660
|
+
throw scalarMembership();
|
|
661
|
+
return (0, fields_1.buildMembershipFilter)(context, (0, mapping_1.resolveFieldReference)(member.name, context), collection.value);
|
|
662
|
+
}
|
|
663
|
+
if ((0, plan_1.isValueOperand)(member) && (0, plan_1.isNamedOperand)(collection)) {
|
|
664
|
+
if ((0, mapping_1.namesToOneRelation)(context.mapper, collection.name)) {
|
|
665
|
+
throw new errors_1.UnsupportedQueryPlanError(`in over ${collection.name}: it is a to-one relation, which CEL reads as a map, and ` +
|
|
666
|
+
"`in` over a map tests its keys (the attribute names present on the related row); " +
|
|
667
|
+
"a Prisma relation filter tests rows, not attribute names");
|
|
668
|
+
}
|
|
669
|
+
const fieldRef = (0, mapping_1.resolveFieldReference)(collection.name, context);
|
|
670
|
+
if (!fieldRef.relations?.length && fieldRef.valueType !== undefined)
|
|
671
|
+
throw scalarMembership();
|
|
672
|
+
return (0, fields_1.buildMembershipFilter)(context, fieldRef, [member.value]);
|
|
673
|
+
}
|
|
674
|
+
if ((0, plan_1.isOperatorOperand)(collection) && collection.operator === "list") {
|
|
675
|
+
return handleListMembership(member, collection.operands, context);
|
|
676
|
+
}
|
|
677
|
+
if ((0, plan_1.isOperatorOperand)(collection) && collection.operator === "add") {
|
|
678
|
+
throw new errors_1.UnsupportedQueryPlanError(`Membership in a list built by ${collection.operator}(...) from columns: each element is ` +
|
|
679
|
+
"compared with the member, and a Prisma field reference compares two plain columns of " +
|
|
680
|
+
"one model, never a column with an expression over another column or with the " +
|
|
681
|
+
"elements of a related list");
|
|
682
|
+
}
|
|
683
|
+
if ((0, plan_1.isNamedOperand)(member) && (0, plan_1.isNamedOperand)(collection)) {
|
|
684
|
+
throw new errors_1.UnsupportedQueryPlanError("Membership between two database attributes is not supported: Prisma cannot compare a related element column with an outer scalar column");
|
|
685
|
+
}
|
|
686
|
+
throw new errors_1.UnsupportedQueryPlanError("in requires a field member and literal list, or a literal member and field collection");
|
|
687
|
+
}
|
|
688
|
+
function scalarMembership() {
|
|
689
|
+
return new errors_1.UnsupportedQueryPlanError("in over a scalar is a CEL error, which a Prisma filter cannot express");
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* `x in [a, b, ...]` over a list the planner could not fold because an element is a column. CEL
|
|
693
|
+
* builds the list first, which raises an error as soon as one element is a missing attribute, and
|
|
694
|
+
* then tests `x` against each element for equality, so the membership is the OR of the comparisons
|
|
695
|
+
* `x == e`, each translated as that comparison would be. With several elements, an element column
|
|
696
|
+
* the caller omits when NULL gets its presence test ANDed on, TRUE when the value is there and
|
|
697
|
+
* UNKNOWN when the column is NULL, so a missing element leaves the whole membership UNKNOWN even
|
|
698
|
+
* where another element matches. One element needs no test: its equality is already UNKNOWN on a
|
|
699
|
+
* NULL column. An element that is neither a literal nor a column has no presence test, so the
|
|
700
|
+
* membership is refused rather than let its error be absorbed by a matching sibling.
|
|
701
|
+
*/
|
|
702
|
+
function handleListMembership(member, elements, context) {
|
|
703
|
+
if (elements.length === 0) {
|
|
704
|
+
throw new errors_1.UnsupportedQueryPlanError("Membership in an empty list must be folded by the Cerbos planner");
|
|
705
|
+
}
|
|
706
|
+
const equalities = elements.map((element) => (0, translate_1.buildPrismaFilterFromCerbosExpression)({ operator: "eq", operands: [member, element] }, context));
|
|
707
|
+
const filter = equalities.length === 1 ? equalities[0] : { OR: equalities };
|
|
708
|
+
if (elements.length === 1)
|
|
709
|
+
return filter;
|
|
710
|
+
const presences = [];
|
|
711
|
+
for (const element of elements) {
|
|
712
|
+
if ((0, plan_1.isValueOperand)(element))
|
|
713
|
+
continue;
|
|
714
|
+
if (!(0, plan_1.isNamedOperand)(element)) {
|
|
715
|
+
throw new errors_1.UnsupportedQueryPlanError("Membership in a list holding an expression beside other elements: building the list " +
|
|
716
|
+
"raises the expression's error even where another element matches, and no Prisma " +
|
|
717
|
+
"filter carries that error for an expression");
|
|
718
|
+
}
|
|
719
|
+
const fieldRef = (0, mapping_1.resolveFieldReference)(element.name, context);
|
|
720
|
+
if ((0, mapping_1.isExplicitNullReference)(fieldRef))
|
|
721
|
+
continue;
|
|
722
|
+
const test = (0, types_1.presence)(element, fieldRef);
|
|
723
|
+
if (test === undefined) {
|
|
724
|
+
throw new errors_1.UnsupportedQueryPlanError(`Membership in a list holding ${element.name} beside other elements requires the ` +
|
|
725
|
+
"column's valueType in the mapper, to spell the presence test a missing element needs");
|
|
726
|
+
}
|
|
727
|
+
presences.push((0, translate_1.buildPrismaFilterFromCerbosExpression)(test, context));
|
|
728
|
+
}
|
|
729
|
+
return presences.length === 0 ? filter : { AND: [...presences, filter] };
|
|
730
|
+
}
|
|
731
|
+
//# sourceMappingURL=comparison.js.map
|