@cerbos/orm-prisma 4.0.0 → 4.1.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 +168 -12
- package/lib/index.d.ts +74 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +344 -90
- package/lib/index.js.map +1 -1
- package/package.json +10 -7
- package/src/adversarial.test.ts +948 -253
- package/src/corpus.ts +414 -0
- package/src/generated/prisma-adversarial/browser.ts +10 -0
- package/src/generated/prisma-adversarial/client.ts +10 -0
- package/src/generated/prisma-adversarial/internal/class.ts +24 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +178 -2
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +27 -1
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +124 -0
- package/src/generated/prisma-adversarial/models.ts +2 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/browser.ts +14 -19
- package/src/generated/{prisma → prisma-adversarial-pg}/client.ts +16 -21
- package/src/generated/{prisma → prisma-adversarial-pg}/commonInputTypes.ts +153 -71
- package/src/generated/prisma-adversarial-pg/internal/class.ts +264 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespace.ts +339 -371
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +56 -48
- 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/AdversarialParent.ts +1476 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +1791 -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 +18 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +11830 -0
- package/src/generated/prisma-adversarial-v6/index.d.ts +3173 -44
- package/src/index.ts +508 -119
- package/src/test-setup.adversarial-url.ts +18 -0
- package/src/test-setup.adversarial.pg.v6.ts +13 -0
- package/src/test-setup.adversarial.pg.v7.ts +9 -0
- package/src/translator.test.ts +1586 -0
- package/src/generated/prisma/internal/class.ts +0 -274
- 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-pg}/enums.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/client.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/default.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/edge.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/runtime/index-browser.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/runtime/library.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/wasm.d.ts +0 -0
|
@@ -0,0 +1,1586 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
import type {
|
|
6
|
+
PlanExpressionOperand,
|
|
7
|
+
PlanResourcesResponse,
|
|
8
|
+
} from "@cerbos/core";
|
|
9
|
+
|
|
10
|
+
import { queryPlanToPrisma, PlanKind } from ".";
|
|
11
|
+
import type {
|
|
12
|
+
Mapper,
|
|
13
|
+
NullAttributeRepresentation,
|
|
14
|
+
PrismaFilter,
|
|
15
|
+
QueryPlanToPrismaResult,
|
|
16
|
+
} from ".";
|
|
17
|
+
import {
|
|
18
|
+
CONFORMANCE_DIR,
|
|
19
|
+
MAPPER,
|
|
20
|
+
MODEL,
|
|
21
|
+
classifyActionsForAdapter,
|
|
22
|
+
planFromWireFixture,
|
|
23
|
+
wireFixtureActions,
|
|
24
|
+
} from "./corpus";
|
|
25
|
+
import type { ActionsFile } from "./corpus";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Translator unit test: for every action in the shared `../conformance/` corpus, the filter this
|
|
29
|
+
* adapter emits. Offline — no Cerbos sidecar, no database, no generated Prisma client.
|
|
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:
|
|
33
|
+
*
|
|
34
|
+
* | assertion | who owns it |
|
|
35
|
+
* | --- | --- |
|
|
36
|
+
* | the plan the PDP produces for a policy | `conformance/wire-fixtures/`, replanned and diffed by the `Conformance Corpus` workflow |
|
|
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.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
const actionsFile: ActionsFile = JSON.parse(
|
|
61
|
+
fs.readFileSync(path.join(CONFORMANCE_DIR, "actions.json"), "utf8")
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The shapes `actions.json` says this adapter must refuse, each with the message it must refuse
|
|
66
|
+
* them with. Identical to the classification `adversarial.test.ts` asserts against a live PDP;
|
|
67
|
+
* asserting it here as well is what lets the completeness guard below be total, and it costs a
|
|
68
|
+
* millisecond rather than a container.
|
|
69
|
+
*/
|
|
70
|
+
const { throwingActions: THROWING_ACTIONS } = classifyActionsForAdapter(
|
|
71
|
+
actionsFile,
|
|
72
|
+
"prisma"
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function translate(
|
|
76
|
+
action: string,
|
|
77
|
+
options: {
|
|
78
|
+
mapper?: Mapper;
|
|
79
|
+
nullAttributeRepresentation?: NullAttributeRepresentation;
|
|
80
|
+
} = {}
|
|
81
|
+
): QueryPlanToPrismaResult {
|
|
82
|
+
return queryPlanToPrisma({
|
|
83
|
+
queryPlan: planFromWireFixture(action),
|
|
84
|
+
mapper: options.mapper ?? MAPPER,
|
|
85
|
+
model: MODEL,
|
|
86
|
+
...(options.nullAttributeRepresentation
|
|
87
|
+
? { nullAttributeRepresentation: options.nullAttributeRepresentation }
|
|
88
|
+
: {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The plan kind for the two corpus actions the planner resolves without a condition.
|
|
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
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The `where` input this adapter emits for every corpus action it can translate, under `MAPPER`
|
|
109
|
+
* and the default (`"explicit"`) null representation.
|
|
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
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
test.each(Object.entries(EXPECTED_KINDS))(
|
|
1207
|
+
"%s resolves without a condition",
|
|
1208
|
+
(action, kind) => {
|
|
1209
|
+
expect(translate(action)).toStrictEqual({ kind });
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
// The message, not just the throw: a mapper typo or an unrelated validation satisfies a bare
|
|
1214
|
+
// `toThrow()` just as well as the limitation the corpus documents (#326). The harness makes the
|
|
1215
|
+
// same assertion against a live PDP; here it costs a millisecond and covers the whole roster,
|
|
1216
|
+
// which is what lets the completeness guard below be total.
|
|
1217
|
+
test.each(THROWING_ACTIONS)(
|
|
1218
|
+
"%s is refused with the message actions.json pins (%s)",
|
|
1219
|
+
(action, _reason, message) => {
|
|
1220
|
+
expect(() => translate(action)).toThrow(message);
|
|
1221
|
+
}
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
test("every corpus action is classified here exactly once", () => {
|
|
1225
|
+
const filters = Object.keys(EXPECTED_FILTERS);
|
|
1226
|
+
const kinds = Object.keys(EXPECTED_KINDS);
|
|
1227
|
+
const throwing = THROWING_ACTIONS.map(([action]) => action);
|
|
1228
|
+
const classified = [...filters, ...kinds, ...throwing].sort();
|
|
1229
|
+
|
|
1230
|
+
// Total: a corpus action with no entry here lands as a failure rather than as silence. This
|
|
1231
|
+
// is the assertion that makes the file self-maintaining — adding a hostile shape to the
|
|
1232
|
+
// corpus forces someone to look at the filter this adapter emits for it.
|
|
1233
|
+
expect(classified).toEqual(wireFixtureActions());
|
|
1234
|
+
// Disjoint: an action pinned as a filter AND declared unsupported would satisfy the union
|
|
1235
|
+
// above while asserting two contradictory things.
|
|
1236
|
+
expect(classified).toEqual([...new Set(classified)].sort());
|
|
1237
|
+
// The table is alphabetical, so a translator change reads as the list of shapes it moved.
|
|
1238
|
+
expect(filters).toEqual([...filters].sort());
|
|
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 });
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
describe("nullAttributeRepresentation", () => {
|
|
1251
|
+
// `null-eq-missing` is the corpus's `nullRepresentationOmitted` probe: `== null` against an
|
|
1252
|
+
// attribute the caller OMITS when the column is NULL. The two conventions are indistinguishable
|
|
1253
|
+
// on the wire — the planner emits the same `eq(attr, null)` either way — so the adapter has to
|
|
1254
|
+
// be told, and the whole behaviour is a translator property with no store in it.
|
|
1255
|
+
test("explicit: a null operand becomes an IS NULL filter", () => {
|
|
1256
|
+
expect(
|
|
1257
|
+
translate("null-eq-missing", { nullAttributeRepresentation: "explicit" })
|
|
1258
|
+
).toStrictEqual({
|
|
1259
|
+
kind: PlanKind.CONDITIONAL,
|
|
1260
|
+
filters: { aOptionalString: { equals: null } },
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
test("omitted: the same plan is refused rather than translated", () => {
|
|
1265
|
+
// A NULL column sends no attribute, so check() denies on a missing-attribute error while the
|
|
1266
|
+
// filter above would return exactly those rows (#302).
|
|
1267
|
+
expect(() =>
|
|
1268
|
+
translate("null-eq-missing", { nullAttributeRepresentation: "omitted" })
|
|
1269
|
+
).toThrow("missing-attribute error");
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
describe("timestamp literals", () => {
|
|
1274
|
+
// `regenerate-wire-fixtures.sh` rewrites the folded `now() - duration("24h")` literal in
|
|
1275
|
+
// `ts-window` to a placeholder, because it differs on every capture. That makes this the one
|
|
1276
|
+
// fixture whose value the reader chooses — so it is also the one place the whole timestamp
|
|
1277
|
+
// boundary can be walked, by substituting the instant and asking what the adapter does with it.
|
|
1278
|
+
const at = (plannedAt: string) =>
|
|
1279
|
+
queryPlanToPrisma({
|
|
1280
|
+
queryPlan: planFromWireFixture("ts-window", plannedAt),
|
|
1281
|
+
mapper: MAPPER,
|
|
1282
|
+
model: MODEL,
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
test("a nanosecond instant — what the PDP actually folds — is refused", () => {
|
|
1286
|
+
// This, and nothing else, is why `ts-window` and `ts-vf` are `adapterUnsupported`. A tidy
|
|
1287
|
+
// millisecond substitution in the loader would translate cleanly and quietly contradict
|
|
1288
|
+
// actions.json.
|
|
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({
|
|
1296
|
+
kind: PlanKind.CONDITIONAL,
|
|
1297
|
+
filters: { createdAt: { lt: "2026-08-11T09:13:39.123Z" } },
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
test("excess fractional digits are accepted only when they are zero", () => {
|
|
1302
|
+
expect(at("2026-08-11T09:13:39.123000Z")).toStrictEqual({
|
|
1303
|
+
kind: PlanKind.CONDITIONAL,
|
|
1304
|
+
filters: { createdAt: { lt: "2026-08-11T09:13:39.123Z" } },
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// Each of these is refused rather than coerced: a `Date` parsed from a lenient string would
|
|
1309
|
+
// compare against the column as some other instant, which is a filter that returns rows the PDP
|
|
1310
|
+
// denies rather than an error the caller can see.
|
|
1311
|
+
test.each([
|
|
1312
|
+
["a date with no time part", "2024-01-01"],
|
|
1313
|
+
["a year outside CEL's instant range", "0000-01-01T00:00:00Z"],
|
|
1314
|
+
["a day that does not exist", "2024-02-30T00:00:00Z"],
|
|
1315
|
+
["sub-millisecond precision", "2024-01-01T00:00:00.1234Z"],
|
|
1316
|
+
["an offset that pushes past the maximum instant", "9999-12-31T23:00:00-02:00"],
|
|
1317
|
+
])("%s fails closed", (_label, value) => {
|
|
1318
|
+
expect(() => at(value)).toThrow(/RFC 3339|millisecond|instant range/);
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
describe("relation subqueryFilter", () => {
|
|
1323
|
+
/**
|
|
1324
|
+
* The one mapping hazard the corpus cannot express with a policy action, because the policy is
|
|
1325
|
+
* irrelevant to it: the rows the adapter's subquery sees must equal the rows the application
|
|
1326
|
+
* put into the resource attributes. Prisma has no schema-level filtered relation — a `where`
|
|
1327
|
+
* injected by a client extension rewrites the top-level query, never the nested
|
|
1328
|
+
* `some`/`every`/`none` this adapter generates — so a narrowing the application applies to its
|
|
1329
|
+
* own reads does not reach the subquery unless the caller declares it. See "Mapping hazards" in
|
|
1330
|
+
* conformance/README.md and cerbos/query-plan-adapters#314.
|
|
1331
|
+
*/
|
|
1332
|
+
const VISIBLE_ONLY: PrismaFilter = { name: { not: "hidden" } };
|
|
1333
|
+
|
|
1334
|
+
const mapperFor = (subqueryFilter?: PrismaFilter): Mapper => ({
|
|
1335
|
+
"request.resource.attr.tags": {
|
|
1336
|
+
relation: {
|
|
1337
|
+
name: "tags",
|
|
1338
|
+
type: "many",
|
|
1339
|
+
fields: { name: { field: "name" } },
|
|
1340
|
+
...(subqueryFilter ? { subqueryFilter } : {}),
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
const filtersFor = (action: string, subqueryFilter?: PrismaFilter) => {
|
|
1346
|
+
const result = translate(action, { mapper: mapperFor(subqueryFilter) });
|
|
1347
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1348
|
+
throw new Error(`Expected CONDITIONAL result for ${action}`);
|
|
1349
|
+
}
|
|
1350
|
+
return result.filters;
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
test("declared: exists() examines only the records the application serialised", () => {
|
|
1354
|
+
expect(filtersFor("exists-on-empty", VISIBLE_ONLY)).toStrictEqual({
|
|
1355
|
+
tags: {
|
|
1356
|
+
some: { AND: [{ name: { not: "hidden" } }, { name: { equals: "public" } }] },
|
|
1357
|
+
},
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
test("declared: all() narrows the records examined, not the records required", () => {
|
|
1362
|
+
// `every: AND(visible, P)` would REQUIRE every record to be visible, dropping any row that
|
|
1363
|
+
// holds a hidden tag. The rewrite to `none: AND(visible, NOT P)` is what makes the
|
|
1364
|
+
// declaration mean "ignore what the application hides" — including the empty-collection case,
|
|
1365
|
+
// where CEL's all() is vacuously true and check() agrees because the application sent an
|
|
1366
|
+
// empty list for the same reason.
|
|
1367
|
+
expect(filtersFor("all-on-empty", VISIBLE_ONLY)).toStrictEqual({
|
|
1368
|
+
tags: {
|
|
1369
|
+
none: {
|
|
1370
|
+
AND: [
|
|
1371
|
+
{ name: { not: "hidden" } },
|
|
1372
|
+
{ NOT: { name: { equals: "public" } } },
|
|
1373
|
+
],
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
test("declared: an emptiness check counts only the visible records", () => {
|
|
1380
|
+
expect(filtersFor("not-empty", VISIBLE_ONLY)).toStrictEqual({
|
|
1381
|
+
NOT: { tags: { none: { name: { not: "hidden" } } } },
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
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
|
+
});
|
|
1398
|
+
|
|
1399
|
+
describe("the mapper contract", () => {
|
|
1400
|
+
// A mapper is caller-supplied, so these are caller errors rather than policy shapes: no corpus
|
|
1401
|
+
// action can produce them, because the corpus fixes one mapper per adapter.
|
|
1402
|
+
|
|
1403
|
+
test("a function mapper resolves a scalar reference", () => {
|
|
1404
|
+
expect(
|
|
1405
|
+
translate("cs-eq", {
|
|
1406
|
+
mapper: (key: string) => ({
|
|
1407
|
+
field: key.replace("request.resource.attr.", ""),
|
|
1408
|
+
}),
|
|
1409
|
+
})
|
|
1410
|
+
).toStrictEqual({
|
|
1411
|
+
kind: PlanKind.CONDITIONAL,
|
|
1412
|
+
filters: { aString: { equals: "one" } },
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("a function mapper resolves a relation", () => {
|
|
1417
|
+
expect(
|
|
1418
|
+
translate("rel-bool-hop", {
|
|
1419
|
+
mapper: () => ({
|
|
1420
|
+
relation: {
|
|
1421
|
+
name: "parent",
|
|
1422
|
+
type: "one",
|
|
1423
|
+
fields: { aBool: { field: "aBool" } },
|
|
1424
|
+
},
|
|
1425
|
+
}),
|
|
1426
|
+
})
|
|
1427
|
+
).toStrictEqual({
|
|
1428
|
+
kind: PlanKind.CONDITIONAL,
|
|
1429
|
+
filters: { parent: { is: { aBool: { equals: true } } } },
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
test("size() against a scalar mapping is refused rather than guessed", () => {
|
|
1434
|
+
expect(() =>
|
|
1435
|
+
translate("not-empty", {
|
|
1436
|
+
mapper: { "request.resource.attr.tags": { field: "tags" } },
|
|
1437
|
+
})
|
|
1438
|
+
).toThrow("size operator requires a relation mapping");
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
describe("plans the planner cannot produce", () => {
|
|
1443
|
+
// Input validation on a public function, not policy shapes. Every other assertion in this file
|
|
1444
|
+
// reads its plan from a fixture precisely because a typed plan is a belief about the planner —
|
|
1445
|
+
// but these are malformed by construction, so there is no fixture to read and nothing to
|
|
1446
|
+
// believe. They exist so a caller who hands the adapter a hand-rolled or half-decoded plan gets
|
|
1447
|
+
// an error rather than a filter.
|
|
1448
|
+
//
|
|
1449
|
+
// The test for a shape CEL *can* express does not belong here, whatever its plan looks like:
|
|
1450
|
+
// it belongs in the corpus, where every adapter is asked about it. Two such shapes were
|
|
1451
|
+
// pinned here by hand-built plan before #377 and are now
|
|
1452
|
+
// [#394](https://github.com/cerbos/query-plan-adapters/issues/394).
|
|
1453
|
+
|
|
1454
|
+
const plan = (condition: unknown): PlanResourcesResponse =>
|
|
1455
|
+
({
|
|
1456
|
+
kind: PlanKind.CONDITIONAL,
|
|
1457
|
+
condition: condition as PlanExpressionOperand,
|
|
1458
|
+
cerbosCallId: "",
|
|
1459
|
+
requestId: "",
|
|
1460
|
+
validationErrors: [],
|
|
1461
|
+
metadata: undefined,
|
|
1462
|
+
}) as PlanResourcesResponse;
|
|
1463
|
+
|
|
1464
|
+
test("an unrecognised plan kind", () => {
|
|
1465
|
+
expect(() =>
|
|
1466
|
+
queryPlanToPrisma({
|
|
1467
|
+
queryPlan: { kind: "INVALID_KIND" } as unknown as PlanResourcesResponse,
|
|
1468
|
+
mapper: {},
|
|
1469
|
+
})
|
|
1470
|
+
).toThrow("Invalid query plan.");
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
test("a condition with neither operator nor operands", () => {
|
|
1474
|
+
expect(() => queryPlanToPrisma({ queryPlan: plan({}), mapper: {} })).toThrow(
|
|
1475
|
+
"Invalid Cerbos expression structure"
|
|
1476
|
+
);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
test("an operator this adapter has never heard of", () => {
|
|
1480
|
+
expect(() =>
|
|
1481
|
+
queryPlanToPrisma({
|
|
1482
|
+
queryPlan: plan({ operator: "unsupported", operands: [] }),
|
|
1483
|
+
mapper: {},
|
|
1484
|
+
})
|
|
1485
|
+
).toThrow("Unsupported operator: unsupported");
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
test("an operand that is neither a variable nor a value", () => {
|
|
1489
|
+
expect(() =>
|
|
1490
|
+
queryPlanToPrisma({
|
|
1491
|
+
queryPlan: plan({ operator: "eq", operands: [{}, { value: "test" }] }),
|
|
1492
|
+
mapper: {},
|
|
1493
|
+
})
|
|
1494
|
+
).toThrow("No valid left operand found");
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
test("a ternary with the wrong arity", () => {
|
|
1498
|
+
expect(() =>
|
|
1499
|
+
queryPlanToPrisma({
|
|
1500
|
+
queryPlan: plan({
|
|
1501
|
+
operator: "if",
|
|
1502
|
+
operands: [{ name: "request.resource.attr.aBool" }, { value: true }],
|
|
1503
|
+
}),
|
|
1504
|
+
mapper: MAPPER,
|
|
1505
|
+
})
|
|
1506
|
+
).toThrow(
|
|
1507
|
+
"if (ternary) requires exactly 3 operands (condition, then, else), got 2"
|
|
1508
|
+
);
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
test("a ternary whose condition is not a boolean expression", () => {
|
|
1512
|
+
expect(() =>
|
|
1513
|
+
queryPlanToPrisma({
|
|
1514
|
+
queryPlan: plan({
|
|
1515
|
+
operator: "if",
|
|
1516
|
+
operands: [{ value: null }, { value: true }, { value: false }],
|
|
1517
|
+
}),
|
|
1518
|
+
mapper: MAPPER,
|
|
1519
|
+
})
|
|
1520
|
+
).toThrow("if (ternary) condition must be a boolean expression");
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
test("a comparison against a ternary with three operands", () => {
|
|
1524
|
+
expect(() =>
|
|
1525
|
+
queryPlanToPrisma({
|
|
1526
|
+
queryPlan: plan({
|
|
1527
|
+
operator: "eq",
|
|
1528
|
+
operands: [
|
|
1529
|
+
{
|
|
1530
|
+
operator: "if",
|
|
1531
|
+
operands: [
|
|
1532
|
+
{ name: "request.resource.attr.aBool" },
|
|
1533
|
+
{ value: true },
|
|
1534
|
+
{ value: false },
|
|
1535
|
+
],
|
|
1536
|
+
},
|
|
1537
|
+
{ value: true },
|
|
1538
|
+
{ value: false },
|
|
1539
|
+
],
|
|
1540
|
+
}),
|
|
1541
|
+
mapper: MAPPER,
|
|
1542
|
+
})
|
|
1543
|
+
).toThrow("eq with a ternary requires exactly 2 operands, got 3");
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
test("a macro over a collection value that is not a list", () => {
|
|
1547
|
+
expect(() =>
|
|
1548
|
+
queryPlanToPrisma({
|
|
1549
|
+
queryPlan: plan({
|
|
1550
|
+
operator: "exists",
|
|
1551
|
+
operands: [
|
|
1552
|
+
{ value: "not-a-list" },
|
|
1553
|
+
{
|
|
1554
|
+
operator: "lambda",
|
|
1555
|
+
operands: [
|
|
1556
|
+
{
|
|
1557
|
+
operator: "eq",
|
|
1558
|
+
operands: [
|
|
1559
|
+
{ name: "request.resource.attr.aString" },
|
|
1560
|
+
{ name: "t" },
|
|
1561
|
+
],
|
|
1562
|
+
},
|
|
1563
|
+
{ name: "t" },
|
|
1564
|
+
],
|
|
1565
|
+
},
|
|
1566
|
+
],
|
|
1567
|
+
}),
|
|
1568
|
+
mapper: MAPPER,
|
|
1569
|
+
})
|
|
1570
|
+
).toThrow("exists over a literal collection requires a list value");
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
test("a constant-false predicate the planner should have folded", () => {
|
|
1574
|
+
expect(() =>
|
|
1575
|
+
queryPlanToPrisma({
|
|
1576
|
+
queryPlan: plan({
|
|
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
|
+
);
|
|
1585
|
+
});
|
|
1586
|
+
});
|