@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.
- package/README.md +611 -460
- 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 +91 -8
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +34 -2301
- 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 +31 -18
- package/src/adversarial.test.ts +488 -488
- package/src/collections.ts +738 -0
- package/src/comparison.ts +1157 -0
- package/src/corpus.ts +597 -0
- 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 +20 -0
- package/src/generated/prisma-adversarial/client.ts +20 -0
- package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
- package/src/generated/prisma-adversarial/internal/class.ts +44 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +350 -3
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +49 -2
- package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
- package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +547 -75
- package/src/generated/prisma-adversarial/models.ts +4 -0
- package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
- package/src/generated/{prisma → prisma-adversarial-mysql}/client.ts +23 -18
- package/src/generated/{prisma → prisma-adversarial-mysql}/commonInputTypes.ts +167 -92
- 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/index.d.ts +13822 -0
- package/src/generated/prisma-adversarial-pg/browser.ts +64 -0
- package/src/generated/prisma-adversarial-pg/client.ts +86 -0
- package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +345 -0
- package/src/generated/prisma-adversarial-pg/enums.ts +15 -0
- package/src/generated/prisma-adversarial-pg/internal/class.ts +284 -0
- package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +1542 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +72 -43
- package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +2139 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
- package/src/generated/prisma-adversarial-pg/models.ts +20 -0
- package/src/generated/prisma-adversarial-pg-v6/client.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/default.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/edge.d.ts +1 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +14918 -0
- package/src/generated/prisma-adversarial-pg-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-adversarial-pg-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-adversarial-pg-v6/wasm.d.ts +1 -0
- package/src/generated/prisma-adversarial-v6/index.d.ts +6515 -304
- package/src/hierarchy.ts +405 -0
- package/src/index.ts +134 -3325
- 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 +21 -0
- 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.pg.v6.ts +13 -0
- package/src/test-setup.adversarial.pg.v7.ts +9 -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 +766 -0
- package/src/types.ts +1310 -0
- package/src/value.ts +16 -0
- package/src/generated/prisma/browser.ts +0 -59
- package/src/generated/prisma/internal/class.ts +0 -274
- package/src/generated/prisma/internal/prismaNamespace.ts +0 -1403
- package/src/generated/prisma/models/Category.ts +0 -1412
- package/src/generated/prisma/models/Label.ts +0 -1276
- package/src/generated/prisma/models/NestedResource.ts +0 -1562
- package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
- package/src/generated/prisma/models/Resource.ts +0 -2196
- package/src/generated/prisma/models/SubCategory.ts +0 -1412
- package/src/generated/prisma/models/Tag.ts +0 -1276
- package/src/generated/prisma/models/User.ts +0 -1503
- package/src/generated/prisma/models.ts +0 -19
- package/src/generated/prisma-v6/index.d.ts +0 -13726
- package/src/index.test.ts +0 -8885
- package/src/test-setup.v6.ts +0 -5
- package/src/test-setup.v7.ts +0 -7
- /package/src/generated/{prisma → prisma-adversarial-mysql}/enums.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/client.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/default.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/edge.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/index-browser.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/library.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/wasm.d.ts +0 -0
package/src/evaluate.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Evaluating operators whose operands are all constants, with CEL's semantics.
|
|
2
|
+
|
|
3
|
+
import type { Value } from "@cerbos/core";
|
|
4
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
5
|
+
import { isNumber, isObject, isString } from "./narrowing";
|
|
6
|
+
|
|
7
|
+
export const ARITHMETIC_OPERATORS = new Set(["add", "sub", "mult", "div"]);
|
|
8
|
+
|
|
9
|
+
export function evaluateConstantComparison(
|
|
10
|
+
operator: string,
|
|
11
|
+
left: Value,
|
|
12
|
+
right: Value
|
|
13
|
+
): boolean {
|
|
14
|
+
switch (operator) {
|
|
15
|
+
case "in":
|
|
16
|
+
if (!Array.isArray(right)) {
|
|
17
|
+
throw new UnsupportedQueryPlanError("in constant comparison requires a list");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return right.some((element) => areValuesEqual(left, element));
|
|
21
|
+
case "eq":
|
|
22
|
+
return areValuesEqual(left, right);
|
|
23
|
+
case "ne":
|
|
24
|
+
return !areValuesEqual(left, right);
|
|
25
|
+
case "lt":
|
|
26
|
+
case "le":
|
|
27
|
+
case "gt":
|
|
28
|
+
case "ge": {
|
|
29
|
+
if (
|
|
30
|
+
isNumber(left) &&
|
|
31
|
+
isNumber(right) &&
|
|
32
|
+
(Number.isNaN(left) || Number.isNaN(right))
|
|
33
|
+
) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!(
|
|
39
|
+
(isNumber(left) && isNumber(right)) ||
|
|
40
|
+
(isString(left) && isString(right))
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
throw new UnsupportedQueryPlanError(
|
|
44
|
+
`${operator} constant comparison requires two numbers or two strings`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const order = compareConstantValues(left, right);
|
|
49
|
+
|
|
50
|
+
switch (operator) {
|
|
51
|
+
case "lt":
|
|
52
|
+
return order < 0;
|
|
53
|
+
case "le":
|
|
54
|
+
return order <= 0;
|
|
55
|
+
case "gt":
|
|
56
|
+
return order > 0;
|
|
57
|
+
case "ge":
|
|
58
|
+
return order >= 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new UnsupportedQueryPlanError(`Unsupported constant comparison operator: ${operator}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Orders numbers numerically and strings by Unicode code point, as CEL does. */
|
|
67
|
+
function compareConstantValues(
|
|
68
|
+
left: number | string,
|
|
69
|
+
right: number | string
|
|
70
|
+
): number {
|
|
71
|
+
if (isNumber(left) && isNumber(right)) {
|
|
72
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!isString(left) || !isString(right)) {
|
|
76
|
+
throw new UnsupportedQueryPlanError("Cannot order constant values of different types");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const leftCodePoints = Array.from(left, (character) =>
|
|
80
|
+
character.codePointAt(0)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const rightCodePoints = Array.from(right, (character) =>
|
|
84
|
+
character.codePointAt(0)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const sharedLength = Math.min(leftCodePoints.length, rightCodePoints.length);
|
|
88
|
+
|
|
89
|
+
for (let index = 0; index < sharedLength; index++) {
|
|
90
|
+
const leftCodePoint = leftCodePoints[index]!;
|
|
91
|
+
const rightCodePoint = rightCodePoints[index]!;
|
|
92
|
+
|
|
93
|
+
if (leftCodePoint !== rightCodePoint) {
|
|
94
|
+
return leftCodePoint < rightCodePoint ? -1 : 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return leftCodePoints.length === rightCodePoints.length
|
|
99
|
+
? 0
|
|
100
|
+
: leftCodePoints.length < rightCodePoints.length
|
|
101
|
+
? -1
|
|
102
|
+
: 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function areValuesEqual(left: Value, right: Value): boolean {
|
|
106
|
+
if (Array.isArray(left)) {
|
|
107
|
+
return (
|
|
108
|
+
Array.isArray(right) &&
|
|
109
|
+
left.length === right.length &&
|
|
110
|
+
left.every((value, index) => areValuesEqual(value, right[index]!))
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(right)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isObject(left)) {
|
|
119
|
+
if (!isObject(right)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const leftKeys = Object.keys(left);
|
|
124
|
+
const rightKeys = Object.keys(right);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
leftKeys.length === rightKeys.length &&
|
|
128
|
+
leftKeys.every(
|
|
129
|
+
(key) => key in right && areValuesEqual(left[key]!, right[key]!)
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return left === right;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function foldArithmetic(
|
|
138
|
+
operator: string,
|
|
139
|
+
left: Value,
|
|
140
|
+
right: Value
|
|
141
|
+
): Value {
|
|
142
|
+
if (operator === "add" && Array.isArray(left) && Array.isArray(right)) {
|
|
143
|
+
return [...left, ...right];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
operator === "add" &&
|
|
148
|
+
isString(left) &&
|
|
149
|
+
isString(right)
|
|
150
|
+
) {
|
|
151
|
+
return left + right;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!isNumber(left) || !isNumber(right)) {
|
|
155
|
+
throw new UnsupportedQueryPlanError(`${operator} operator requires string or number operands`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
switch (operator) {
|
|
159
|
+
case "add":
|
|
160
|
+
return left + right;
|
|
161
|
+
case "sub":
|
|
162
|
+
return left - right;
|
|
163
|
+
case "mult":
|
|
164
|
+
return left * right;
|
|
165
|
+
case "div":
|
|
166
|
+
return left / right;
|
|
167
|
+
default:
|
|
168
|
+
throw new UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/fields.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// Predicates on one resolved column: the leaves every operator handler bottoms out in.
|
|
2
|
+
|
|
3
|
+
import type { Value } from "@cerbos/core";
|
|
4
|
+
|
|
5
|
+
import type { PrismaFilter } from "./index";
|
|
6
|
+
import {
|
|
7
|
+
assertNullOperandTranslatable,
|
|
8
|
+
getLeafField,
|
|
9
|
+
isExplicitNullReference,
|
|
10
|
+
} from "./mapping";
|
|
11
|
+
import type { ResolvedFieldReference, TranslationContext } from "./mapping";
|
|
12
|
+
import { CERBOS_TO_PRISMA_OPERATOR, assertDefined } from "./plan";
|
|
13
|
+
import { wrapInRelations } from "./relations";
|
|
14
|
+
import { UnsupportedQueryPlanError } from "./errors";
|
|
15
|
+
import { isBoolean, isNumber, isObject, lookup, typeofValue } from "./narrowing";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prisma has no model-agnostic always-false `where` shape — empty logical arrays are ignored —
|
|
19
|
+
* so a predicate that folds to constant false is refused. Real PDP plans fold it to
|
|
20
|
+
* ALWAYS_DENIED before it reaches the adapter.
|
|
21
|
+
*/
|
|
22
|
+
export function rejectConstantFalse(): never {
|
|
23
|
+
throw new UnsupportedQueryPlanError(
|
|
24
|
+
"A constant-false conditional predicate must be folded by the Cerbos planner"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `{ field: { [prismaOp]: value } }`, nested through the field's relations. */
|
|
29
|
+
export function buildFieldFilter(
|
|
30
|
+
fieldRef: ResolvedFieldReference,
|
|
31
|
+
prismaOp: string,
|
|
32
|
+
value: Value
|
|
33
|
+
): PrismaFilter {
|
|
34
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
35
|
+
|
|
36
|
+
return wrapInRelations(fieldRef.relations, {
|
|
37
|
+
[fieldName]: { [prismaOp]: value },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const LIKE_METACHARACTERS = /[%_\\]/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `column.startsWith(prefix)`, or, when `strict`, the prefix followed by at least one character.
|
|
45
|
+
*
|
|
46
|
+
* Prisma's `startsWith` is a LIKE with no ESCAPE clause, so a prefix holding `%`, `_` or `\` is
|
|
47
|
+
* spelled as the range of strings it bounds instead: every string with that prefix is at least the
|
|
48
|
+
* prefix and below the prefix with its last code point stepped up, and no other string is, under
|
|
49
|
+
* the code-point order CEL's own string comparisons already require of the collation. A NULL
|
|
50
|
+
* column is UNKNOWN under either spelling, as the missing attribute's error requires.
|
|
51
|
+
*/
|
|
52
|
+
export function buildPrefixFilter(
|
|
53
|
+
fieldRef: ResolvedFieldReference,
|
|
54
|
+
prefix: string,
|
|
55
|
+
strict = false
|
|
56
|
+
): PrismaFilter {
|
|
57
|
+
if (!LIKE_METACHARACTERS.test(prefix)) {
|
|
58
|
+
return buildFieldFilter(fieldRef, "startsWith", strict ? `${prefix}_` : prefix);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
62
|
+
const lower = { [fieldName]: { [strict ? "gt" : "gte"]: prefix } };
|
|
63
|
+
const bound = firstStringPastPrefix(prefix);
|
|
64
|
+
|
|
65
|
+
return wrapInRelations(
|
|
66
|
+
fieldRef.relations,
|
|
67
|
+
bound === undefined ? lower : { AND: [lower, { [fieldName]: { lt: bound } }] }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The least string, in code-point order, above every string that starts with `prefix`: the prefix
|
|
73
|
+
* with its last code point stepped up, skipping the surrogate range no string holds. A trailing
|
|
74
|
+
* U+10FFFF has no successor, so it is dropped and the step applied before it; undefined when
|
|
75
|
+
* nothing is left, where no string is above the prefix.
|
|
76
|
+
*/
|
|
77
|
+
function firstStringPastPrefix(prefix: string): string | undefined {
|
|
78
|
+
const codePoints = Array.from(prefix, (character) => character.codePointAt(0)!);
|
|
79
|
+
|
|
80
|
+
while (codePoints.length > 0) {
|
|
81
|
+
const last = codePoints.pop()!;
|
|
82
|
+
|
|
83
|
+
if (last >= 0x10ffff) continue;
|
|
84
|
+
const next = last + 1 === 0xd800 ? 0xe000 : last + 1;
|
|
85
|
+
|
|
86
|
+
return String.fromCodePoint(...codePoints, next);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A filter no row satisfies, spelled on a column the model is known to have. */
|
|
93
|
+
export function buildImpossibleFilter(
|
|
94
|
+
fieldRef: ResolvedFieldReference
|
|
95
|
+
): PrismaFilter {
|
|
96
|
+
return buildFieldFilter(fieldRef, "in", []);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildDirectOrInLeaf(
|
|
100
|
+
fieldRef: ResolvedFieldReference,
|
|
101
|
+
values: Value[]
|
|
102
|
+
): PrismaFilter {
|
|
103
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
104
|
+
|
|
105
|
+
return values.length === 1
|
|
106
|
+
? { [fieldName]: values[0] }
|
|
107
|
+
: { [fieldName]: { in: values } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `field IS NOT NULL AND <leaf>`, so a NULL column makes the whole thing definitely FALSE rather
|
|
112
|
+
* than merely unmatched — which is what lets an enclosing `NOT` include the row.
|
|
113
|
+
*
|
|
114
|
+
* Prisma has no three-valued logic to expose: a record either matches a `where` or it does not.
|
|
115
|
+
* That is exactly why the guard has to be explicit — `{ NOT: { field: { equals: "x" } } }` leans on
|
|
116
|
+
* the generated SQL's UNKNOWN, which excludes the record under both polarities, and an
|
|
117
|
+
* explicit-null attribute needs the negation to include it.
|
|
118
|
+
*
|
|
119
|
+
* The guard is ANDed with the leaf BEFORE the relation wrapping, so both halves are evaluated
|
|
120
|
+
* against the same element rather than against two independently-matched ones.
|
|
121
|
+
*/
|
|
122
|
+
function guardedFieldFilter(
|
|
123
|
+
fieldRef: ResolvedFieldReference,
|
|
124
|
+
leaf: PrismaFilter
|
|
125
|
+
): PrismaFilter {
|
|
126
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
127
|
+
|
|
128
|
+
return wrapInRelations(fieldRef.relations, {
|
|
129
|
+
AND: [{ [fieldName]: { not: null } }, leaf],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function assertScalarValue(
|
|
134
|
+
fieldRef: ResolvedFieldReference,
|
|
135
|
+
value: Value,
|
|
136
|
+
operator: string
|
|
137
|
+
): void {
|
|
138
|
+
if (isObject(value)) {
|
|
139
|
+
throw new UnsupportedQueryPlanError(
|
|
140
|
+
`${operator} requires scalar values: Prisma cannot compare list or map elements`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
value !== null &&
|
|
146
|
+
fieldRef.valueType !== undefined &&
|
|
147
|
+
fieldRef.valueType !== "dateTime" &&
|
|
148
|
+
typeofValue(value) !== fieldRef.valueType
|
|
149
|
+
) {
|
|
150
|
+
throw new UnsupportedQueryPlanError(
|
|
151
|
+
`${operator} value type does not match mapped ${fieldRef.valueType} field`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function assertStringField(
|
|
157
|
+
fieldRef: ResolvedFieldReference,
|
|
158
|
+
operator: string
|
|
159
|
+
): void {
|
|
160
|
+
if (fieldRef.valueType !== undefined && fieldRef.valueType !== "string") {
|
|
161
|
+
throw new UnsupportedQueryPlanError(
|
|
162
|
+
`${operator} requires a string field, got ${fieldRef.valueType}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `field IN values`, with CEL's semantics for a null element and for an explicit-null column. */
|
|
168
|
+
export function buildMembershipFilter(
|
|
169
|
+
context: TranslationContext,
|
|
170
|
+
fieldRef: ResolvedFieldReference,
|
|
171
|
+
listed: Value[]
|
|
172
|
+
): PrismaFilter {
|
|
173
|
+
// A column declared NOT NULL never equals a null element, and Prisma rejects `{ field: null }` on one.
|
|
174
|
+
const values = fieldRef.nullable === false ? listed.filter((value) => value !== null) : listed;
|
|
175
|
+
|
|
176
|
+
for (const value of values) assertScalarValue(fieldRef, value, "in");
|
|
177
|
+
const nonNullValues = values.filter((value) => value !== null);
|
|
178
|
+
const carriesNull = nonNullValues.length !== values.length;
|
|
179
|
+
|
|
180
|
+
if (carriesNull) {
|
|
181
|
+
assertNullOperandTranslatable(
|
|
182
|
+
context,
|
|
183
|
+
"a null element in an `in` list",
|
|
184
|
+
fieldRef.nullAttributeRepresentation
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const filters: PrismaFilter[] = [];
|
|
189
|
+
|
|
190
|
+
if (nonNullValues.length > 0 || values.length === 0) {
|
|
191
|
+
// Without a null element nothing has made the membership definite yet: a NULL column is
|
|
192
|
+
// dropped by `{ in: [...] }` under BOTH polarities, while CEL compares a null VALUE against
|
|
193
|
+
// each element and gets a definite false, so its negation is TRUE. With a null element the
|
|
194
|
+
// `[null]` disjunct below already settles it.
|
|
195
|
+
const leaf = buildDirectOrInLeaf(fieldRef, nonNullValues);
|
|
196
|
+
filters.push(
|
|
197
|
+
carriesNull || !isExplicitNullReference(fieldRef)
|
|
198
|
+
? wrapInRelations(fieldRef.relations, leaf)
|
|
199
|
+
: guardedFieldFilter(fieldRef, leaf)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (carriesNull) {
|
|
204
|
+
filters.push(
|
|
205
|
+
wrapInRelations(fieldRef.relations, buildDirectOrInLeaf(fieldRef, [null]))
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return filters.length === 1 ? filters[0]! : { OR: filters };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The two-clause bracket a fractional constant needs, or undefined for an operator that has
|
|
214
|
+
* none. Prisma coerces filter values to the column's type: on an Int column, `gte: 1.5` is
|
|
215
|
+
* silently bound as `gte: 1`, inverting rows like aNumber == 1. Each bracket pairs the exact
|
|
216
|
+
* constant with its integer neighbor so the combination is correct BOTH for Float columns (the
|
|
217
|
+
* extra clause is redundant) and Int columns (whichever way Prisma truncates or floors the
|
|
218
|
+
* fraction).
|
|
219
|
+
*/
|
|
220
|
+
function fractionalBracket(
|
|
221
|
+
fieldName: string,
|
|
222
|
+
operator: string,
|
|
223
|
+
value: number
|
|
224
|
+
): PrismaFilter | undefined {
|
|
225
|
+
const lower = Math.floor(value);
|
|
226
|
+
const upper = Math.ceil(value);
|
|
227
|
+
|
|
228
|
+
const equalsBracket = {
|
|
229
|
+
AND: [
|
|
230
|
+
{ [fieldName]: { equals: value } },
|
|
231
|
+
{ [fieldName]: { gt: lower } },
|
|
232
|
+
{ [fieldName]: { lt: upper } },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
switch (operator) {
|
|
237
|
+
case "gt":
|
|
238
|
+
return {
|
|
239
|
+
OR: [{ [fieldName]: { gt: value } }, { [fieldName]: { gte: upper } }],
|
|
240
|
+
};
|
|
241
|
+
case "ge":
|
|
242
|
+
return {
|
|
243
|
+
AND: [{ [fieldName]: { gte: value } }, { [fieldName]: { gt: lower } }],
|
|
244
|
+
};
|
|
245
|
+
case "lt":
|
|
246
|
+
return {
|
|
247
|
+
OR: [{ [fieldName]: { lt: value } }, { [fieldName]: { lte: lower } }],
|
|
248
|
+
};
|
|
249
|
+
case "le":
|
|
250
|
+
return {
|
|
251
|
+
AND: [{ [fieldName]: { lte: value } }, { [fieldName]: { lt: upper } }],
|
|
252
|
+
};
|
|
253
|
+
case "eq":
|
|
254
|
+
return equalsBracket;
|
|
255
|
+
case "ne":
|
|
256
|
+
return { NOT: equalsBracket };
|
|
257
|
+
default:
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* An ordering against a boolean constant. CEL orders bools, `false < true`, but Prisma's Boolean
|
|
264
|
+
* filter has only `equals` and `not`, so the ordering becomes the set of bools that satisfy it,
|
|
265
|
+
* spelled with `equals` alone. Every spelling reads the column, so a NULL stays UNKNOWN under both
|
|
266
|
+
* polarities, as the missing-attribute error requires: an empty set is a contradiction on the
|
|
267
|
+
* column and the full set a tautology on it. Plain `equals` rather than the explicit-null guard,
|
|
268
|
+
* because ordering a null VALUE is a no-overload error in CEL, not a definite answer.
|
|
269
|
+
*/
|
|
270
|
+
function buildBooleanOrderingFilter(
|
|
271
|
+
fieldRef: ResolvedFieldReference,
|
|
272
|
+
operator: string,
|
|
273
|
+
value: boolean
|
|
274
|
+
): PrismaFilter {
|
|
275
|
+
const satisfied = [false, true].filter((candidate) => {
|
|
276
|
+
const [a, b] = [Number(candidate), Number(value)];
|
|
277
|
+
|
|
278
|
+
switch (operator) {
|
|
279
|
+
case "lt":
|
|
280
|
+
return a < b;
|
|
281
|
+
case "le":
|
|
282
|
+
return a <= b;
|
|
283
|
+
case "gt":
|
|
284
|
+
return a > b;
|
|
285
|
+
case "ge":
|
|
286
|
+
return a >= b;
|
|
287
|
+
default:
|
|
288
|
+
throw new UnsupportedQueryPlanError(`Unsupported operator: ${operator}`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (satisfied.length === 1) return buildFieldFilter(fieldRef, "equals", satisfied[0]!);
|
|
293
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
294
|
+
const both = [{ [fieldName]: { equals: false } }, { [fieldName]: { equals: true } }];
|
|
295
|
+
|
|
296
|
+
return wrapInRelations(
|
|
297
|
+
fieldRef.relations,
|
|
298
|
+
satisfied.length === 0 ? { AND: both } : { OR: both }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Builds a field-vs-constant comparison. Fractional constants are emitted as a two-clause
|
|
304
|
+
* bracket (see fractionalBracket).
|
|
305
|
+
*/
|
|
306
|
+
export function buildComparisonFilter(
|
|
307
|
+
context: TranslationContext,
|
|
308
|
+
fieldRef: ResolvedFieldReference,
|
|
309
|
+
operator: string,
|
|
310
|
+
value: Value
|
|
311
|
+
): PrismaFilter {
|
|
312
|
+
const prismaOperator = assertDefined(
|
|
313
|
+
lookup(CERBOS_TO_PRISMA_OPERATOR, operator),
|
|
314
|
+
`Unsupported operator: ${operator}`
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
assertScalarValue(fieldRef, value, operator);
|
|
318
|
+
|
|
319
|
+
if (isBoolean(value) && operator !== "eq" && operator !== "ne") {
|
|
320
|
+
return buildBooleanOrderingFilter(fieldRef, operator, value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (value === null) {
|
|
324
|
+
assertNullOperandTranslatable(
|
|
325
|
+
context,
|
|
326
|
+
`\`${operator}\` against a null operand`,
|
|
327
|
+
fieldRef.nullAttributeRepresentation
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// An attribute the caller sends as an explicit null holds a null VALUE in CEL, so equality
|
|
332
|
+
// against a non-null operand is a definite FALSE and inequality a definite TRUE. Prisma leans
|
|
333
|
+
// on SQL's UNKNOWN for both, which drops the row under either polarity, so the presence of the
|
|
334
|
+
// column has to become part of the predicate. Confined to the equality family: an ordering
|
|
335
|
+
// comparison against a null receiver is a no-overload error in CEL, which denies exactly as the
|
|
336
|
+
// dropped row does.
|
|
337
|
+
if (
|
|
338
|
+
value !== null &&
|
|
339
|
+
isExplicitNullReference(fieldRef) &&
|
|
340
|
+
(operator === "eq" || operator === "ne")
|
|
341
|
+
) {
|
|
342
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
343
|
+
|
|
344
|
+
const equality = guardedFieldFilter(fieldRef, {
|
|
345
|
+
[fieldName]: { equals: value },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return operator === "eq" ? equality : { NOT: equality };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isNumber(value) && !Number.isInteger(value)) {
|
|
352
|
+
const bracket = fractionalBracket(
|
|
353
|
+
getLeafField(fieldRef.path),
|
|
354
|
+
operator,
|
|
355
|
+
value
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (bracket) {
|
|
359
|
+
return wrapInRelations(fieldRef.relations, bracket);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return buildFieldFilter(fieldRef, prismaOperator, value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** OR of `field IS NULL` checks for every nullable element column the lambda touched. */
|
|
367
|
+
export function buildNullWitnessFilter(
|
|
368
|
+
nullableFields: Set<string>
|
|
369
|
+
): PrismaFilter {
|
|
370
|
+
const checks = [...nullableFields].map((field) => ({ [field]: null }));
|
|
371
|
+
|
|
372
|
+
return checks.length === 1 ? checks[0]! : { OR: checks };
|
|
373
|
+
}
|