@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/translator.test.ts
CHANGED
|
@@ -1,1305 +1,320 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
|
|
4
1
|
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import type { JsonValue } from "@bufbuild/protobuf";
|
|
5
3
|
import type {
|
|
6
4
|
PlanExpressionOperand,
|
|
7
5
|
PlanResourcesResponse,
|
|
6
|
+
Value,
|
|
8
7
|
} from "@cerbos/core";
|
|
9
8
|
|
|
10
|
-
import { queryPlanToPrisma, PlanKind } from ".";
|
|
9
|
+
import { queryPlanToPrisma, PlanKind, UnsupportedQueryPlanError } from ".";
|
|
11
10
|
import type {
|
|
12
11
|
Mapper,
|
|
12
|
+
MapperConfig,
|
|
13
13
|
NullAttributeRepresentation,
|
|
14
14
|
PrismaFilter,
|
|
15
15
|
QueryPlanToPrismaResult,
|
|
16
16
|
} from ".";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
import type { ActionsFile } from "./corpus";
|
|
17
|
+
import { MAPPER, MODEL, pdpTags, planOf, readGolden, readGoldens } from "./corpus";
|
|
18
|
+
import { isObject } from "./narrowing";
|
|
19
|
+
|
|
20
|
+
type Relation = NonNullable<MapperConfig["relation"]>;
|
|
21
|
+
|
|
22
|
+
/** `response` with `field` replaced by a value its type rules out: a plan the SDK never decodes. */
|
|
23
|
+
const overwritten = (response: PlanResourcesResponse, field: string, value: Value): PlanResourcesResponse =>
|
|
24
|
+
Object.defineProperty({ ...response }, field, { value, enumerable: true });
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* A per-adapter suite used to braid four assertions into every test. Three of them are somebody
|
|
32
|
-
* else's job now, and this file makes only the fourth:
|
|
27
|
+
* Offline unit tests: what a caller can pass that the conformance corpus cannot vary (mapper forms,
|
|
28
|
+
* `subqueryFilter`, element nullability, `nullAttributeRepresentation`), the refusal type, the
|
|
29
|
+
* timestamp literal contract, and plans the planner cannot produce.
|
|
33
30
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* | which shapes this adapter must refuse, and with what message | `conformance/actions.json` — read below, not restated |
|
|
38
|
-
* | the rows a filter returns | `adversarial.test.ts`, against a real store with `check()` as the oracle |
|
|
39
|
-
* | **the filter this adapter emits for a plan** | **here** |
|
|
40
|
-
*
|
|
41
|
-
* **The plans are read, not written.** A hand-built plan is a *belief* about what the planner
|
|
42
|
-
* emits, and this repository keeps golden fixtures because that belief has been wrong before: a
|
|
43
|
-
* planner change used to fail fixture regeneration and silently leave every adapter's hand-written
|
|
44
|
-
* plans describing a wire contract that no longer existed. Sourcing from fixtures inverts that —
|
|
45
|
-
* the drift check now protects the plans this file asserts against. See
|
|
46
|
-
* [ADR 0006](../../docs/adr/0006-translator-unit-tests-take-their-plans-from-wire-fixtures.md).
|
|
47
|
-
*
|
|
48
|
-
* **What a pinned filter buys over the harness.** The harness proves the filter returns the right
|
|
49
|
-
* rows *against the 21 rows it seeds*. Two different filters can agree on all of them and
|
|
50
|
-
* disagree on the row a consumer has, so a rewrite that quietly changes the emitted SQL passes
|
|
51
|
-
* there and shows up here as a diff a reviewer reads. It is also the only place a
|
|
52
|
-
* `nullAttributeRepresentation` boundary, a timestamp literal, or a mapping the corpus cannot
|
|
53
|
-
* express (`subqueryFilter`) can be pinned at all.
|
|
54
|
-
*
|
|
55
|
-
* **Adding a corpus action fails this file.** Every wire fixture must be classified here exactly
|
|
56
|
-
* once — expected filter, expected plan kind, or expected throw — and the guard at the bottom is
|
|
57
|
-
* what makes a new action land as a failure rather than as silence.
|
|
31
|
+
* Which rows a corpus case returns is the conformance harness's job (`adversarial.test.ts`); nothing
|
|
32
|
+
* here pins the filter emitted for a corpus case. Plans come from the current PDP's golden files,
|
|
33
|
+
* read by case id, rather than being typed by hand.
|
|
58
34
|
*/
|
|
59
35
|
|
|
60
|
-
const
|
|
61
|
-
fs.readFileSync(path.join(CONFORMANCE_DIR, "actions.json"), "utf8")
|
|
62
|
-
);
|
|
36
|
+
const CURRENT = pdpTags()[0]!;
|
|
63
37
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* millisecond rather than a container.
|
|
69
|
-
*/
|
|
70
|
-
const { throwingActions: THROWING_ACTIONS } = classifyActionsForAdapter(
|
|
71
|
-
actionsFile,
|
|
72
|
-
"prisma"
|
|
73
|
-
);
|
|
38
|
+
/** The current PDP's recorded plan for a corpus case, `__NOW_MINUS_24H__` filled as `now`. */
|
|
39
|
+
function planFor(id: string, now?: string): Promise<PlanResourcesResponse> {
|
|
40
|
+
return planOf(readGolden(CURRENT, id), now);
|
|
41
|
+
}
|
|
74
42
|
|
|
75
|
-
function translate(
|
|
76
|
-
|
|
43
|
+
async function translate(
|
|
44
|
+
id: string,
|
|
77
45
|
options: {
|
|
78
46
|
mapper?: Mapper;
|
|
79
47
|
nullAttributeRepresentation?: NullAttributeRepresentation;
|
|
80
48
|
} = {}
|
|
81
|
-
): QueryPlanToPrismaResult {
|
|
49
|
+
): Promise<QueryPlanToPrismaResult> {
|
|
82
50
|
return queryPlanToPrisma({
|
|
83
|
-
queryPlan:
|
|
51
|
+
queryPlan: await planFor(id),
|
|
84
52
|
mapper: options.mapper ?? MAPPER,
|
|
85
53
|
model: MODEL,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
: {}),
|
|
54
|
+
// Varied explicitly below; the adapter's own default is pinned under "nullAttributeRepresentation".
|
|
55
|
+
nullAttributeRepresentation: options.nullAttributeRepresentation ?? "explicit",
|
|
89
56
|
});
|
|
90
57
|
}
|
|
91
58
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* `p-has` is `knownDivergences` for every adapter — the planner folds `has(unknown attr)` to
|
|
96
|
-
* ALWAYS_ALLOWED, so the harness cannot compare it against the oracle. Translation is still
|
|
97
|
-
* defined, and pinning it here is the only assertion the corpus makes about the action.
|
|
98
|
-
*/
|
|
99
|
-
const EXPECTED_KINDS: Record<
|
|
100
|
-
string,
|
|
101
|
-
PlanKind.ALWAYS_ALLOWED | PlanKind.ALWAYS_DENIED
|
|
102
|
-
> = {
|
|
103
|
-
"in-empty": PlanKind.ALWAYS_DENIED,
|
|
104
|
-
"p-has": PlanKind.ALWAYS_ALLOWED,
|
|
105
|
-
};
|
|
59
|
+
describe("the refusal type", () => {
|
|
60
|
+
test("an untranslatable shape raises UnsupportedQueryPlanError, which is an Error", async () => {
|
|
61
|
+
let raised: unknown;
|
|
106
62
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* Alphabetical by action, so the diff of a translator change reads as a list of the shapes it
|
|
112
|
-
* moved. Comments are on the entries where the emitted shape is the whole point of the action;
|
|
113
|
-
* the rest are pins.
|
|
114
|
-
*/
|
|
115
|
-
const EXPECTED_FILTERS: Record<string, PrismaFilter> = {
|
|
116
|
-
// The three-valued-logic guard. `every` collapses UNKNOWN to false at the EXISTS boundary, so
|
|
117
|
-
// an element whose `name` is NULL would be silently satisfied; CEL raises a missing-attribute
|
|
118
|
-
// error and check() denies. The second conjunct is what keeps the two aligned, and it is only
|
|
119
|
-
// emitted because the mapper declares `name` nullable.
|
|
120
|
-
"all-on-empty": {
|
|
121
|
-
AND: [
|
|
122
|
-
{ tags: { every: { name: { equals: "public" } } } },
|
|
123
|
-
{ tags: { none: { name: null } } },
|
|
124
|
-
],
|
|
125
|
-
},
|
|
126
|
-
"arith-add": { aNumber: { gt: 1 } },
|
|
127
|
-
"arith-div": { aNumber: { equals: 2 } },
|
|
128
|
-
"arith-div-frac": { aNumber: { gte: 3 } },
|
|
129
|
-
"arith-mult-neg": { OR: [{ aNumber: { gt: -1.5 } }, { aNumber: { gte: -1 } }] },
|
|
130
|
-
"arith-sub": { aNumber: { lte: 3 } },
|
|
131
|
-
// Value-first: the plan reads `K < add(aNumber, K)`, and the mirrored operator is the whole
|
|
132
|
-
// assertion. Emitting `lt` here would invert the comparison — this repository's canonical bug
|
|
133
|
-
// class, and the reason `vf-*` and `arith-vf` pin the same filter as their column-first twins.
|
|
134
|
-
"arith-vf": { aNumber: { gt: 1 } },
|
|
135
|
-
// A constant receiver with a column needle: `K.contains(aString)` cannot become a LIKE, so the
|
|
136
|
-
// adapter enumerates every substring of the constant and tests membership. The enumeration is
|
|
137
|
-
// long, and pinning it verbatim is the point — an off-by-one at either end of the window is
|
|
138
|
-
// invisible in a row comparison over 22 seeds and obvious in this list.
|
|
139
|
-
"cr-contains": {
|
|
140
|
-
aString: {
|
|
141
|
-
in: [
|
|
142
|
-
"",
|
|
143
|
-
"s",
|
|
144
|
-
"s1",
|
|
145
|
-
"s10",
|
|
146
|
-
"s100",
|
|
147
|
-
"s100X",
|
|
148
|
-
"s100Xd",
|
|
149
|
-
"s100Xdo",
|
|
150
|
-
"s100Xdon",
|
|
151
|
-
"s100Xdone",
|
|
152
|
-
"s100Xdone-",
|
|
153
|
-
"s100Xdone-t",
|
|
154
|
-
"s100Xdone-ta",
|
|
155
|
-
"s100Xdone-tai",
|
|
156
|
-
"s100Xdone-tail",
|
|
157
|
-
"s100Xdone-tail\\",
|
|
158
|
-
"s100Xdone-tail\\o",
|
|
159
|
-
"s100Xdone-tail\\on",
|
|
160
|
-
"s100Xdone-tail\\one",
|
|
161
|
-
"s100Xdone-tail\\one-",
|
|
162
|
-
"s100Xdone-tail\\one-e",
|
|
163
|
-
"s100Xdone-tail\\one-en",
|
|
164
|
-
"s100Xdone-tail\\one-end",
|
|
165
|
-
"1",
|
|
166
|
-
"10",
|
|
167
|
-
"100",
|
|
168
|
-
"100X",
|
|
169
|
-
"100Xd",
|
|
170
|
-
"100Xdo",
|
|
171
|
-
"100Xdon",
|
|
172
|
-
"100Xdone",
|
|
173
|
-
"100Xdone-",
|
|
174
|
-
"100Xdone-t",
|
|
175
|
-
"100Xdone-ta",
|
|
176
|
-
"100Xdone-tai",
|
|
177
|
-
"100Xdone-tail",
|
|
178
|
-
"100Xdone-tail\\",
|
|
179
|
-
"100Xdone-tail\\o",
|
|
180
|
-
"100Xdone-tail\\on",
|
|
181
|
-
"100Xdone-tail\\one",
|
|
182
|
-
"100Xdone-tail\\one-",
|
|
183
|
-
"100Xdone-tail\\one-e",
|
|
184
|
-
"100Xdone-tail\\one-en",
|
|
185
|
-
"100Xdone-tail\\one-end",
|
|
186
|
-
"0",
|
|
187
|
-
"00",
|
|
188
|
-
"00X",
|
|
189
|
-
"00Xd",
|
|
190
|
-
"00Xdo",
|
|
191
|
-
"00Xdon",
|
|
192
|
-
"00Xdone",
|
|
193
|
-
"00Xdone-",
|
|
194
|
-
"00Xdone-t",
|
|
195
|
-
"00Xdone-ta",
|
|
196
|
-
"00Xdone-tai",
|
|
197
|
-
"00Xdone-tail",
|
|
198
|
-
"00Xdone-tail\\",
|
|
199
|
-
"00Xdone-tail\\o",
|
|
200
|
-
"00Xdone-tail\\on",
|
|
201
|
-
"00Xdone-tail\\one",
|
|
202
|
-
"00Xdone-tail\\one-",
|
|
203
|
-
"00Xdone-tail\\one-e",
|
|
204
|
-
"00Xdone-tail\\one-en",
|
|
205
|
-
"00Xdone-tail\\one-end",
|
|
206
|
-
"0X",
|
|
207
|
-
"0Xd",
|
|
208
|
-
"0Xdo",
|
|
209
|
-
"0Xdon",
|
|
210
|
-
"0Xdone",
|
|
211
|
-
"0Xdone-",
|
|
212
|
-
"0Xdone-t",
|
|
213
|
-
"0Xdone-ta",
|
|
214
|
-
"0Xdone-tai",
|
|
215
|
-
"0Xdone-tail",
|
|
216
|
-
"0Xdone-tail\\",
|
|
217
|
-
"0Xdone-tail\\o",
|
|
218
|
-
"0Xdone-tail\\on",
|
|
219
|
-
"0Xdone-tail\\one",
|
|
220
|
-
"0Xdone-tail\\one-",
|
|
221
|
-
"0Xdone-tail\\one-e",
|
|
222
|
-
"0Xdone-tail\\one-en",
|
|
223
|
-
"0Xdone-tail\\one-end",
|
|
224
|
-
"X",
|
|
225
|
-
"Xd",
|
|
226
|
-
"Xdo",
|
|
227
|
-
"Xdon",
|
|
228
|
-
"Xdone",
|
|
229
|
-
"Xdone-",
|
|
230
|
-
"Xdone-t",
|
|
231
|
-
"Xdone-ta",
|
|
232
|
-
"Xdone-tai",
|
|
233
|
-
"Xdone-tail",
|
|
234
|
-
"Xdone-tail\\",
|
|
235
|
-
"Xdone-tail\\o",
|
|
236
|
-
"Xdone-tail\\on",
|
|
237
|
-
"Xdone-tail\\one",
|
|
238
|
-
"Xdone-tail\\one-",
|
|
239
|
-
"Xdone-tail\\one-e",
|
|
240
|
-
"Xdone-tail\\one-en",
|
|
241
|
-
"Xdone-tail\\one-end",
|
|
242
|
-
"d",
|
|
243
|
-
"do",
|
|
244
|
-
"don",
|
|
245
|
-
"done",
|
|
246
|
-
"done-",
|
|
247
|
-
"done-t",
|
|
248
|
-
"done-ta",
|
|
249
|
-
"done-tai",
|
|
250
|
-
"done-tail",
|
|
251
|
-
"done-tail\\",
|
|
252
|
-
"done-tail\\o",
|
|
253
|
-
"done-tail\\on",
|
|
254
|
-
"done-tail\\one",
|
|
255
|
-
"done-tail\\one-",
|
|
256
|
-
"done-tail\\one-e",
|
|
257
|
-
"done-tail\\one-en",
|
|
258
|
-
"done-tail\\one-end",
|
|
259
|
-
"o",
|
|
260
|
-
"on",
|
|
261
|
-
"one",
|
|
262
|
-
"one-",
|
|
263
|
-
"one-t",
|
|
264
|
-
"one-ta",
|
|
265
|
-
"one-tai",
|
|
266
|
-
"one-tail",
|
|
267
|
-
"one-tail\\",
|
|
268
|
-
"one-tail\\o",
|
|
269
|
-
"one-tail\\on",
|
|
270
|
-
"one-tail\\one",
|
|
271
|
-
"one-tail\\one-",
|
|
272
|
-
"one-tail\\one-e",
|
|
273
|
-
"one-tail\\one-en",
|
|
274
|
-
"one-tail\\one-end",
|
|
275
|
-
"n",
|
|
276
|
-
"ne",
|
|
277
|
-
"ne-",
|
|
278
|
-
"ne-t",
|
|
279
|
-
"ne-ta",
|
|
280
|
-
"ne-tai",
|
|
281
|
-
"ne-tail",
|
|
282
|
-
"ne-tail\\",
|
|
283
|
-
"ne-tail\\o",
|
|
284
|
-
"ne-tail\\on",
|
|
285
|
-
"ne-tail\\one",
|
|
286
|
-
"ne-tail\\one-",
|
|
287
|
-
"ne-tail\\one-e",
|
|
288
|
-
"ne-tail\\one-en",
|
|
289
|
-
"ne-tail\\one-end",
|
|
290
|
-
"e",
|
|
291
|
-
"e-",
|
|
292
|
-
"e-t",
|
|
293
|
-
"e-ta",
|
|
294
|
-
"e-tai",
|
|
295
|
-
"e-tail",
|
|
296
|
-
"e-tail\\",
|
|
297
|
-
"e-tail\\o",
|
|
298
|
-
"e-tail\\on",
|
|
299
|
-
"e-tail\\one",
|
|
300
|
-
"e-tail\\one-",
|
|
301
|
-
"e-tail\\one-e",
|
|
302
|
-
"e-tail\\one-en",
|
|
303
|
-
"e-tail\\one-end",
|
|
304
|
-
"-",
|
|
305
|
-
"-t",
|
|
306
|
-
"-ta",
|
|
307
|
-
"-tai",
|
|
308
|
-
"-tail",
|
|
309
|
-
"-tail\\",
|
|
310
|
-
"-tail\\o",
|
|
311
|
-
"-tail\\on",
|
|
312
|
-
"-tail\\one",
|
|
313
|
-
"-tail\\one-",
|
|
314
|
-
"-tail\\one-e",
|
|
315
|
-
"-tail\\one-en",
|
|
316
|
-
"-tail\\one-end",
|
|
317
|
-
"t",
|
|
318
|
-
"ta",
|
|
319
|
-
"tai",
|
|
320
|
-
"tail",
|
|
321
|
-
"tail\\",
|
|
322
|
-
"tail\\o",
|
|
323
|
-
"tail\\on",
|
|
324
|
-
"tail\\one",
|
|
325
|
-
"tail\\one-",
|
|
326
|
-
"tail\\one-e",
|
|
327
|
-
"tail\\one-en",
|
|
328
|
-
"tail\\one-end",
|
|
329
|
-
"a",
|
|
330
|
-
"ai",
|
|
331
|
-
"ail",
|
|
332
|
-
"ail\\",
|
|
333
|
-
"ail\\o",
|
|
334
|
-
"ail\\on",
|
|
335
|
-
"ail\\one",
|
|
336
|
-
"ail\\one-",
|
|
337
|
-
"ail\\one-e",
|
|
338
|
-
"ail\\one-en",
|
|
339
|
-
"ail\\one-end",
|
|
340
|
-
"i",
|
|
341
|
-
"il",
|
|
342
|
-
"il\\",
|
|
343
|
-
"il\\o",
|
|
344
|
-
"il\\on",
|
|
345
|
-
"il\\one",
|
|
346
|
-
"il\\one-",
|
|
347
|
-
"il\\one-e",
|
|
348
|
-
"il\\one-en",
|
|
349
|
-
"il\\one-end",
|
|
350
|
-
"l",
|
|
351
|
-
"l\\",
|
|
352
|
-
"l\\o",
|
|
353
|
-
"l\\on",
|
|
354
|
-
"l\\one",
|
|
355
|
-
"l\\one-",
|
|
356
|
-
"l\\one-e",
|
|
357
|
-
"l\\one-en",
|
|
358
|
-
"l\\one-end",
|
|
359
|
-
"\\",
|
|
360
|
-
"\\o",
|
|
361
|
-
"\\on",
|
|
362
|
-
"\\one",
|
|
363
|
-
"\\one-",
|
|
364
|
-
"\\one-e",
|
|
365
|
-
"\\one-en",
|
|
366
|
-
"\\one-end",
|
|
367
|
-
"one-e",
|
|
368
|
-
"one-en",
|
|
369
|
-
"one-end",
|
|
370
|
-
"ne-e",
|
|
371
|
-
"ne-en",
|
|
372
|
-
"ne-end",
|
|
373
|
-
"e-e",
|
|
374
|
-
"e-en",
|
|
375
|
-
"e-end",
|
|
376
|
-
"-e",
|
|
377
|
-
"-en",
|
|
378
|
-
"-end",
|
|
379
|
-
"en",
|
|
380
|
-
"end",
|
|
381
|
-
"nd",
|
|
382
|
-
],
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
"cr-endswith": {
|
|
386
|
-
aString: {
|
|
387
|
-
in: [
|
|
388
|
-
"",
|
|
389
|
-
"prefix-xaXby",
|
|
390
|
-
"refix-xaXby",
|
|
391
|
-
"efix-xaXby",
|
|
392
|
-
"fix-xaXby",
|
|
393
|
-
"ix-xaXby",
|
|
394
|
-
"x-xaXby",
|
|
395
|
-
"-xaXby",
|
|
396
|
-
"xaXby",
|
|
397
|
-
"aXby",
|
|
398
|
-
"Xby",
|
|
399
|
-
"by",
|
|
400
|
-
"y",
|
|
401
|
-
],
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
"cr-startswith": {
|
|
405
|
-
aString: {
|
|
406
|
-
in: [
|
|
407
|
-
"",
|
|
408
|
-
"x",
|
|
409
|
-
"xa",
|
|
410
|
-
"xaX",
|
|
411
|
-
"xaXb",
|
|
412
|
-
"xaXby",
|
|
413
|
-
"xaXby-",
|
|
414
|
-
"xaXby-t",
|
|
415
|
-
"xaXby-ta",
|
|
416
|
-
"xaXby-tai",
|
|
417
|
-
"xaXby-tail",
|
|
418
|
-
],
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
"cr-startswith-concat": {
|
|
422
|
-
aString: {
|
|
423
|
-
in: [
|
|
424
|
-
"",
|
|
425
|
-
"x",
|
|
426
|
-
"xa",
|
|
427
|
-
"xaX",
|
|
428
|
-
"xaXb",
|
|
429
|
-
"xaXby",
|
|
430
|
-
"xaXby-",
|
|
431
|
-
"xaXby-t",
|
|
432
|
-
"xaXby-ta",
|
|
433
|
-
"xaXby-tai",
|
|
434
|
-
"xaXby-tail",
|
|
435
|
-
],
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
"cs-contains": { aString: { contains: "one" } },
|
|
439
|
-
"cs-endswith": { aString: { endsWith: "one" } },
|
|
440
|
-
"cs-eq": { aString: { equals: "one" } },
|
|
441
|
-
"cs-startswith": { aString: { startsWith: "one" } },
|
|
442
|
-
"double-negation": { NOT: { aBool: { equals: false } } },
|
|
443
|
-
"double-threshold": { AND: [{ aNumber: { gte: 1.5 } }, { aNumber: { gt: 1 } }] },
|
|
444
|
-
"empty-string-eq": { aString: { equals: "" } },
|
|
445
|
-
"exists-on-empty": { tags: { some: { name: { equals: "public" } } } },
|
|
446
|
-
"field-to-field": {
|
|
447
|
-
aString: { equals: { _ref: "aOptionalString", _container: "AdversarialResource" } },
|
|
448
|
-
},
|
|
449
|
-
"gt-bare": { aNumber: { gt: 1 } },
|
|
450
|
-
"hier-ancestor-cf": { scope: { startsWith: "dept.eng." } },
|
|
451
|
-
"hier-ancestor-ff": { scope: { in: ["dept", "dept.eng"] } },
|
|
452
|
-
"hier-descendent-cf": { scope: { in: ["dept", "dept.eng"] } },
|
|
453
|
-
"hier-descendent-ff": { scope: { startsWith: "dept.eng." } },
|
|
454
|
-
"hier-list-id": { id: { equals: "f1" } },
|
|
455
|
-
"hier-meta-in": { scope: { in: ["50%", "50%.a_b"] } },
|
|
456
|
-
"hier-overlaps-cf": {
|
|
457
|
-
OR: [
|
|
458
|
-
{ scope: { in: ["dept"] } },
|
|
459
|
-
{ scope: { equals: "dept.eng" } },
|
|
460
|
-
{ scope: { startsWith: "dept.eng." } },
|
|
461
|
-
],
|
|
462
|
-
},
|
|
463
|
-
"hier-overlaps-ff": {
|
|
464
|
-
OR: [
|
|
465
|
-
{ scope: { in: ["dept"] } },
|
|
466
|
-
{ scope: { equals: "dept.eng" } },
|
|
467
|
-
{ scope: { startsWith: "dept.eng." } },
|
|
468
|
-
],
|
|
469
|
-
},
|
|
470
|
-
"id-concat-vf": { id: { equals: "f1" } },
|
|
471
|
-
// The primary key, which arrives as the variable `request.resource.id` rather than through
|
|
472
|
-
// `attr` — the planner leaves the resource's own identity symbolic because PlanResources is
|
|
473
|
-
// asked about a kind, not a row. An adapter that resolves references by stripping a
|
|
474
|
-
// `request.resource.attr.` prefix never reaches this name, and fails silently when it does not.
|
|
475
|
-
"id-eq-const": { id: { equals: "f1" } },
|
|
476
|
-
// `_ref`/`_container` is Prisma's field-reference encoding: comparing two columns needs the
|
|
477
|
-
// model name, which is why `queryPlanToPrisma` takes `model`.
|
|
478
|
-
"id-f2f": { aString: { equals: { _ref: "id", _container: "AdversarialResource" } } },
|
|
479
|
-
"id-f2f-ne": {
|
|
480
|
-
NOT: { aString: { equals: { _ref: "id", _container: "AdversarialResource" } } },
|
|
481
|
-
},
|
|
482
|
-
"in-null-elem-hasint": {
|
|
483
|
-
OR: [{ tags: { some: { name: "public" } } }, { tags: { some: { name: null } } }],
|
|
484
|
-
},
|
|
485
|
-
"in-null-elem-mixed": {
|
|
486
|
-
OR: [{ aOptionalString: { in: ["x", "one_two"] } }, { aOptionalString: null }],
|
|
487
|
-
},
|
|
488
|
-
"in-null-elem-neg": {
|
|
489
|
-
NOT: {
|
|
490
|
-
OR: [{ aOptionalString: { in: ["x", "one_two"] } }, { aOptionalString: null }],
|
|
491
|
-
},
|
|
492
|
-
},
|
|
493
|
-
"in-null-elem-only": { aOptionalString: { equals: null } },
|
|
494
|
-
"in-null-elem-only-neg": { NOT: { aOptionalString: { equals: null } } },
|
|
495
|
-
"in-null-elem-rel": { tags: { some: { name: null } } },
|
|
496
|
-
"in-null-elem-rel-neg": { NOT: { tags: { some: { name: null } } } },
|
|
497
|
-
"in-single": { aString: { equals: "one" } },
|
|
498
|
-
"lambda-in-principal": { tags: { some: { name: { in: ["public", "special"] } } } },
|
|
499
|
-
"le-bare": { aNumber: { lte: 2 } },
|
|
500
|
-
"like-bracket": { aString: { startsWith: "[SEC]" } },
|
|
501
|
-
"macro-depth3-all": {
|
|
502
|
-
categories: {
|
|
503
|
-
some: {
|
|
504
|
-
subCategories: { every: { labels: { some: { name: { equals: "gold" } } } } },
|
|
505
|
-
},
|
|
506
|
-
},
|
|
507
|
-
},
|
|
508
|
-
"macro-depth3-exists": {
|
|
509
|
-
categories: {
|
|
510
|
-
some: {
|
|
511
|
-
subCategories: { some: { labels: { some: { name: { equals: "gold" } } } } },
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
"macro-depth3-not-exists": {
|
|
516
|
-
AND: [
|
|
517
|
-
{
|
|
518
|
-
NOT: {
|
|
519
|
-
categories: {
|
|
520
|
-
some: {
|
|
521
|
-
subCategories: { some: { labels: { some: { name: { equals: "gold" } } } } },
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
NOT: {
|
|
528
|
-
categories: {
|
|
529
|
-
some: {
|
|
530
|
-
AND: [
|
|
531
|
-
{
|
|
532
|
-
NOT: {
|
|
533
|
-
subCategories: { some: { labels: { some: { name: { equals: "gold" } } } } },
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
subCategories: {
|
|
538
|
-
some: {
|
|
539
|
-
AND: [
|
|
540
|
-
{
|
|
541
|
-
NOT: { labels: { some: { name: { equals: "gold" } } } },
|
|
542
|
-
},
|
|
543
|
-
{ labels: { some: { name: null } } },
|
|
544
|
-
],
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
],
|
|
549
|
-
},
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
},
|
|
553
|
-
],
|
|
554
|
-
},
|
|
555
|
-
"n-all-mixed-null": {
|
|
556
|
-
AND: [
|
|
557
|
-
{ tags: { every: { name: { not: "x" } } } },
|
|
558
|
-
{ tags: { none: { name: null } } },
|
|
559
|
-
],
|
|
560
|
-
},
|
|
561
|
-
"n-not-all-absorb": { tags: { some: { NOT: { name: { not: "public" } } } } },
|
|
562
|
-
"n-not-all-null": { tags: { some: { NOT: { name: { equals: "public" } } } } },
|
|
563
|
-
"nan-ord-inf": {
|
|
564
|
-
OR: [
|
|
565
|
-
{ aBool: { equals: true } },
|
|
566
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
567
|
-
],
|
|
568
|
-
},
|
|
569
|
-
"nan-ord-le": {
|
|
570
|
-
OR: [
|
|
571
|
-
{
|
|
572
|
-
AND: [
|
|
573
|
-
{ aBool: { equals: true } },
|
|
574
|
-
{
|
|
575
|
-
OR: [
|
|
576
|
-
{
|
|
577
|
-
AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }],
|
|
578
|
-
},
|
|
579
|
-
],
|
|
580
|
-
},
|
|
581
|
-
],
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
AND: [
|
|
585
|
-
{ aBool: { equals: false } },
|
|
586
|
-
{
|
|
587
|
-
OR: [
|
|
588
|
-
{ aBool: { equals: true } },
|
|
589
|
-
{
|
|
590
|
-
AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }],
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
},
|
|
596
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
597
|
-
],
|
|
598
|
-
},
|
|
599
|
-
"nan-ord-ternary": {
|
|
600
|
-
OR: [
|
|
601
|
-
{ aBool: { equals: true } },
|
|
602
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
603
|
-
],
|
|
604
|
-
},
|
|
605
|
-
"nan-ord-ternary-vf": {
|
|
606
|
-
OR: [
|
|
607
|
-
{ aBool: { equals: true } },
|
|
608
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
609
|
-
],
|
|
610
|
-
},
|
|
611
|
-
"nary-and": {
|
|
612
|
-
AND: [
|
|
613
|
-
{ aBool: { equals: true } },
|
|
614
|
-
{ aNumber: { gte: 0 } },
|
|
615
|
-
{ aString: { not: "one" } },
|
|
616
|
-
],
|
|
617
|
-
},
|
|
618
|
-
"neg-number": { aNumber: { lt: -1 } },
|
|
619
|
-
// The De Morgan branch. `NOT` over a conjunction was unreachable from the corpus until #387,
|
|
620
|
-
// and it is the same wire shape a DENY-composed `nand` rule produces — byte-identical
|
|
621
|
-
// `filter.condition` JSON, which is why the DENY spelling ported as a delete.
|
|
622
|
-
"not-and": { NOT: { AND: [{ aBool: { equals: true } }, { aString: { not: "one" } }] } },
|
|
623
|
-
"not-empty": { NOT: { tags: { none: {} } } },
|
|
624
|
-
"not-exists": {
|
|
625
|
-
AND: [
|
|
626
|
-
{ NOT: { tags: { some: { name: { equals: "private" } } } } },
|
|
627
|
-
{ NOT: { tags: { some: { name: null } } } },
|
|
628
|
-
],
|
|
629
|
-
},
|
|
630
|
-
"not-gt": { NOT: { aNumber: { gt: 1 } } },
|
|
631
|
-
"not-lt": { NOT: { aNumber: { lt: 2 } } },
|
|
632
|
-
"null-eq": { aOptionalString: { equals: null } },
|
|
633
|
-
"null-eq-missing": { aOptionalString: { equals: null } },
|
|
634
|
-
"null-ne": { aOptionalString: { not: null } },
|
|
635
|
-
"null-not-eq": { NOT: { aOptionalString: { equals: null } } },
|
|
636
|
-
"null-value-f2f": {
|
|
637
|
-
OR: [
|
|
638
|
-
{ AND: [{ aOptionalString: null }, { scope: null }] },
|
|
639
|
-
{
|
|
640
|
-
AND: [
|
|
641
|
-
{ aOptionalString: { not: null } },
|
|
642
|
-
{ scope: { not: null } },
|
|
643
|
-
{
|
|
644
|
-
aOptionalString: { equals: { _ref: "scope", _container: "AdversarialResource" } },
|
|
645
|
-
},
|
|
646
|
-
],
|
|
647
|
-
},
|
|
648
|
-
],
|
|
649
|
-
},
|
|
650
|
-
"null-value-ne-const": {
|
|
651
|
-
NOT: {
|
|
652
|
-
AND: [{ aOptionalString: { not: null } }, { aOptionalString: { equals: "x" } }],
|
|
653
|
-
},
|
|
654
|
-
},
|
|
655
|
-
// Under the explicit-null convention CEL holds a null VALUE, so `null != "x"` is TRUE and
|
|
656
|
-
// `null == "x"` is FALSE — both definite. Prisma's bare `{ not: "x" }` drops the row under both
|
|
657
|
-
// polarities, so the equality family is rendered so it can never depend on SQL's UNKNOWN. The
|
|
658
|
-
// `not: null` conjunct is that rendering, and it is what the `owner`/`coOwner` declarations in
|
|
659
|
-
// the mapper buy (cerbos/query-plan-adapters#308).
|
|
660
|
-
"null-value-not-eq-const": {
|
|
661
|
-
NOT: {
|
|
662
|
-
AND: [{ aOptionalString: { not: null } }, { aOptionalString: { equals: "x" } }],
|
|
663
|
-
},
|
|
664
|
-
},
|
|
665
|
-
"null-value-not-in-const": {
|
|
666
|
-
NOT: {
|
|
667
|
-
AND: [
|
|
668
|
-
{ aOptionalString: { not: null } },
|
|
669
|
-
{ aOptionalString: { in: ["x", "one_two"] } },
|
|
670
|
-
],
|
|
671
|
-
},
|
|
672
|
-
},
|
|
673
|
-
"null-value-pv-not-exists": {
|
|
674
|
-
AND: [
|
|
675
|
-
{
|
|
676
|
-
NOT: {
|
|
677
|
-
AND: [
|
|
678
|
-
{ aOptionalString: { not: null } },
|
|
679
|
-
{ aOptionalString: { equals: "set" } },
|
|
680
|
-
],
|
|
681
|
-
},
|
|
682
|
-
},
|
|
683
|
-
{
|
|
684
|
-
NOT: {
|
|
685
|
-
AND: [
|
|
686
|
-
{ aOptionalString: { not: null } },
|
|
687
|
-
{ aOptionalString: { equals: "same" } },
|
|
688
|
-
],
|
|
689
|
-
},
|
|
690
|
-
},
|
|
691
|
-
{
|
|
692
|
-
NOT: {
|
|
693
|
-
AND: [
|
|
694
|
-
{ aOptionalString: { not: null } },
|
|
695
|
-
{ aOptionalString: { equals: "" } },
|
|
696
|
-
],
|
|
697
|
-
},
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
NOT: {
|
|
701
|
-
AND: [
|
|
702
|
-
{ aOptionalString: { not: null } },
|
|
703
|
-
{ aOptionalString: { equals: "%_o" } },
|
|
704
|
-
],
|
|
705
|
-
},
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
NOT: {
|
|
709
|
-
AND: [
|
|
710
|
-
{ aOptionalString: { not: null } },
|
|
711
|
-
{ aOptionalString: { equals: "X" } },
|
|
712
|
-
],
|
|
713
|
-
},
|
|
714
|
-
},
|
|
715
|
-
{
|
|
716
|
-
NOT: {
|
|
717
|
-
AND: [
|
|
718
|
-
{ aOptionalString: { not: null } },
|
|
719
|
-
{ aOptionalString: { equals: "Y" } },
|
|
720
|
-
],
|
|
721
|
-
},
|
|
722
|
-
},
|
|
723
|
-
{
|
|
724
|
-
NOT: {
|
|
725
|
-
AND: [
|
|
726
|
-
{ aOptionalString: { not: null } },
|
|
727
|
-
{ aOptionalString: { equals: "MIRROR" } },
|
|
728
|
-
],
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
|
-
{
|
|
732
|
-
NOT: {
|
|
733
|
-
AND: [
|
|
734
|
-
{ aOptionalString: { not: null } },
|
|
735
|
-
{ aOptionalString: { equals: "filler-1" } },
|
|
736
|
-
],
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
NOT: {
|
|
741
|
-
AND: [
|
|
742
|
-
{ aOptionalString: { not: null } },
|
|
743
|
-
{ aOptionalString: { equals: "filler-2" } },
|
|
744
|
-
],
|
|
745
|
-
},
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
NOT: {
|
|
749
|
-
AND: [
|
|
750
|
-
{ aOptionalString: { not: null } },
|
|
751
|
-
{ aOptionalString: { equals: "filler-3" } },
|
|
752
|
-
],
|
|
753
|
-
},
|
|
754
|
-
},
|
|
755
|
-
{
|
|
756
|
-
NOT: {
|
|
757
|
-
AND: [
|
|
758
|
-
{ aOptionalString: { not: null } },
|
|
759
|
-
{ aOptionalString: { equals: "filler-4" } },
|
|
760
|
-
],
|
|
761
|
-
},
|
|
762
|
-
},
|
|
763
|
-
],
|
|
764
|
-
},
|
|
765
|
-
"optional-ne": { aOptionalString: { not: "x" } },
|
|
766
|
-
"or-eq-exists": {
|
|
767
|
-
OR: [
|
|
768
|
-
{ aBool: { equals: true } },
|
|
769
|
-
{ tags: { some: { name: { equals: "public" } } } },
|
|
770
|
-
],
|
|
771
|
-
},
|
|
772
|
-
"or-eq-in": { OR: [{ aBool: { equals: true } }, { tags: { some: { name: "public" } } }] },
|
|
773
|
-
"outer-attr-depth2": {
|
|
774
|
-
AND: [
|
|
775
|
-
{
|
|
776
|
-
categories: { some: { subCategories: { some: { name: { equals: "finance" } } } } },
|
|
777
|
-
},
|
|
778
|
-
{ aBool: { equals: true } },
|
|
779
|
-
],
|
|
780
|
-
},
|
|
781
|
-
"p-arith-in-lambda": {
|
|
782
|
-
AND: [{ tags: { some: { name: { equals: "public" } } } }, { aNumber: { gt: 1 } }],
|
|
783
|
-
},
|
|
784
|
-
"p-double-frac": {
|
|
785
|
-
AND: [
|
|
786
|
-
{ aNumber: { equals: 2.9999999999999996 } },
|
|
787
|
-
{ aNumber: { gt: 2 } },
|
|
788
|
-
{ aNumber: { lt: 3 } },
|
|
789
|
-
],
|
|
790
|
-
},
|
|
791
|
-
"p-hasintersection-map": {
|
|
792
|
-
AND: [
|
|
793
|
-
{
|
|
794
|
-
tags: {
|
|
795
|
-
some: { name: { in: ["public", "h\u00e9llo\ud83d\ude80", "100%_x"] } },
|
|
796
|
-
},
|
|
797
|
-
},
|
|
798
|
-
{ NOT: { tags: { some: { name: null } } } },
|
|
799
|
-
],
|
|
800
|
-
},
|
|
801
|
-
"p-in-null-multi": { aOptionalString: { in: ["x", "one_two"] } },
|
|
802
|
-
"p-in-null-single": { aOptionalString: { equals: "x" } },
|
|
803
|
-
"p-lambda-inner-f2f": {
|
|
804
|
-
tags: {
|
|
805
|
-
some: { tagId: { equals: { _ref: "name", _container: "AdversarialTag" } } },
|
|
806
|
-
},
|
|
807
|
-
},
|
|
808
|
-
"p-not-exists-empty": {
|
|
809
|
-
AND: [
|
|
810
|
-
{ NOT: { tags: { some: { name: { equals: "public" } } } } },
|
|
811
|
-
{ NOT: { tags: { some: { name: null } } } },
|
|
812
|
-
],
|
|
813
|
-
},
|
|
814
|
-
"p-not-ternary-null": {
|
|
815
|
-
NOT: {
|
|
816
|
-
OR: [
|
|
817
|
-
{ AND: [{ aOptionalString: { not: "x" } }, { aNumber: { gt: 1 } }] },
|
|
818
|
-
{
|
|
819
|
-
AND: [
|
|
820
|
-
{ aOptionalString: { not: "x" } },
|
|
821
|
-
{ NOT: { aOptionalString: { not: "x" } } },
|
|
822
|
-
],
|
|
823
|
-
},
|
|
824
|
-
],
|
|
825
|
-
},
|
|
826
|
-
},
|
|
827
|
-
"p-struct": { aString: { equals: "one" } },
|
|
828
|
-
"p-ternary-in-exists": {
|
|
829
|
-
OR: [
|
|
830
|
-
{
|
|
831
|
-
AND: [
|
|
832
|
-
{ aBool: { equals: true } },
|
|
833
|
-
{ tags: { some: { name: { equals: "public" } } } },
|
|
834
|
-
],
|
|
835
|
-
},
|
|
836
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
837
|
-
],
|
|
838
|
-
},
|
|
839
|
-
"p-ternary-of-ternaries": {
|
|
840
|
-
OR: [
|
|
841
|
-
{
|
|
842
|
-
AND: [
|
|
843
|
-
{ aBool: { equals: true } },
|
|
844
|
-
{
|
|
845
|
-
OR: [
|
|
846
|
-
{
|
|
847
|
-
AND: [{ NOT: { aString: { equals: "" } } }, { aNumber: { gt: 1 } }],
|
|
848
|
-
},
|
|
849
|
-
{
|
|
850
|
-
AND: [
|
|
851
|
-
{ aString: { equals: "" } },
|
|
852
|
-
{ NOT: { aString: { equals: "" } } },
|
|
853
|
-
],
|
|
854
|
-
},
|
|
855
|
-
],
|
|
856
|
-
},
|
|
857
|
-
],
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
AND: [
|
|
861
|
-
{ aBool: { equals: false } },
|
|
862
|
-
{
|
|
863
|
-
OR: [
|
|
864
|
-
{ NOT: { aNumber: { lt: 0 } } },
|
|
865
|
-
{ AND: [{ aNumber: { lt: 0 } }, { NOT: { aNumber: { lt: 0 } } }] },
|
|
866
|
-
],
|
|
867
|
-
},
|
|
868
|
-
],
|
|
869
|
-
},
|
|
870
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
871
|
-
],
|
|
872
|
-
},
|
|
873
|
-
"p-ternary-under-all": {
|
|
874
|
-
OR: [
|
|
875
|
-
{
|
|
876
|
-
AND: [
|
|
877
|
-
{ aBool: { equals: true } },
|
|
878
|
-
{
|
|
879
|
-
AND: [
|
|
880
|
-
{ tags: { every: { name: { not: "private" } } } },
|
|
881
|
-
{ tags: { none: { name: null } } },
|
|
882
|
-
],
|
|
883
|
-
},
|
|
884
|
-
],
|
|
885
|
-
},
|
|
886
|
-
{ aBool: { equals: false } },
|
|
887
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
888
|
-
],
|
|
889
|
-
},
|
|
890
|
-
"p-ternary-vs-ternary": {
|
|
891
|
-
OR: [
|
|
892
|
-
{
|
|
893
|
-
AND: [
|
|
894
|
-
{ aBool: { equals: true } },
|
|
895
|
-
{
|
|
896
|
-
OR: [
|
|
897
|
-
{ AND: [{ aString: { equals: "" } }, { aNumber: { gt: 1 } }] },
|
|
898
|
-
{
|
|
899
|
-
AND: [{ NOT: { aString: { equals: "" } } }, { aNumber: { gt: 2 } }],
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
AND: [
|
|
903
|
-
{ aString: { equals: "" } },
|
|
904
|
-
{ NOT: { aString: { equals: "" } } },
|
|
905
|
-
],
|
|
906
|
-
},
|
|
907
|
-
],
|
|
908
|
-
},
|
|
909
|
-
],
|
|
910
|
-
},
|
|
911
|
-
{
|
|
912
|
-
AND: [
|
|
913
|
-
{ aBool: { equals: false } },
|
|
914
|
-
{
|
|
915
|
-
OR: [
|
|
916
|
-
{
|
|
917
|
-
AND: [
|
|
918
|
-
{ aString: { equals: "" } },
|
|
919
|
-
{ NOT: { aString: { equals: "" } } },
|
|
920
|
-
],
|
|
921
|
-
},
|
|
922
|
-
],
|
|
923
|
-
},
|
|
924
|
-
],
|
|
925
|
-
},
|
|
926
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
927
|
-
],
|
|
928
|
-
},
|
|
929
|
-
"pv-all": {
|
|
930
|
-
AND: [
|
|
931
|
-
{ aOptionalString: { not: "set" } },
|
|
932
|
-
{ aOptionalString: { not: "same" } },
|
|
933
|
-
{ aOptionalString: { not: "" } },
|
|
934
|
-
{ aOptionalString: { not: "%_o" } },
|
|
935
|
-
{ aOptionalString: { not: "X" } },
|
|
936
|
-
{ aOptionalString: { not: "Y" } },
|
|
937
|
-
{ aOptionalString: { not: "MIRROR" } },
|
|
938
|
-
{ aOptionalString: { not: "filler-1" } },
|
|
939
|
-
{ aOptionalString: { not: "filler-2" } },
|
|
940
|
-
{ aOptionalString: { not: "filler-3" } },
|
|
941
|
-
{ aOptionalString: { not: "filler-4" } },
|
|
942
|
-
],
|
|
943
|
-
},
|
|
944
|
-
"pv-all-unrolled": {
|
|
945
|
-
AND: [
|
|
946
|
-
{ aOptionalString: { not: "set" } },
|
|
947
|
-
{
|
|
948
|
-
AND: [{ aOptionalString: { not: "" } }, { aOptionalString: { not: "%_o" } }],
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
},
|
|
952
|
-
"pv-exists": {
|
|
953
|
-
OR: [
|
|
954
|
-
{ aOptionalString: { equals: "set" } },
|
|
955
|
-
{ aOptionalString: { equals: "same" } },
|
|
956
|
-
{ aOptionalString: { equals: "" } },
|
|
957
|
-
{ aOptionalString: { equals: "%_o" } },
|
|
958
|
-
{ aOptionalString: { equals: "X" } },
|
|
959
|
-
{ aOptionalString: { equals: "Y" } },
|
|
960
|
-
{ aOptionalString: { equals: "MIRROR" } },
|
|
961
|
-
{ aOptionalString: { equals: "filler-1" } },
|
|
962
|
-
{ aOptionalString: { equals: "filler-2" } },
|
|
963
|
-
{ aOptionalString: { equals: "filler-3" } },
|
|
964
|
-
{ aOptionalString: { equals: "filler-4" } },
|
|
965
|
-
],
|
|
966
|
-
},
|
|
967
|
-
"pv-exists-unrolled": {
|
|
968
|
-
OR: [
|
|
969
|
-
{ aOptionalString: { equals: "set" } },
|
|
970
|
-
{
|
|
971
|
-
OR: [
|
|
972
|
-
{ aOptionalString: { equals: "" } },
|
|
973
|
-
{ aOptionalString: { equals: "%_o" } },
|
|
974
|
-
],
|
|
975
|
-
},
|
|
976
|
-
],
|
|
977
|
-
},
|
|
978
|
-
"rel-bool-hop": { parent: { is: { aBool: { equals: true } } } },
|
|
979
|
-
"rel-bool-hop2": { parent: { is: { inner: { is: { aBool: { equals: true } } } } } },
|
|
980
|
-
"rel-contains-hop": { parent: { is: { aString: { contains: "done" } } } },
|
|
981
|
-
"rel-eq-hop": { parent: { is: { aString: { equals: "One" } } } },
|
|
982
|
-
"rel-eq-num-hop": { parent: { is: { aNumber: { equals: 2 } } } },
|
|
983
|
-
"rel-ge-hop": { parent: { is: { aNumber: { gte: 2 } } } },
|
|
984
|
-
"rel-gt-hop": { parent: { is: { aNumber: { gt: 2 } } } },
|
|
985
|
-
"rel-hop-and-root": {
|
|
986
|
-
AND: [
|
|
987
|
-
{ aBool: { equals: true } },
|
|
988
|
-
{ parent: { is: { aString: { contains: "re" } } } },
|
|
989
|
-
],
|
|
990
|
-
},
|
|
991
|
-
"rel-hop2-or-exists": {
|
|
992
|
-
OR: [
|
|
993
|
-
{ parent: { is: { inner: { is: { aBool: { equals: true } } } } } },
|
|
994
|
-
{ categories: { some: { name: { equals: "business" } } } },
|
|
995
|
-
],
|
|
996
|
-
},
|
|
997
|
-
"rel-le-hop": { parent: { is: { aNumber: { lte: 2 } } } },
|
|
998
|
-
"rel-lt-hop": { parent: { is: { aNumber: { lt: 2 } } } },
|
|
999
|
-
"rel-ne-null-hop": { parent: { is: { aOptionalString: { not: null } } } },
|
|
1000
|
-
// The absent-parent guard. `NOT { parent: { is: P } }` is satisfied by a row with no parent at
|
|
1001
|
-
// all, while CEL raises a missing-attribute error on the hop and check() denies. The first
|
|
1002
|
-
// conjunct — an existence test on the hop itself — is what keeps a negated shape over an
|
|
1003
|
-
// OPTIONAL to-one relation from over-granting. The positive shapes cannot discriminate it,
|
|
1004
|
-
// which is why the corpus carries the negated ones (ADR 0005).
|
|
1005
|
-
"rel-not-bool-hop": {
|
|
1006
|
-
AND: [
|
|
1007
|
-
{ parent: { is: {} } },
|
|
1008
|
-
{ NOT: { parent: { is: { aBool: { equals: true } } } } },
|
|
1009
|
-
],
|
|
1010
|
-
},
|
|
1011
|
-
"rel-range-hop": {
|
|
1012
|
-
AND: [
|
|
1013
|
-
{ parent: { is: { aNumber: { gt: 2 } } } },
|
|
1014
|
-
{ parent: { is: { aNumber: { lt: 12 } } } },
|
|
1015
|
-
],
|
|
1016
|
-
},
|
|
1017
|
-
"rel-startswith-hop2": { parent: { is: { inner: { is: { aString: { startsWith: "100" } } } } } },
|
|
1018
|
-
"root-bare-bool": { aBool: { equals: true } },
|
|
1019
|
-
"root-or": { OR: [{ aBool: { equals: true } }, { aNumber: { lt: 0 } }] },
|
|
1020
|
-
"ternary-bare": {
|
|
1021
|
-
OR: [
|
|
1022
|
-
{ AND: [{ aBool: { equals: true } }, { aString: { equals: "one" } }] },
|
|
1023
|
-
{ AND: [{ aBool: { equals: false } }, { aNumber: { lt: 0 } }] },
|
|
1024
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
1025
|
-
],
|
|
1026
|
-
},
|
|
1027
|
-
"ternary-cmp": {
|
|
1028
|
-
OR: [
|
|
1029
|
-
{ AND: [{ aBool: { equals: true } }, { aNumber: { gt: 1 } }] },
|
|
1030
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
1031
|
-
],
|
|
1032
|
-
},
|
|
1033
|
-
"ternary-expr-cond": {
|
|
1034
|
-
OR: [
|
|
1035
|
-
{ AND: [{ aString: { startsWith: "100" } }, { aNumber: { gte: 0 } }] },
|
|
1036
|
-
{
|
|
1037
|
-
AND: [
|
|
1038
|
-
{ aString: { startsWith: "100" } },
|
|
1039
|
-
{ NOT: { aString: { startsWith: "100" } } },
|
|
1040
|
-
],
|
|
1041
|
-
},
|
|
1042
|
-
],
|
|
1043
|
-
},
|
|
1044
|
-
"ternary-negated": {
|
|
1045
|
-
NOT: {
|
|
1046
|
-
OR: [
|
|
1047
|
-
{ AND: [{ aBool: { equals: true } }, { aNumber: { gt: 1 } }] },
|
|
1048
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
1049
|
-
],
|
|
1050
|
-
},
|
|
1051
|
-
},
|
|
1052
|
-
"ternary-nested": {
|
|
1053
|
-
OR: [
|
|
1054
|
-
{
|
|
1055
|
-
AND: [
|
|
1056
|
-
{ aBool: { equals: true } },
|
|
1057
|
-
{
|
|
1058
|
-
OR: [
|
|
1059
|
-
{
|
|
1060
|
-
AND: [{ NOT: { aString: { equals: "" } } }, { aNumber: { gte: 2 } }],
|
|
1061
|
-
},
|
|
1062
|
-
{
|
|
1063
|
-
AND: [
|
|
1064
|
-
{ aString: { equals: "" } },
|
|
1065
|
-
{ NOT: { aString: { equals: "" } } },
|
|
1066
|
-
],
|
|
1067
|
-
},
|
|
1068
|
-
],
|
|
1069
|
-
},
|
|
1070
|
-
],
|
|
1071
|
-
},
|
|
1072
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
1073
|
-
],
|
|
1074
|
-
},
|
|
1075
|
-
"ternary-null-cond": {
|
|
1076
|
-
OR: [
|
|
1077
|
-
{ AND: [{ aOptionalString: { not: "x" } }, { aNumber: { gt: 1 } }] },
|
|
1078
|
-
{
|
|
1079
|
-
AND: [
|
|
1080
|
-
{ aOptionalString: { not: "x" } },
|
|
1081
|
-
{ NOT: { aOptionalString: { not: "x" } } },
|
|
1082
|
-
],
|
|
1083
|
-
},
|
|
1084
|
-
],
|
|
1085
|
-
},
|
|
1086
|
-
"ternary-value-first": {
|
|
1087
|
-
OR: [
|
|
1088
|
-
{ AND: [{ aBool: { equals: true } }, { aNumber: { gt: 0 } }] },
|
|
1089
|
-
{ AND: [{ aBool: { equals: true } }, { aBool: { equals: false } }] },
|
|
1090
|
-
],
|
|
1091
|
-
},
|
|
1092
|
-
"triple-negation": { NOT: { NOT: { aBool: { equals: false } } } },
|
|
1093
|
-
"ts-eq": { createdAt: { equals: "2024-06-01T00:00:00.000Z" } },
|
|
1094
|
-
"ts-eq-offset": { createdAt: { equals: "2024-06-01T00:00:00.000Z" } },
|
|
1095
|
-
"ts-ne": { createdAt: { not: "2024-06-01T00:00:00.000Z" } },
|
|
1096
|
-
"unicode-eq": { aString: { equals: "h\u00e9llo\ud83d\ude80" } },
|
|
1097
|
-
"vf-ge": { aNumber: { lte: 2 } },
|
|
1098
|
-
"vf-hasint": { tags: { some: { name: { in: ["public", "other"] } } } },
|
|
1099
|
-
"vf-le": { aNumber: { gte: 3 } },
|
|
1100
|
-
"vf-lt": { aNumber: { gt: 1 } },
|
|
1101
|
-
"vf-ne": { aString: { not: "one" } },
|
|
1102
|
-
"vf-null-ne": { aOptionalString: { not: null } },
|
|
1103
|
-
"vf-size": { tags: { some: {} } },
|
|
1104
|
-
"w1-exists-chain": {
|
|
1105
|
-
categories: { some: { subCategories: { some: { name: { equals: "finance" } } } } },
|
|
1106
|
-
},
|
|
1107
|
-
"w1-in-chain": { categories: { some: { subCategories: { some: { name: "finance" } } } } },
|
|
1108
|
-
// The same absent-hop guard as `rel-not-bool-hop`, one level up a multi-hop chain: the negation
|
|
1109
|
-
// is joined through every intermediate relation, never off the root, and the leading existence
|
|
1110
|
-
// test is what stops a row with no category at all from satisfying the negation.
|
|
1111
|
-
"w1-not-exists-chain": {
|
|
1112
|
-
AND: [
|
|
1113
|
-
{ categories: { some: {} } },
|
|
1114
|
-
{
|
|
1115
|
-
NOT: {
|
|
1116
|
-
categories: { some: { subCategories: { some: { name: { equals: "finance" } } } } },
|
|
1117
|
-
},
|
|
1118
|
-
},
|
|
1119
|
-
],
|
|
1120
|
-
},
|
|
1121
|
-
"w1-not-hasint-chain": {
|
|
1122
|
-
AND: [
|
|
1123
|
-
{ categories: { some: {} } },
|
|
1124
|
-
{
|
|
1125
|
-
NOT: {
|
|
1126
|
-
categories: { some: { subCategories: { some: { name: "finance" } } } },
|
|
1127
|
-
},
|
|
1128
|
-
},
|
|
1129
|
-
],
|
|
1130
|
-
},
|
|
1131
|
-
"w1-not-in-chain": {
|
|
1132
|
-
AND: [
|
|
1133
|
-
{ categories: { some: {} } },
|
|
1134
|
-
{
|
|
1135
|
-
NOT: {
|
|
1136
|
-
categories: { some: { subCategories: { some: { name: "finance" } } } },
|
|
1137
|
-
},
|
|
1138
|
-
},
|
|
1139
|
-
],
|
|
1140
|
-
},
|
|
1141
|
-
"w1-not-size-chain": {
|
|
1142
|
-
AND: [
|
|
1143
|
-
{ categories: { some: {} } },
|
|
1144
|
-
{ NOT: { categories: { some: { subCategories: { some: {} } } } } },
|
|
1145
|
-
],
|
|
1146
|
-
},
|
|
1147
|
-
"w1-size-chain": { categories: { some: { subCategories: { some: {} } } } },
|
|
1148
|
-
"w1-size-zero-chain": { categories: { some: { subCategories: { none: {} } } } },
|
|
1149
|
-
"w1-ternary-chain-cond": {
|
|
1150
|
-
OR: [
|
|
1151
|
-
{
|
|
1152
|
-
AND: [
|
|
1153
|
-
{
|
|
1154
|
-
categories: { some: { subCategories: { some: { name: "finance" } } } },
|
|
1155
|
-
},
|
|
1156
|
-
{ aBool: { equals: true } },
|
|
1157
|
-
],
|
|
1158
|
-
},
|
|
1159
|
-
{
|
|
1160
|
-
AND: [
|
|
1161
|
-
{
|
|
1162
|
-
AND: [
|
|
1163
|
-
{ categories: { some: {} } },
|
|
1164
|
-
{
|
|
1165
|
-
NOT: {
|
|
1166
|
-
categories: { some: { subCategories: { some: { name: "finance" } } } },
|
|
1167
|
-
},
|
|
1168
|
-
},
|
|
1169
|
-
],
|
|
1170
|
-
},
|
|
1171
|
-
{ aBool: { equals: false } },
|
|
1172
|
-
],
|
|
1173
|
-
},
|
|
1174
|
-
{
|
|
1175
|
-
AND: [
|
|
1176
|
-
{
|
|
1177
|
-
categories: { some: { subCategories: { some: { name: "finance" } } } },
|
|
1178
|
-
},
|
|
1179
|
-
{
|
|
1180
|
-
AND: [
|
|
1181
|
-
{ categories: { some: {} } },
|
|
1182
|
-
{
|
|
1183
|
-
NOT: {
|
|
1184
|
-
categories: { some: { subCategories: { some: { name: "finance" } } } },
|
|
1185
|
-
},
|
|
1186
|
-
},
|
|
1187
|
-
],
|
|
1188
|
-
},
|
|
1189
|
-
],
|
|
1190
|
-
},
|
|
1191
|
-
],
|
|
1192
|
-
},
|
|
1193
|
-
};
|
|
1194
|
-
|
|
1195
|
-
describe("corpus shapes", () => {
|
|
1196
|
-
test.each(Object.entries(EXPECTED_FILTERS))(
|
|
1197
|
-
"%s emits the pinned filter",
|
|
1198
|
-
(action, filters) => {
|
|
1199
|
-
expect(translate(action)).toStrictEqual({
|
|
1200
|
-
kind: PlanKind.CONDITIONAL,
|
|
1201
|
-
filters,
|
|
1202
|
-
});
|
|
63
|
+
try {
|
|
64
|
+
await translate("collection/index/first-element-of-string-list");
|
|
65
|
+
} catch (error) {
|
|
66
|
+
raised = error;
|
|
1203
67
|
}
|
|
1204
|
-
);
|
|
1205
68
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
(
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
);
|
|
69
|
+
expect(raised).toBeInstanceOf(UnsupportedQueryPlanError);
|
|
70
|
+
expect(raised).toBeInstanceOf(Error);
|
|
71
|
+
expect(raised).toMatchObject({ name: "UnsupportedQueryPlanError" });
|
|
72
|
+
});
|
|
1212
73
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
74
|
+
test("mapper misconfiguration is a plain Error, not a refusal", async () => {
|
|
75
|
+
const queryPlan = await planFor("comparison/equals/field-to-field");
|
|
76
|
+
let raised: unknown;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
queryPlanToPrisma({ queryPlan, mapper: MAPPER });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
raised = error;
|
|
1221
82
|
}
|
|
1222
|
-
);
|
|
1223
83
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
// Tripwires. Bump them deliberately: a count that moves without anyone noticing is how a
|
|
1241
|
-
// shape gets dropped from a table nobody reads end to end.
|
|
1242
|
-
expect({
|
|
1243
|
-
filters: filters.length,
|
|
1244
|
-
kinds: kinds.length,
|
|
1245
|
-
throwing: throwing.length,
|
|
1246
|
-
}).toEqual({ filters: 136, kinds: 2, throwing: 61 });
|
|
84
|
+
expect(raised).toBeInstanceOf(Error);
|
|
85
|
+
expect(raised).not.toBeInstanceOf(UnsupportedQueryPlanError);
|
|
86
|
+
expect(raised).toHaveProperty("message", expect.stringContaining("requires the `model` option"));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("declared scalar types", () => {
|
|
91
|
+
test("an undeclared type preserves the legacy mapper contract", async () => {
|
|
92
|
+
expect(
|
|
93
|
+
await translate("type-mismatch/equals/string-field-against-number-principal", {
|
|
94
|
+
mapper: { "request.resource.attr.aString": { field: "aString" } },
|
|
95
|
+
}),
|
|
96
|
+
).toEqual({
|
|
97
|
+
kind: PlanKind.CONDITIONAL,
|
|
98
|
+
filters: { aString: { equals: 0 } },
|
|
99
|
+
});
|
|
1247
100
|
});
|
|
1248
101
|
});
|
|
1249
102
|
|
|
103
|
+
/** The shared mapper with every per-attribute null declaration stripped. */
|
|
104
|
+
const UNDECLARED: Record<string, MapperConfig> = Object.fromEntries(
|
|
105
|
+
Object.entries(MAPPER).map(([reference, config]) => {
|
|
106
|
+
const { nullAttributeRepresentation: _stripped, ...rest } = config;
|
|
107
|
+
|
|
108
|
+
return [reference, rest];
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/** Whether any operand anywhere in the wire plan is a literal null, or a list containing one. */
|
|
113
|
+
function carriesNullLiteral(node: JsonValue | undefined): boolean {
|
|
114
|
+
if (Array.isArray(node)) return node.some(carriesNullLiteral);
|
|
115
|
+
|
|
116
|
+
if (!isObject(node)) return false;
|
|
117
|
+
const record = node;
|
|
118
|
+
|
|
119
|
+
if ("value" in record) {
|
|
120
|
+
const value = record["value"];
|
|
121
|
+
|
|
122
|
+
return value === null || (Array.isArray(value) && value.includes(null));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Object.values(record).some((child) =>
|
|
126
|
+
Array.isArray(child) ? child.some(carriesNullLiteral) : carriesNullLiteral(child)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
1250
130
|
describe("nullAttributeRepresentation", () => {
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
|
|
131
|
+
// `== null` against an attribute the caller OMITS when the column is NULL. The two conventions
|
|
132
|
+
// are indistinguishable on the wire — the planner emits the same `eq(attr, null)` either way — so
|
|
133
|
+
// the adapter has to be told. The corpus mapping declares it per attribute; these vary the
|
|
134
|
+
// call-level option and the declaration, which the corpus cannot.
|
|
135
|
+
const MISSING = "null/equals/null-literal-on-missing-attribute";
|
|
136
|
+
const NEVER = { kind: PlanKind.CONDITIONAL, filters: { NOT: { aOptionalString: { startsWith: "" } } } };
|
|
137
|
+
|
|
138
|
+
test("unset: the default is omitted, so == null selects no row", async () => {
|
|
139
|
+
// The default is the convention whose wrong guess fails closed (ADR 0011).
|
|
1256
140
|
expect(
|
|
1257
|
-
|
|
1258
|
-
).
|
|
141
|
+
queryPlanToPrisma({ queryPlan: await planFor(MISSING), mapper: UNDECLARED, model: MODEL })
|
|
142
|
+
).toEqual(NEVER);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("explicit: == null is an IS NULL filter", async () => {
|
|
146
|
+
expect(await translate(MISSING, { mapper: UNDECLARED })).toStrictEqual({
|
|
1259
147
|
kind: PlanKind.CONDITIONAL,
|
|
1260
148
|
filters: { aOptionalString: { equals: null } },
|
|
1261
149
|
});
|
|
1262
150
|
});
|
|
1263
151
|
|
|
1264
|
-
test("omitted: the same plan
|
|
152
|
+
test("omitted: the same plan selects no row rather than the NULL ones", async () => {
|
|
1265
153
|
// A NULL column sends no attribute, so check() denies on a missing-attribute error while the
|
|
1266
|
-
// filter
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
154
|
+
// IS NULL filter would return exactly those rows (#302); a present value is never null, so
|
|
155
|
+
// `== null` is FALSE for it — `NOT LIKE '%'`, which stays UNKNOWN on the NULL rows.
|
|
156
|
+
expect(
|
|
157
|
+
await translate(MISSING, { mapper: UNDECLARED, nullAttributeRepresentation: "omitted" })
|
|
158
|
+
).toEqual(NEVER);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a per-attribute declaration overrides the call-level option, in both directions", async () => {
|
|
162
|
+
// Declared omitted, called explicit: no IS NULL filter (#308).
|
|
163
|
+
expect(await translate(MISSING)).toEqual(NEVER);
|
|
164
|
+
// Declared explicit (`owner`), called omitted: translated.
|
|
165
|
+
expect(
|
|
166
|
+
(await translate("null/equals/null-literal", { nullAttributeRepresentation: "omitted" })).kind
|
|
167
|
+
).toBe(PlanKind.CONDITIONAL);
|
|
168
|
+
// Strip the declaration and the same call selects no row.
|
|
169
|
+
expect(
|
|
170
|
+
await translate("null/equals/null-literal", {
|
|
171
|
+
mapper: UNDECLARED,
|
|
172
|
+
nullAttributeRepresentation: "omitted",
|
|
173
|
+
})
|
|
174
|
+
).toEqual(NEVER);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// The rejection must key off the null OPERAND, not a list of operators: `hasIntersection(tagNames,
|
|
178
|
+
// ["public", null])` carries one in its value list. Enumerating the goldens rather than naming
|
|
179
|
+
// shapes covers a newly added case carrying a null constant automatically.
|
|
180
|
+
// A plan whose null comparison settles passes too: `x == null` is FALSE for a present value and
|
|
181
|
+
// an error for a missing one, rendered with presence tests that are UNKNOWN on NULL. Any
|
|
182
|
+
// NULL-selecting filter needs an IS NULL leaf, i.e. a null literal in the emitted where-input.
|
|
183
|
+
test("no plan carrying a null literal emits an IS NULL filter under call-level omitted", async () => {
|
|
184
|
+
const carrying = readGoldens(CURRENT).filter((golden) => carriesNullLiteral(golden.plan));
|
|
185
|
+
const ids = carrying.map((golden) => golden.id);
|
|
186
|
+
expect(ids).toContain(MISSING);
|
|
187
|
+
expect(ids).toContain("null/has-intersection/literal-list-with-null-element");
|
|
188
|
+
|
|
189
|
+
const plans = await Promise.all(carrying.map((golden) => planOf(golden)));
|
|
190
|
+
|
|
191
|
+
const notRejected = carrying.flatMap((golden, index) => {
|
|
192
|
+
try {
|
|
193
|
+
const result = queryPlanToPrisma({
|
|
194
|
+
queryPlan: plans[index]!,
|
|
195
|
+
mapper: UNDECLARED,
|
|
196
|
+
model: MODEL,
|
|
197
|
+
nullAttributeRepresentation: "omitted",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return result.kind === PlanKind.CONDITIONAL && JSON.stringify(result.filters).includes("null")
|
|
201
|
+
? [golden.id]
|
|
202
|
+
: [];
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// A positional read of a list compares an element, not an optionally absent field; the
|
|
205
|
+
// index operator has no Prisma filter form under either representation. A null inside a
|
|
206
|
+
// whole-list literal is an element too, and comparing a list is refused under either, as
|
|
207
|
+
// is an `in` over a computed member. A null arithmetic operand has no CEL overload, so its
|
|
208
|
+
// fold is refused under either.
|
|
209
|
+
const message = String(error);
|
|
210
|
+
|
|
211
|
+
return message.includes("missing-attribute error") ||
|
|
212
|
+
message.includes("Unsupported operator: index") ||
|
|
213
|
+
message.includes("in requires a field member and literal list") ||
|
|
214
|
+
message.includes("Prisma cannot compare list or map elements") ||
|
|
215
|
+
message.includes("in over a scalar is a CEL error") ||
|
|
216
|
+
message.includes("operator requires string or number operands")
|
|
217
|
+
? []
|
|
218
|
+
: [`${golden.id} (${message})`];
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(notRejected).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("reentrant function mappers", () => {
|
|
227
|
+
// Function mappers are caller-supplied, so the corpus cannot exercise nested adapter calls. The
|
|
228
|
+
// nested call's "omitted" must not leak into the outer "explicit" translation of `== null`.
|
|
229
|
+
test.each([
|
|
230
|
+
"null/equals/null-literal-on-missing-attribute",
|
|
231
|
+
"collection/all/empty-collection",
|
|
232
|
+
])("keeps %s isolated from a nested translation", async (action) => {
|
|
233
|
+
const expected = await translate(action, { mapper: UNDECLARED });
|
|
234
|
+
const nested = await planFor("arithmetic/add/field-plus-constant");
|
|
235
|
+
let calls = 0;
|
|
236
|
+
|
|
237
|
+
const mapper: Mapper = (key) => {
|
|
238
|
+
calls++;
|
|
239
|
+
queryPlanToPrisma({
|
|
240
|
+
queryPlan: nested,
|
|
241
|
+
mapper: MAPPER,
|
|
242
|
+
model: "NestedModel",
|
|
243
|
+
nullAttributeRepresentation: "omitted",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return UNDECLARED[key] ?? { field: key };
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
expect(await translate(action, { mapper })).toStrictEqual(expected);
|
|
250
|
+
expect(calls).toBeGreaterThan(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("an unplannable nested map does not register nullable fields on the outer lambda", async () => {
|
|
255
|
+
// CEL cannot reach this branch: Cerbos 0.54.0 rejects
|
|
256
|
+
// R.attr.tags.all(t, R.attr.tags.map(x, x.name)) with
|
|
257
|
+
// "expected type 'bool' but found 'list(dyn)'". This is a hand-crafted plan contract. A list
|
|
258
|
+
// where a boolean is required is a CEL error, so the body settles to false (all() holds only
|
|
259
|
+
// over no tags) before any lambda scope is entered.
|
|
260
|
+
const condition: PlanExpressionOperand = {
|
|
261
|
+
operator: "all",
|
|
262
|
+
operands: [
|
|
263
|
+
{ name: "request.resource.attr.tags" },
|
|
264
|
+
{
|
|
265
|
+
operator: "lambda",
|
|
266
|
+
operands: [
|
|
267
|
+
{
|
|
268
|
+
operator: "map",
|
|
269
|
+
operands: [
|
|
270
|
+
{ name: "request.resource.attr.tags" },
|
|
271
|
+
{
|
|
272
|
+
operator: "lambda",
|
|
273
|
+
operands: [{ name: "x.name" }, { name: "x" }],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
{ name: "t" },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const result = queryPlanToPrisma({
|
|
284
|
+
queryPlan: { ...(await planFor("collection/all/empty-collection")), kind: PlanKind.CONDITIONAL, condition },
|
|
285
|
+
mapper: MAPPER,
|
|
286
|
+
model: MODEL,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(result).toStrictEqual({
|
|
290
|
+
kind: PlanKind.CONDITIONAL,
|
|
291
|
+
filters: { tags: { none: {} } },
|
|
1270
292
|
});
|
|
1271
293
|
});
|
|
1272
294
|
|
|
1273
295
|
describe("timestamp literals", () => {
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
const at = (plannedAt: string) =>
|
|
296
|
+
// The golden records the folded `now() - duration("24h")` literal as `__NOW_MINUS_24H__`, because
|
|
297
|
+
// it differs on every capture. That makes this the one plan whose value the reader chooses — so
|
|
298
|
+
// it is also the one place the whole timestamp boundary can be walked, by substituting the
|
|
299
|
+
// instant and asking what the adapter does with it.
|
|
300
|
+
const at = async (plannedAt: string) =>
|
|
1279
301
|
queryPlanToPrisma({
|
|
1280
|
-
queryPlan:
|
|
302
|
+
queryPlan: await planFor("timestamp/less-than/relative-window", plannedAt),
|
|
1281
303
|
mapper: MAPPER,
|
|
1282
304
|
model: MODEL,
|
|
1283
305
|
});
|
|
1284
306
|
|
|
1285
|
-
test("a nanosecond instant — what the PDP actually folds —
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1288
|
-
|
|
1289
|
-
expect(() => translate("ts-window")).toThrow(
|
|
1290
|
-
"Timestamp value exceeds millisecond precision"
|
|
1291
|
-
);
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
test("the same plan at millisecond precision translates", () => {
|
|
1295
|
-
expect(at("2026-08-11T09:13:39.123Z")).toStrictEqual({
|
|
307
|
+
test("a nanosecond instant — what the PDP actually folds — compares against the next millisecond", async () => {
|
|
308
|
+
// A DateTime attribute is a whole millisecond, so `a < T` for a T between two milliseconds is
|
|
309
|
+
// `a < ceil(T)`. The corpus loader substitutes a nanosecond instant for the same reason.
|
|
310
|
+
expect(await at("2026-08-11T09:13:39.123456789Z")).toStrictEqual({
|
|
1296
311
|
kind: PlanKind.CONDITIONAL,
|
|
1297
|
-
filters: { createdAt: { lt: "2026-08-11T09:13:39.
|
|
312
|
+
filters: { createdAt: { lt: "2026-08-11T09:13:39.124Z" } },
|
|
1298
313
|
});
|
|
1299
314
|
});
|
|
1300
315
|
|
|
1301
|
-
test("excess fractional digits are accepted only when they are zero", () => {
|
|
1302
|
-
expect(at("2026-08-11T09:13:39.123000Z")).toStrictEqual({
|
|
316
|
+
test("excess fractional digits are accepted only when they are zero", async () => {
|
|
317
|
+
expect(await at("2026-08-11T09:13:39.123000Z")).toStrictEqual({
|
|
1303
318
|
kind: PlanKind.CONDITIONAL,
|
|
1304
319
|
filters: { createdAt: { lt: "2026-08-11T09:13:39.123Z" } },
|
|
1305
320
|
});
|
|
@@ -1312,10 +327,9 @@ describe("timestamp literals", () => {
|
|
|
1312
327
|
["a date with no time part", "2024-01-01"],
|
|
1313
328
|
["a year outside CEL's instant range", "0000-01-01T00:00:00Z"],
|
|
1314
329
|
["a day that does not exist", "2024-02-30T00:00:00Z"],
|
|
1315
|
-
["sub-millisecond precision", "2024-01-01T00:00:00.1234Z"],
|
|
1316
330
|
["an offset that pushes past the maximum instant", "9999-12-31T23:00:00-02:00"],
|
|
1317
|
-
])("%s fails closed", (_label, value) => {
|
|
1318
|
-
expect(
|
|
331
|
+
])("%s fails closed", async (_label, value) => {
|
|
332
|
+
await expect(at(value)).rejects.toThrow(/RFC 3339|millisecond|instant range/);
|
|
1319
333
|
});
|
|
1320
334
|
});
|
|
1321
335
|
|
|
@@ -1331,40 +345,45 @@ describe("relation subqueryFilter", () => {
|
|
|
1331
345
|
*/
|
|
1332
346
|
const VISIBLE_ONLY: PrismaFilter = { name: { not: "hidden" } };
|
|
1333
347
|
|
|
1334
|
-
const mapperFor = (subqueryFilter?: PrismaFilter): Mapper =>
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
348
|
+
const mapperFor = (subqueryFilter?: PrismaFilter): Mapper => {
|
|
349
|
+
const relation: Relation = {
|
|
350
|
+
name: "tags",
|
|
351
|
+
type: "many",
|
|
352
|
+
// `nullable: false` keeps the element NULL guard (see "relation element nullability"
|
|
353
|
+
// below) out of these filters, so they isolate what the declaration changes.
|
|
354
|
+
fields: { name: { field: "name", nullable: false } },
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (subqueryFilter) relation.subqueryFilter = subqueryFilter;
|
|
358
|
+
|
|
359
|
+
return { "request.resource.attr.tags": { relation } };
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const filtersFor = async (action: string, subqueryFilter?: PrismaFilter) => {
|
|
363
|
+
const result = await translate(action, { mapper: mapperFor(subqueryFilter) });
|
|
1344
364
|
|
|
1345
|
-
const filtersFor = (action: string, subqueryFilter?: PrismaFilter) => {
|
|
1346
|
-
const result = translate(action, { mapper: mapperFor(subqueryFilter) });
|
|
1347
365
|
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1348
366
|
throw new Error(`Expected CONDITIONAL result for ${action}`);
|
|
1349
367
|
}
|
|
368
|
+
|
|
1350
369
|
return result.filters;
|
|
1351
370
|
};
|
|
1352
371
|
|
|
1353
|
-
test("declared: exists() examines only the records the application serialised", () => {
|
|
1354
|
-
expect(filtersFor("exists-
|
|
372
|
+
test("declared: exists() examines only the records the application serialised", async () => {
|
|
373
|
+
expect(await filtersFor("collection/exists/empty-collection", VISIBLE_ONLY)).toStrictEqual({
|
|
1355
374
|
tags: {
|
|
1356
375
|
some: { AND: [{ name: { not: "hidden" } }, { name: { equals: "public" } }] },
|
|
1357
376
|
},
|
|
1358
377
|
});
|
|
1359
378
|
});
|
|
1360
379
|
|
|
1361
|
-
test("declared: all() narrows the records examined, not the records required", () => {
|
|
380
|
+
test("declared: all() narrows the records examined, not the records required", async () => {
|
|
1362
381
|
// `every: AND(visible, P)` would REQUIRE every record to be visible, dropping any row that
|
|
1363
382
|
// holds a hidden tag. The rewrite to `none: AND(visible, NOT P)` is what makes the
|
|
1364
383
|
// declaration mean "ignore what the application hides" — including the empty-collection case,
|
|
1365
384
|
// where CEL's all() is vacuously true and check() agrees because the application sent an
|
|
1366
385
|
// empty list for the same reason.
|
|
1367
|
-
expect(filtersFor("all-
|
|
386
|
+
expect(await filtersFor("collection/all/empty-collection", VISIBLE_ONLY)).toStrictEqual({
|
|
1368
387
|
tags: {
|
|
1369
388
|
none: {
|
|
1370
389
|
AND: [
|
|
@@ -1376,33 +395,41 @@ describe("relation subqueryFilter", () => {
|
|
|
1376
395
|
});
|
|
1377
396
|
});
|
|
1378
397
|
|
|
1379
|
-
test("declared: an emptiness check counts only the visible records", () => {
|
|
1380
|
-
expect(filtersFor("
|
|
398
|
+
test("declared: an emptiness check counts only the visible records", async () => {
|
|
399
|
+
expect(await filtersFor("size/equals/negated-collection-zero", VISIBLE_ONLY)).toStrictEqual({
|
|
1381
400
|
NOT: { tags: { none: { name: { not: "hidden" } } } },
|
|
1382
401
|
});
|
|
1383
402
|
});
|
|
1384
|
-
|
|
1385
|
-
test("undeclared: the emitted filter is what it was before the field existed", () => {
|
|
1386
|
-
// The non-breaking guarantee. Silence must not add a clause, and must not warn.
|
|
1387
|
-
expect(filtersFor("exists-on-empty")).toStrictEqual({
|
|
1388
|
-
tags: { some: { name: { equals: "public" } } },
|
|
1389
|
-
});
|
|
1390
|
-
expect(filtersFor("all-on-empty")).toStrictEqual({
|
|
1391
|
-
tags: { every: { name: { equals: "public" } } },
|
|
1392
|
-
});
|
|
1393
|
-
expect(filtersFor("not-empty")).toStrictEqual({
|
|
1394
|
-
NOT: { tags: { none: {} } },
|
|
1395
|
-
});
|
|
1396
|
-
});
|
|
1397
403
|
});
|
|
1398
404
|
|
|
1399
405
|
describe("the mapper contract", () => {
|
|
1400
406
|
// A mapper is caller-supplied, so these are caller errors rather than policy shapes: no corpus
|
|
1401
407
|
// action can produce them, because the corpus fixes one mapper per adapter.
|
|
1402
408
|
|
|
1403
|
-
test(
|
|
409
|
+
test.each([undefined, true, false])(
|
|
410
|
+
"hierarchy segment nullability %s is respected",
|
|
411
|
+
async (nullable) => {
|
|
412
|
+
const result = await translate("hierarchy/overlaps/path-built-from-list-value-first", {
|
|
413
|
+
mapper: { "request.resource.attr.scope": { field: "scope", nullable } },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(result).toStrictEqual({
|
|
417
|
+
kind: PlanKind.CONDITIONAL,
|
|
418
|
+
filters:
|
|
419
|
+
nullable === false
|
|
420
|
+
? {}
|
|
421
|
+
: {
|
|
422
|
+
AND: [
|
|
423
|
+
{ OR: [{ scope: { equals: "" } }, { scope: { not: "" } }] },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
test("a function mapper resolves a scalar reference", async () => {
|
|
1404
431
|
expect(
|
|
1405
|
-
translate("
|
|
432
|
+
await translate("string/equals/case-sensitive", {
|
|
1406
433
|
mapper: (key: string) => ({
|
|
1407
434
|
field: key.replace("request.resource.attr.", ""),
|
|
1408
435
|
}),
|
|
@@ -1413,36 +440,166 @@ describe("the mapper contract", () => {
|
|
|
1413
440
|
});
|
|
1414
441
|
});
|
|
1415
442
|
|
|
1416
|
-
test("a
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
443
|
+
test("a key under a scalar-mapped prefix is refused, not resolved to the prefix's column", async () => {
|
|
444
|
+
// cerbos/query-plan-adapters#689: the plan reads aString beneath a prefix mapped to one column.
|
|
445
|
+
await expect(
|
|
446
|
+
translate("string/equals/case-sensitive", {
|
|
447
|
+
mapper: { "request.resource.attr": { field: "owner" } },
|
|
448
|
+
}),
|
|
449
|
+
).rejects.toThrow(UnsupportedQueryPlanError);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test.each(["collection/exists/scalar-list-equals"])(
|
|
453
|
+
"%s resolves a projection supplied through a prefix mapper",
|
|
454
|
+
async (action) => {
|
|
455
|
+
const direct = await translate(action);
|
|
456
|
+
|
|
457
|
+
if (direct.kind !== PlanKind.CONDITIONAL) {
|
|
458
|
+
throw new Error("Expected a conditional projection plan");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const mapper: Mapper = {
|
|
462
|
+
"request.resource.attr": {
|
|
1420
463
|
relation: {
|
|
1421
|
-
name: "
|
|
1422
|
-
type: "
|
|
1423
|
-
fields: {
|
|
464
|
+
name: "categories",
|
|
465
|
+
type: "many",
|
|
466
|
+
fields: {
|
|
467
|
+
tagNames: {
|
|
468
|
+
relation: { name: "tags", type: "many", field: "name" },
|
|
469
|
+
},
|
|
470
|
+
},
|
|
1424
471
|
},
|
|
1425
|
-
}
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
expect(await translate(action, { mapper })).toStrictEqual({
|
|
476
|
+
kind: PlanKind.CONDITIONAL,
|
|
477
|
+
filters: { categories: { some: direct.filters } },
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
test("negating an overlaps that folds to an unconditional filter is refused", async () => {
|
|
483
|
+
// `nullable: false` on every segment lets `overlaps(hierarchy("dept"), hierarchy(["dept",
|
|
484
|
+
// scope]))` fold to `{}` (asserted above). Prisma evaluates `{ NOT: {} }` as true, so the
|
|
485
|
+
// negation would match every row where CEL's `!true` matches none; the adapter has no
|
|
486
|
+
// field-free false condition to emit instead (cerbos/query-plan-adapters#495).
|
|
487
|
+
const fixture = await planFor("hierarchy/overlaps/path-built-from-list-value-first");
|
|
488
|
+
|
|
489
|
+
if (fixture.kind !== PlanKind.CONDITIONAL) {
|
|
490
|
+
throw new Error("Expected a conditional hierarchy plan");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
expect(() =>
|
|
494
|
+
queryPlanToPrisma({
|
|
495
|
+
queryPlan: {
|
|
496
|
+
...fixture,
|
|
497
|
+
condition: { operator: "not", operands: [fixture.condition] },
|
|
498
|
+
},
|
|
499
|
+
mapper: { "request.resource.attr.scope": { field: "scope", nullable: false } },
|
|
1426
500
|
})
|
|
1427
|
-
).
|
|
1428
|
-
|
|
1429
|
-
|
|
501
|
+
).toThrow("Cannot negate an unconditional filter");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("relation element nullability", () => {
|
|
505
|
+
// Omitting the element NULL guard is what over-grants — a negated exists(), an all() or a
|
|
506
|
+
// hasIntersection() over map() would admit rows holding a NULL element that check() denies
|
|
507
|
+
// — so silence means nullable, and only `nullable: false` drops the guard
|
|
508
|
+
// (cerbos/query-plan-adapters#495). The corpus maps one element column per nullability, so
|
|
509
|
+
// it cannot vary the declaration itself.
|
|
510
|
+
const guarded = {
|
|
511
|
+
AND: [
|
|
512
|
+
{ tags: { every: { name: { equals: "public" } } } },
|
|
513
|
+
{ tags: { none: { name: null } } },
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const unguarded = { tags: { every: { name: { equals: "public" } } } };
|
|
518
|
+
|
|
519
|
+
const allFor = (fields?: Relation["fields"]) => {
|
|
520
|
+
const relation: Relation = { name: "tags", type: "many" };
|
|
521
|
+
|
|
522
|
+
if (fields) relation.fields = fields;
|
|
523
|
+
|
|
524
|
+
return translate("collection/all/empty-collection", {
|
|
525
|
+
mapper: { "request.resource.attr.tags": { relation } },
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
test.each<[string, Relation["fields"], PrismaFilter]>([
|
|
530
|
+
["undeclared", { name: { field: "name" } }, guarded],
|
|
531
|
+
["nullable: true", { name: { field: "name", nullable: true } }, guarded],
|
|
532
|
+
["nullable: false", { name: { field: "name", nullable: false } }, unguarded],
|
|
533
|
+
["an element with no field mapping at all", undefined, guarded],
|
|
534
|
+
])("%s", async (_label, fields, filters) => {
|
|
535
|
+
expect(await allFor(fields)).toStrictEqual({
|
|
536
|
+
kind: PlanKind.CONDITIONAL,
|
|
537
|
+
filters,
|
|
538
|
+
});
|
|
1430
539
|
});
|
|
1431
540
|
});
|
|
1432
541
|
|
|
1433
|
-
test("size() against a scalar mapping is refused rather than guessed", () => {
|
|
1434
|
-
expect(
|
|
1435
|
-
translate("
|
|
542
|
+
test("size() against a scalar mapping is refused rather than guessed", async () => {
|
|
543
|
+
await expect(
|
|
544
|
+
translate("size/equals/negated-collection-zero", {
|
|
1436
545
|
mapper: { "request.resource.attr.tags": { field: "tags" } },
|
|
1437
546
|
})
|
|
1438
|
-
).toThrow("size operator requires a relation mapping");
|
|
547
|
+
).rejects.toThrow("size operator requires a relation mapping");
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe("which patterns RE2 rejects", () => {
|
|
552
|
+
// matches() raises an error on a pattern RE2 rejects, which denies every row, so the adapter
|
|
553
|
+
// must agree with RE2 on exactly which patterns those are. The corpus pins the rejected shapes
|
|
554
|
+
// it carries; this pins the one the previous hand-rolled check got wrong: a digit after a
|
|
555
|
+
// backslash is a backreference to RE2 only on its own, and an octal escape when another octal
|
|
556
|
+
// digit follows, so `\11` is a tab.
|
|
557
|
+
const matches = (pattern: string, sibling = false): PlanResourcesResponse => {
|
|
558
|
+
const test: PlanExpressionOperand = {
|
|
559
|
+
operator: "matches",
|
|
560
|
+
operands: [{ name: "request.resource.attr.aString" }, { value: pattern }],
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const condition: PlanExpressionOperand = sibling
|
|
564
|
+
? {
|
|
565
|
+
operator: "or",
|
|
566
|
+
operands: [test, { operator: "eq", operands: [{ name: "request.resource.attr.aNumber" }, { value: 5 }] }],
|
|
567
|
+
}
|
|
568
|
+
: test;
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
kind: PlanKind.CONDITIONAL,
|
|
572
|
+
condition,
|
|
573
|
+
cerbosCallId: "",
|
|
574
|
+
requestId: "",
|
|
575
|
+
validationErrors: [],
|
|
576
|
+
metadata: undefined,
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
test("an octal escape is the character it names, not a backreference", () => {
|
|
581
|
+
const result = queryPlanToPrisma({ queryPlan: matches("^\\11"), mapper: MAPPER, model: MODEL });
|
|
582
|
+
expect(result.kind).toBe(PlanKind.CONDITIONAL);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("a backreference is an error on every row, so only its sibling decides", () => {
|
|
586
|
+
const result = queryPlanToPrisma({
|
|
587
|
+
queryPlan: matches("(a)\\1", true),
|
|
588
|
+
mapper: MAPPER,
|
|
589
|
+
model: MODEL,
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(result).toEqual({ kind: PlanKind.CONDITIONAL, filters: { aNumber: { equals: 5 } } });
|
|
593
|
+
expect(queryPlanToPrisma({ queryPlan: matches("\\8"), mapper: MAPPER, model: MODEL })).toEqual({
|
|
594
|
+
kind: PlanKind.ALWAYS_DENIED,
|
|
595
|
+
});
|
|
1439
596
|
});
|
|
1440
597
|
});
|
|
1441
598
|
|
|
1442
599
|
describe("plans the planner cannot produce", () => {
|
|
1443
600
|
// Input validation on a public function, not policy shapes. Every other assertion in this file
|
|
1444
|
-
// reads its plan from a
|
|
1445
|
-
// but these are malformed by construction, so there is no
|
|
601
|
+
// reads its plan from a golden file precisely because a typed plan is a belief about the planner —
|
|
602
|
+
// but these are malformed by construction, so there is no golden to read and nothing to
|
|
1446
603
|
// believe. They exist so a caller who hands the adapter a hand-rolled or half-decoded plan gets
|
|
1447
604
|
// an error rather than a filter.
|
|
1448
605
|
//
|
|
@@ -1451,20 +608,28 @@ describe("plans the planner cannot produce", () => {
|
|
|
1451
608
|
// pinned here by hand-built plan before #377 and are now
|
|
1452
609
|
// [#394](https://github.com/cerbos/query-plan-adapters/issues/394).
|
|
1453
610
|
|
|
1454
|
-
const plan = (condition:
|
|
1455
|
-
(
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
611
|
+
const plan = (condition: Value): PlanResourcesResponse =>
|
|
612
|
+
overwritten(
|
|
613
|
+
{
|
|
614
|
+
kind: PlanKind.CONDITIONAL,
|
|
615
|
+
condition: { value: true },
|
|
616
|
+
cerbosCallId: "",
|
|
617
|
+
requestId: "",
|
|
618
|
+
validationErrors: [],
|
|
619
|
+
metadata: undefined,
|
|
620
|
+
},
|
|
621
|
+
"condition",
|
|
622
|
+
condition,
|
|
623
|
+
);
|
|
1463
624
|
|
|
1464
625
|
test("an unrecognised plan kind", () => {
|
|
1465
626
|
expect(() =>
|
|
1466
627
|
queryPlanToPrisma({
|
|
1467
|
-
queryPlan:
|
|
628
|
+
queryPlan: overwritten(
|
|
629
|
+
{ kind: PlanKind.ALWAYS_ALLOWED, cerbosCallId: "", requestId: "", validationErrors: [], metadata: undefined },
|
|
630
|
+
"kind",
|
|
631
|
+
"INVALID_KIND"
|
|
632
|
+
),
|
|
1468
633
|
mapper: {},
|
|
1469
634
|
})
|
|
1470
635
|
).toThrow("Invalid query plan.");
|
|
@@ -1570,17 +735,32 @@ describe("plans the planner cannot produce", () => {
|
|
|
1570
735
|
).toThrow("exists over a literal collection requires a list value");
|
|
1571
736
|
});
|
|
1572
737
|
|
|
1573
|
-
|
|
738
|
+
// An empty conjunction is `{ AND: [] }` in Prisma, which matches every row, and the constant
|
|
739
|
+
// folder would otherwise reduce it to `true` before the translator saw it
|
|
740
|
+
// (cerbos/query-plan-adapters#495). The empty disjunction is refused alongside it: neither is
|
|
741
|
+
// something the planner emits.
|
|
742
|
+
test.each<[string, Value, string]>([
|
|
743
|
+
["an empty and", { operator: "and", operands: [] }, "and requires at least one operand"],
|
|
744
|
+
["an empty or", { operator: "or", operands: [] }, "or requires at least one operand"],
|
|
745
|
+
[
|
|
746
|
+
"an empty and under an or",
|
|
747
|
+
{
|
|
748
|
+
operator: "or",
|
|
749
|
+
operands: [
|
|
750
|
+
{ name: "request.resource.attr.aBool" },
|
|
751
|
+
{ operator: "and", operands: [] },
|
|
752
|
+
],
|
|
753
|
+
},
|
|
754
|
+
"and requires at least one operand",
|
|
755
|
+
],
|
|
756
|
+
[
|
|
757
|
+
"an empty and under a not",
|
|
758
|
+
{ operator: "not", operands: [{ operator: "and", operands: [] }] },
|
|
759
|
+
"and requires at least one operand",
|
|
760
|
+
],
|
|
761
|
+
])("%s", (_label, condition, message) => {
|
|
1574
762
|
expect(() =>
|
|
1575
|
-
queryPlanToPrisma({
|
|
1576
|
-
|
|
1577
|
-
operator: "if",
|
|
1578
|
-
operands: [{ value: true }, { value: false }, { value: true }],
|
|
1579
|
-
}),
|
|
1580
|
-
mapper: MAPPER,
|
|
1581
|
-
})
|
|
1582
|
-
).toThrow(
|
|
1583
|
-
"A constant-false conditional predicate must be folded by the Cerbos planner"
|
|
1584
|
-
);
|
|
763
|
+
queryPlanToPrisma({ queryPlan: plan(condition), mapper: MAPPER })
|
|
764
|
+
).toThrow(message);
|
|
1585
765
|
});
|
|
1586
766
|
});
|