@cerbos/orm-prisma 2.0.1 → 3.0.1

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.
@@ -0,0 +1,4093 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const _1 = require(".");
4
+ const globals_1 = require("@jest/globals");
5
+ const client_1 = require("@prisma/client");
6
+ const grpc_1 = require("@cerbos/grpc");
7
+ const prisma = new client_1.PrismaClient();
8
+ const cerbos = new grpc_1.GRPC("127.0.0.1:3593", { tls: false });
9
+ function createConditionalPlan(condition) {
10
+ return {
11
+ kind: _1.PlanKind.CONDITIONAL,
12
+ condition,
13
+ cerbosCallId: "",
14
+ requestId: "",
15
+ validationErrors: [],
16
+ metadata: undefined,
17
+ };
18
+ }
19
+ function getExpressionOperand(expression, index) {
20
+ const operand = expression.operands[index];
21
+ if (!operand) {
22
+ throw new Error(`Missing operand at index ${index}`);
23
+ }
24
+ return operand;
25
+ }
26
+ const fixtureUsers = [
27
+ {
28
+ id: "user1",
29
+ aBool: true,
30
+ aNumber: 1,
31
+ aString: "string",
32
+ },
33
+ {
34
+ id: "user2",
35
+ aBool: true,
36
+ aNumber: 2,
37
+ aString: "string",
38
+ },
39
+ ];
40
+ const fixtureNextLevelResources = [
41
+ {
42
+ id: "nextLevel1",
43
+ aBool: true,
44
+ aNumber: 1,
45
+ aString: "string",
46
+ },
47
+ {
48
+ id: "nextLevel2",
49
+ aBool: false,
50
+ aNumber: 1,
51
+ aString: "string",
52
+ },
53
+ {
54
+ id: "nextLevel3",
55
+ aBool: true,
56
+ aNumber: 1,
57
+ aString: "string",
58
+ },
59
+ ];
60
+ const fixtureTags = [
61
+ {
62
+ id: "tag1",
63
+ name: "public",
64
+ },
65
+ {
66
+ id: "tag2",
67
+ name: "private",
68
+ },
69
+ {
70
+ id: "tag3",
71
+ name: "draft",
72
+ },
73
+ ];
74
+ const fixtureNestedResources = [
75
+ {
76
+ id: "nested1",
77
+ aBool: true,
78
+ aNumber: 1,
79
+ aString: "string",
80
+ nextlevel: {
81
+ connect: {
82
+ id: "nextLevel1",
83
+ },
84
+ },
85
+ },
86
+ {
87
+ id: "nested2",
88
+ aBool: false,
89
+ aNumber: 1,
90
+ aString: "string",
91
+ nextlevel: {
92
+ connect: {
93
+ id: "nextLevel2",
94
+ },
95
+ },
96
+ },
97
+ {
98
+ id: "nested3",
99
+ aBool: true,
100
+ aNumber: 1,
101
+ aString: "string",
102
+ nextlevel: {
103
+ connect: {
104
+ id: "nextLevel3",
105
+ },
106
+ },
107
+ },
108
+ ];
109
+ const fixtureLabels = [
110
+ { id: "label1", name: "important" },
111
+ { id: "label2", name: "archived" },
112
+ { id: "label3", name: "flagged" },
113
+ ];
114
+ const fixtureSubCategories = [
115
+ {
116
+ id: "sub1",
117
+ name: "finance",
118
+ labels: {
119
+ connect: [{ id: "label1" }, { id: "label2" }],
120
+ },
121
+ },
122
+ {
123
+ id: "sub2",
124
+ name: "tech",
125
+ labels: {
126
+ connect: [{ id: "label2" }, { id: "label3" }],
127
+ },
128
+ },
129
+ ];
130
+ const fixtureCategories = [
131
+ {
132
+ id: "cat1",
133
+ name: "business",
134
+ subCategories: {
135
+ connect: [{ id: "sub1" }],
136
+ },
137
+ },
138
+ {
139
+ id: "cat2",
140
+ name: "development",
141
+ subCategories: {
142
+ connect: [{ id: "sub2" }],
143
+ },
144
+ },
145
+ ];
146
+ const fixtureResources = [
147
+ {
148
+ id: "resource1",
149
+ aBool: true,
150
+ aNumber: 1,
151
+ aString: "string",
152
+ aOptionalString: "optionalString",
153
+ createdBy: {
154
+ connect: {
155
+ id: "user1",
156
+ },
157
+ },
158
+ ownedBy: {
159
+ connect: [
160
+ {
161
+ id: "user1",
162
+ },
163
+ ],
164
+ },
165
+ nested: {
166
+ connect: {
167
+ id: "nested1",
168
+ },
169
+ },
170
+ tags: {
171
+ connect: [
172
+ { id: "tag1" }, // public
173
+ ],
174
+ },
175
+ categories: {
176
+ connect: [{ id: "cat1" }],
177
+ },
178
+ },
179
+ {
180
+ id: "resource2",
181
+ aBool: false,
182
+ aNumber: 2,
183
+ aString: "string2",
184
+ createdBy: {
185
+ connect: {
186
+ id: "user2",
187
+ },
188
+ },
189
+ ownedBy: {
190
+ connect: [
191
+ {
192
+ id: "user2",
193
+ },
194
+ ],
195
+ },
196
+ nested: {
197
+ connect: {
198
+ id: "nested3",
199
+ },
200
+ },
201
+ tags: {
202
+ connect: [
203
+ { id: "tag2" }, // private
204
+ ],
205
+ },
206
+ categories: {
207
+ connect: [{ id: "cat2" }],
208
+ },
209
+ },
210
+ {
211
+ id: "resource3",
212
+ aBool: false,
213
+ aNumber: 3,
214
+ aString: "string3",
215
+ createdBy: {
216
+ connect: {
217
+ id: "user1",
218
+ },
219
+ },
220
+ ownedBy: {
221
+ connect: [
222
+ {
223
+ id: "user1",
224
+ },
225
+ {
226
+ id: "user2",
227
+ },
228
+ ],
229
+ },
230
+ nested: {
231
+ connect: {
232
+ id: "nested3",
233
+ },
234
+ },
235
+ tags: {
236
+ connect: [
237
+ { id: "tag1" }, // public
238
+ { id: "tag3" }, // draft
239
+ ],
240
+ },
241
+ categories: {
242
+ connect: [{ id: "cat1" }, { id: "cat2" }],
243
+ },
244
+ },
245
+ ];
246
+ (0, globals_1.beforeAll)(async () => {
247
+ await prisma.resource.deleteMany();
248
+ await prisma.nextLevelNestedResource.deleteMany();
249
+ await prisma.nestedResource.deleteMany();
250
+ await prisma.tag.deleteMany();
251
+ await prisma.user.deleteMany();
252
+ });
253
+ (0, globals_1.beforeEach)(async () => {
254
+ for (const tag of fixtureTags) {
255
+ await prisma.tag.create({ data: tag });
256
+ }
257
+ for (const user of fixtureUsers) {
258
+ await prisma.user.create({ data: user });
259
+ }
260
+ for (const resource of fixtureNextLevelResources) {
261
+ await prisma.nextLevelNestedResource.create({ data: resource });
262
+ }
263
+ for (const resource of fixtureNestedResources) {
264
+ await prisma.nestedResource.create({ data: resource });
265
+ }
266
+ for (const label of fixtureLabels) {
267
+ await prisma.label.create({ data: label });
268
+ }
269
+ for (const subCategory of fixtureSubCategories) {
270
+ await prisma.subCategory.create({ data: subCategory });
271
+ }
272
+ for (const category of fixtureCategories) {
273
+ await prisma.category.create({ data: category });
274
+ }
275
+ for (const resource of fixtureResources) {
276
+ await prisma.resource.create({ data: resource });
277
+ }
278
+ });
279
+ (0, globals_1.afterEach)(async () => {
280
+ await prisma.resource.deleteMany();
281
+ await prisma.nestedResource.deleteMany();
282
+ await prisma.nextLevelNestedResource.deleteMany();
283
+ await prisma.tag.deleteMany();
284
+ await prisma.user.deleteMany();
285
+ await prisma.category.deleteMany();
286
+ await prisma.subCategory.deleteMany();
287
+ await prisma.label.deleteMany();
288
+ });
289
+ // Core Functionality
290
+ (0, globals_1.describe)("Basic Plan Types", () => {
291
+ (0, globals_1.test)("always allowed", async () => {
292
+ const queryPlan = await cerbos.planResources({
293
+ principal: { id: "user1", roles: ["USER"] },
294
+ resource: { kind: "resource" },
295
+ action: "always-allow",
296
+ });
297
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.ALWAYS_ALLOWED);
298
+ const result = (0, _1.queryPlanToPrisma)({
299
+ queryPlan,
300
+ });
301
+ (0, globals_1.expect)(result).toStrictEqual({
302
+ kind: _1.PlanKind.ALWAYS_ALLOWED,
303
+ });
304
+ const query = await prisma.resource.findMany({});
305
+ (0, globals_1.expect)(query.length).toEqual(fixtureResources.length);
306
+ });
307
+ (0, globals_1.test)("always denied", async () => {
308
+ const queryPlan = await cerbos.planResources({
309
+ principal: { id: "user1", roles: ["USER"] },
310
+ resource: { kind: "resource" },
311
+ action: "always-deny",
312
+ });
313
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.ALWAYS_DENIED);
314
+ const result = (0, _1.queryPlanToPrisma)({
315
+ queryPlan,
316
+ mapper: {},
317
+ });
318
+ (0, globals_1.expect)(result).toEqual({
319
+ kind: _1.PlanKind.ALWAYS_DENIED,
320
+ });
321
+ });
322
+ });
323
+ // Field Operations
324
+ (0, globals_1.describe)("Field Operations", () => {
325
+ (0, globals_1.describe)("Basic Field Tests", () => {
326
+ (0, globals_1.test)("conditional - eq", async () => {
327
+ const queryPlan = await cerbos.planResources({
328
+ principal: { id: "user1", roles: ["USER"] },
329
+ resource: { kind: "resource" },
330
+ action: "equal",
331
+ });
332
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
333
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
334
+ operator: "eq",
335
+ operands: [{ name: "request.resource.attr.aBool" }, { value: true }],
336
+ });
337
+ const result = (0, _1.queryPlanToPrisma)({
338
+ queryPlan,
339
+ mapper: {
340
+ "request.resource.attr.aBool": { field: "aBool" },
341
+ },
342
+ });
343
+ (0, globals_1.expect)(result).toStrictEqual({
344
+ kind: _1.PlanKind.CONDITIONAL,
345
+ filters: { aBool: { equals: true } },
346
+ });
347
+ const query = await prisma.resource.findMany({
348
+ where: result.kind === _1.PlanKind.CONDITIONAL ? { ...result.filters } : {},
349
+ });
350
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources.filter((a) => a.aBool).map((r) => r.id));
351
+ });
352
+ (0, globals_1.test)("conditional - eq - inverted order", async () => {
353
+ const queryPlan = await cerbos.planResources({
354
+ principal: { id: "user1", roles: ["USER"] },
355
+ resource: { kind: "resource" },
356
+ action: "equal",
357
+ });
358
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
359
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
360
+ operator: "eq",
361
+ operands: [{ name: "request.resource.attr.aBool" }, { value: true }],
362
+ });
363
+ const typeQp = queryPlan;
364
+ const invertedQueryPlan = {
365
+ ...typeQp,
366
+ condition: {
367
+ ...typeQp.condition,
368
+ operands: [
369
+ getExpressionOperand(typeQp.condition, 1),
370
+ getExpressionOperand(typeQp.condition, 0),
371
+ ],
372
+ },
373
+ };
374
+ const result = (0, _1.queryPlanToPrisma)({
375
+ queryPlan: invertedQueryPlan,
376
+ mapper: {
377
+ "request.resource.attr.aBool": { field: "aBool" },
378
+ },
379
+ });
380
+ (0, globals_1.expect)(result).toStrictEqual({
381
+ kind: _1.PlanKind.CONDITIONAL,
382
+ filters: { aBool: { equals: true } },
383
+ });
384
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
385
+ throw new Error("Expected CONDITIONAL result");
386
+ }
387
+ const query = await prisma.resource.findMany({
388
+ where: { ...result.filters },
389
+ });
390
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources.filter((a) => a.aBool).map((r) => r.id));
391
+ });
392
+ (0, globals_1.test)("conditional - ne", async () => {
393
+ const queryPlan = await cerbos.planResources({
394
+ principal: { id: "user1", roles: ["USER"] },
395
+ resource: { kind: "resource" },
396
+ action: "ne",
397
+ });
398
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
399
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
400
+ operator: "ne",
401
+ operands: [
402
+ { name: "request.resource.attr.aString" },
403
+ { value: "string" },
404
+ ],
405
+ });
406
+ const result = (0, _1.queryPlanToPrisma)({
407
+ queryPlan,
408
+ mapper: {
409
+ "request.resource.attr.aString": { field: "aString" },
410
+ },
411
+ });
412
+ (0, globals_1.expect)(result).toStrictEqual({
413
+ kind: _1.PlanKind.CONDITIONAL,
414
+ filters: { aString: { not: "string" } },
415
+ });
416
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
417
+ throw new Error("Expected CONDITIONAL result");
418
+ }
419
+ const query = await prisma.resource.findMany({
420
+ where: { ...result.filters },
421
+ });
422
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources.filter((a) => a.aString != "string").map((r) => r.id));
423
+ });
424
+ (0, globals_1.test)("conditional - explicit-deny", async () => {
425
+ const queryPlan = await cerbos.planResources({
426
+ principal: { id: "user1", roles: ["USER"] },
427
+ resource: { kind: "resource" },
428
+ action: "explicit-deny",
429
+ });
430
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
431
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
432
+ operator: "not",
433
+ operands: [
434
+ {
435
+ operator: "eq",
436
+ operands: [
437
+ { name: "request.resource.attr.aBool" },
438
+ { value: true },
439
+ ],
440
+ },
441
+ ],
442
+ });
443
+ const result = (0, _1.queryPlanToPrisma)({
444
+ queryPlan,
445
+ mapper: {
446
+ "request.resource.attr.aBool": { field: "aBool" },
447
+ },
448
+ });
449
+ (0, globals_1.expect)(result).toStrictEqual({
450
+ kind: _1.PlanKind.CONDITIONAL,
451
+ filters: { NOT: { aBool: { equals: true } } },
452
+ });
453
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
454
+ throw new Error("Expected CONDITIONAL result");
455
+ }
456
+ const query = await prisma.resource.findMany({
457
+ where: { ...result.filters },
458
+ });
459
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources.filter((a) => !a.aBool).map((r) => r.id));
460
+ });
461
+ });
462
+ (0, globals_1.describe)("Comparison Tests", () => {
463
+ (0, globals_1.test)("conditional - gt", async () => {
464
+ const queryPlan = await cerbos.planResources({
465
+ principal: { id: "user1", roles: ["USER"] },
466
+ resource: { kind: "resource" },
467
+ action: "gt",
468
+ });
469
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
470
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
471
+ operator: "gt",
472
+ operands: [{ name: "request.resource.attr.aNumber" }, { value: 1 }],
473
+ });
474
+ const result = (0, _1.queryPlanToPrisma)({
475
+ queryPlan,
476
+ mapper: {
477
+ "request.resource.attr.aNumber": { field: "aNumber" },
478
+ },
479
+ });
480
+ (0, globals_1.expect)(result).toStrictEqual({
481
+ kind: _1.PlanKind.CONDITIONAL,
482
+ filters: {
483
+ aNumber: { gt: 1 },
484
+ },
485
+ });
486
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
487
+ throw new Error("Expected CONDITIONAL result");
488
+ }
489
+ const query = await prisma.resource.findMany({
490
+ where: { ...result.filters },
491
+ });
492
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
493
+ .filter((r) => {
494
+ return r.aNumber > 1;
495
+ })
496
+ .map((r) => r.id));
497
+ });
498
+ (0, globals_1.test)("conditional - lt", async () => {
499
+ const queryPlan = await cerbos.planResources({
500
+ principal: { id: "user1", roles: ["USER"] },
501
+ resource: { kind: "resource" },
502
+ action: "lt",
503
+ });
504
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
505
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
506
+ operator: "lt",
507
+ operands: [{ name: "request.resource.attr.aNumber" }, { value: 2 }],
508
+ });
509
+ const result = (0, _1.queryPlanToPrisma)({
510
+ queryPlan,
511
+ mapper: {
512
+ "request.resource.attr.aNumber": { field: "aNumber" },
513
+ },
514
+ });
515
+ (0, globals_1.expect)(result).toStrictEqual({
516
+ kind: _1.PlanKind.CONDITIONAL,
517
+ filters: {
518
+ aNumber: { lt: 2 },
519
+ },
520
+ });
521
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
522
+ throw new Error("Expected CONDITIONAL result");
523
+ }
524
+ const query = await prisma.resource.findMany({
525
+ where: { ...result.filters },
526
+ });
527
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
528
+ .filter((r) => {
529
+ return r.aNumber < 2;
530
+ })
531
+ .map((r) => r.id));
532
+ });
533
+ (0, globals_1.test)("conditional - gte", async () => {
534
+ const queryPlan = await cerbos.planResources({
535
+ principal: { id: "user1", roles: ["USER"] },
536
+ resource: { kind: "resource" },
537
+ action: "gte",
538
+ });
539
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
540
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
541
+ operator: "ge",
542
+ operands: [{ name: "request.resource.attr.aNumber" }, { value: 1 }],
543
+ });
544
+ const result = (0, _1.queryPlanToPrisma)({
545
+ queryPlan,
546
+ mapper: {
547
+ "request.resource.attr.aNumber": { field: "aNumber" },
548
+ },
549
+ });
550
+ (0, globals_1.expect)(result).toStrictEqual({
551
+ kind: _1.PlanKind.CONDITIONAL,
552
+ filters: {
553
+ aNumber: { gte: 1 },
554
+ },
555
+ });
556
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
557
+ throw new Error("Expected CONDITIONAL result");
558
+ }
559
+ const query = await prisma.resource.findMany({
560
+ where: { ...result.filters },
561
+ });
562
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
563
+ .filter((r) => {
564
+ return r.aNumber >= 1;
565
+ })
566
+ .map((r) => r.id));
567
+ });
568
+ (0, globals_1.test)("conditional - lte", async () => {
569
+ const queryPlan = await cerbos.planResources({
570
+ principal: { id: "user1", roles: ["USER"] },
571
+ resource: { kind: "resource" },
572
+ action: "lte",
573
+ });
574
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
575
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
576
+ operator: "le",
577
+ operands: [{ name: "request.resource.attr.aNumber" }, { value: 2 }],
578
+ });
579
+ const result = (0, _1.queryPlanToPrisma)({
580
+ queryPlan,
581
+ mapper: {
582
+ "request.resource.attr.aNumber": { field: "aNumber" },
583
+ },
584
+ });
585
+ (0, globals_1.expect)(result).toStrictEqual({
586
+ kind: _1.PlanKind.CONDITIONAL,
587
+ filters: {
588
+ aNumber: { lte: 2 },
589
+ },
590
+ });
591
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
592
+ throw new Error("Expected CONDITIONAL result");
593
+ }
594
+ const query = await prisma.resource.findMany({
595
+ where: { ...result.filters },
596
+ });
597
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
598
+ .filter((r) => {
599
+ return r.aNumber <= 2;
600
+ })
601
+ .map((r) => r.id));
602
+ });
603
+ });
604
+ (0, globals_1.describe)("String Tests", () => {
605
+ (0, globals_1.test)("conditional - contains", async () => {
606
+ const queryPlan = await cerbos.planResources({
607
+ principal: { id: "user1", roles: ["USER"] },
608
+ resource: { kind: "resource" },
609
+ action: "contains",
610
+ });
611
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
612
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
613
+ operands: [
614
+ {
615
+ name: "request.resource.attr.aString",
616
+ },
617
+ {
618
+ value: "str",
619
+ },
620
+ ],
621
+ operator: "contains",
622
+ });
623
+ const result = (0, _1.queryPlanToPrisma)({
624
+ queryPlan,
625
+ mapper: {
626
+ "request.resource.attr.aString": { field: "aString" },
627
+ },
628
+ });
629
+ (0, globals_1.expect)(result).toStrictEqual({
630
+ kind: _1.PlanKind.CONDITIONAL,
631
+ filters: { aString: { contains: "str" } },
632
+ });
633
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
634
+ throw new Error("Expected CONDITIONAL result");
635
+ }
636
+ const query = await prisma.resource.findMany({
637
+ where: { ...result.filters },
638
+ });
639
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
640
+ .filter((a) => a.aString.includes("str"))
641
+ .map((r) => r.id));
642
+ });
643
+ (0, globals_1.test)("conditional - startsWith", async () => {
644
+ const queryPlan = await cerbos.planResources({
645
+ principal: { id: "user1", roles: ["USER"] },
646
+ resource: { kind: "resource" },
647
+ action: "starts-with",
648
+ });
649
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
650
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
651
+ operands: [
652
+ {
653
+ name: "request.resource.attr.aString",
654
+ },
655
+ {
656
+ value: "str",
657
+ },
658
+ ],
659
+ operator: "startsWith",
660
+ });
661
+ const result = (0, _1.queryPlanToPrisma)({
662
+ queryPlan,
663
+ mapper: {
664
+ "request.resource.attr.aString": { field: "aString" },
665
+ },
666
+ });
667
+ (0, globals_1.expect)(result).toStrictEqual({
668
+ kind: _1.PlanKind.CONDITIONAL,
669
+ filters: { aString: { startsWith: "str" } },
670
+ });
671
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
672
+ throw new Error("Expected CONDITIONAL result");
673
+ }
674
+ const query = await prisma.resource.findMany({
675
+ where: { ...result.filters },
676
+ });
677
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
678
+ .filter((a) => a.aString.startsWith("str"))
679
+ .map((r) => r.id));
680
+ });
681
+ (0, globals_1.test)("conditional - endsWith", async () => {
682
+ const queryPlan = await cerbos.planResources({
683
+ principal: { id: "user1", roles: ["USER"] },
684
+ resource: { kind: "resource" },
685
+ action: "ends-with",
686
+ });
687
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
688
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
689
+ operands: [
690
+ {
691
+ name: "request.resource.attr.aString",
692
+ },
693
+ {
694
+ value: "ing",
695
+ },
696
+ ],
697
+ operator: "endsWith",
698
+ });
699
+ const result = (0, _1.queryPlanToPrisma)({
700
+ queryPlan,
701
+ mapper: {
702
+ "request.resource.attr.aString": { field: "aString" },
703
+ },
704
+ });
705
+ (0, globals_1.expect)(result).toStrictEqual({
706
+ kind: _1.PlanKind.CONDITIONAL,
707
+ filters: { aString: { endsWith: "ing" } },
708
+ });
709
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
710
+ throw new Error("Expected CONDITIONAL result");
711
+ }
712
+ const query = await prisma.resource.findMany({
713
+ where: { ...result.filters },
714
+ });
715
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
716
+ .filter((a) => a.aString.endsWith("ing"))
717
+ .map((r) => r.id));
718
+ });
719
+ (0, globals_1.test)("conditional - isSet", async () => {
720
+ const queryPlan = await cerbos.planResources({
721
+ principal: { id: "user1", roles: ["USER"] },
722
+ resource: { kind: "resource" },
723
+ action: "is-set",
724
+ });
725
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
726
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
727
+ operands: [
728
+ {
729
+ name: "request.resource.attr.aOptionalString",
730
+ },
731
+ {
732
+ value: null,
733
+ },
734
+ ],
735
+ operator: "ne",
736
+ });
737
+ const result = (0, _1.queryPlanToPrisma)({
738
+ queryPlan,
739
+ mapper: {
740
+ "request.resource.attr.aOptionalString": { field: "aOptionalString" },
741
+ },
742
+ });
743
+ (0, globals_1.expect)(result).toStrictEqual({
744
+ kind: _1.PlanKind.CONDITIONAL,
745
+ filters: { aOptionalString: { not: null } },
746
+ });
747
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
748
+ throw new Error("Expected CONDITIONAL result");
749
+ }
750
+ const query = await prisma.resource.findMany({
751
+ where: { ...result.filters },
752
+ });
753
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources.filter((a) => a.aOptionalString).map((r) => r.id));
754
+ });
755
+ });
756
+ });
757
+ // Collection Operations
758
+ (0, globals_1.describe)("Collection Operations", () => {
759
+ (0, globals_1.describe)("Basic Collections", () => {
760
+ (0, globals_1.test)("conditional - in", async () => {
761
+ const queryPlan = await cerbos.planResources({
762
+ principal: { id: "user1", roles: ["USER"] },
763
+ resource: { kind: "resource" },
764
+ action: "in",
765
+ });
766
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
767
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
768
+ operator: "in",
769
+ operands: [
770
+ { name: "request.resource.attr.aString" },
771
+ { value: ["string", "anotherString"] },
772
+ ],
773
+ });
774
+ const result = (0, _1.queryPlanToPrisma)({
775
+ queryPlan,
776
+ mapper: {
777
+ "request.resource.attr.aString": { field: "aString" },
778
+ },
779
+ });
780
+ (0, globals_1.expect)(result).toStrictEqual({
781
+ kind: _1.PlanKind.CONDITIONAL,
782
+ filters: {
783
+ aString: { in: ["string", "anotherString"] },
784
+ },
785
+ });
786
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
787
+ throw new Error("Expected CONDITIONAL result");
788
+ }
789
+ const query = await prisma.resource.findMany({
790
+ where: { ...result.filters },
791
+ });
792
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
793
+ .filter((r) => {
794
+ return ["string", "anotherString"].includes(r.aString);
795
+ })
796
+ .map((r) => r.id));
797
+ });
798
+ (0, globals_1.test)("conditional - in - scalar value", async () => {
799
+ const queryPlan = createConditionalPlan({
800
+ operator: "in",
801
+ operands: [
802
+ { name: "request.resource.attr.aString" },
803
+ { value: "string" },
804
+ ],
805
+ });
806
+ const result = (0, _1.queryPlanToPrisma)({
807
+ queryPlan,
808
+ mapper: {
809
+ "request.resource.attr.aString": { field: "aString" },
810
+ },
811
+ });
812
+ (0, globals_1.expect)(result).toStrictEqual({
813
+ kind: _1.PlanKind.CONDITIONAL,
814
+ filters: { aString: "string" },
815
+ });
816
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
817
+ throw new Error("Expected CONDITIONAL result");
818
+ }
819
+ const query = await prisma.resource.findMany({
820
+ where: { ...result.filters },
821
+ });
822
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(fixtureResources
823
+ .filter((r) => r.aString === "string")
824
+ .map((r) => r.id)
825
+ .sort());
826
+ });
827
+ (0, globals_1.test)("conditional - relation in", async () => {
828
+ const queryPlan = createConditionalPlan({
829
+ operator: "in",
830
+ operands: [
831
+ { name: "request.resource.attr.categories.name" },
832
+ { value: ["business"] },
833
+ ],
834
+ });
835
+ const result = (0, _1.queryPlanToPrisma)({
836
+ queryPlan,
837
+ mapper: {
838
+ "request.resource.attr.categories": {
839
+ relation: {
840
+ name: "categories",
841
+ type: "many",
842
+ fields: {
843
+ name: { field: "name" },
844
+ },
845
+ },
846
+ },
847
+ },
848
+ });
849
+ (0, globals_1.expect)(result).toStrictEqual({
850
+ kind: _1.PlanKind.CONDITIONAL,
851
+ filters: {
852
+ categories: {
853
+ some: {
854
+ name: "business",
855
+ },
856
+ },
857
+ },
858
+ });
859
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
860
+ throw new Error("Expected CONDITIONAL result");
861
+ }
862
+ const query = await prisma.resource.findMany({
863
+ where: { ...result.filters },
864
+ });
865
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(fixtureResources
866
+ .filter((resource) => {
867
+ const categoryRefs = resource.categories?.connect ??
868
+ [];
869
+ return categoryRefs.some((categoryRef) => {
870
+ const category = fixtureCategories.find((fc) => fc.id === categoryRef.id);
871
+ return category?.name === "business";
872
+ });
873
+ })
874
+ .map((r) => r.id)
875
+ .sort());
876
+ });
877
+ (0, globals_1.test)("conditional - relation in multiple values", async () => {
878
+ const queryPlan = createConditionalPlan({
879
+ operator: "in",
880
+ operands: [
881
+ { name: "request.resource.attr.categories.name" },
882
+ { value: ["business", "development"] },
883
+ ],
884
+ });
885
+ const result = (0, _1.queryPlanToPrisma)({
886
+ queryPlan,
887
+ mapper: {
888
+ "request.resource.attr.categories": {
889
+ relation: {
890
+ name: "categories",
891
+ type: "many",
892
+ fields: {
893
+ name: { field: "name" },
894
+ },
895
+ },
896
+ },
897
+ },
898
+ });
899
+ (0, globals_1.expect)(result).toStrictEqual({
900
+ kind: _1.PlanKind.CONDITIONAL,
901
+ filters: {
902
+ categories: {
903
+ some: {
904
+ name: { in: ["business", "development"] },
905
+ },
906
+ },
907
+ },
908
+ });
909
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
910
+ throw new Error("Expected CONDITIONAL result");
911
+ }
912
+ const query = await prisma.resource.findMany({
913
+ where: { ...result.filters },
914
+ });
915
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(fixtureResources
916
+ .filter((resource) => {
917
+ const categoryRefs = resource.categories?.connect ??
918
+ [];
919
+ return categoryRefs.some((categoryRef) => {
920
+ const category = fixtureCategories.find((fc) => fc.id === categoryRef.id);
921
+ return ["business", "development"].includes(category?.name ?? "");
922
+ });
923
+ })
924
+ .map((r) => r.id)
925
+ .sort());
926
+ });
927
+ (0, globals_1.test)("conditional - except relation subset", async () => {
928
+ const queryPlan = createConditionalPlan({
929
+ operator: "except",
930
+ operands: [
931
+ { name: "request.resource.attr.categories" },
932
+ {
933
+ operator: "lambda",
934
+ operands: [
935
+ {
936
+ operator: "eq",
937
+ operands: [
938
+ { name: "cat.name" },
939
+ { value: "business" },
940
+ ],
941
+ },
942
+ { name: "cat" },
943
+ ],
944
+ },
945
+ ],
946
+ });
947
+ const result = (0, _1.queryPlanToPrisma)({
948
+ queryPlan,
949
+ mapper: {
950
+ "request.resource.attr.categories": {
951
+ relation: {
952
+ name: "categories",
953
+ type: "many",
954
+ fields: {
955
+ name: { field: "name" },
956
+ },
957
+ },
958
+ },
959
+ },
960
+ });
961
+ (0, globals_1.expect)(result).toStrictEqual({
962
+ kind: _1.PlanKind.CONDITIONAL,
963
+ filters: {
964
+ categories: {
965
+ some: {
966
+ NOT: {
967
+ name: { equals: "business" },
968
+ },
969
+ },
970
+ },
971
+ },
972
+ });
973
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
974
+ throw new Error("Expected CONDITIONAL result");
975
+ }
976
+ const query = await prisma.resource.findMany({
977
+ where: { ...result.filters },
978
+ });
979
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(fixtureResources
980
+ .filter((resource) => {
981
+ const categoryRefs = resource.categories?.connect ??
982
+ [];
983
+ return categoryRefs.some((categoryRef) => {
984
+ const category = fixtureCategories.find((fc) => fc.id === categoryRef.id);
985
+ return category?.name !== "business";
986
+ });
987
+ })
988
+ .map((r) => r.id)
989
+ .sort());
990
+ });
991
+ (0, globals_1.test)("conditional - except nested relation subset", async () => {
992
+ const queryPlan = createConditionalPlan({
993
+ operator: "except",
994
+ operands: [
995
+ { name: "request.resource.attr.categories" },
996
+ {
997
+ operator: "lambda",
998
+ operands: [
999
+ {
1000
+ operator: "exists",
1001
+ operands: [
1002
+ { name: "cat.subCategories" },
1003
+ {
1004
+ operator: "lambda",
1005
+ operands: [
1006
+ {
1007
+ operator: "eq",
1008
+ operands: [
1009
+ { name: "sub.name" },
1010
+ { value: "finance" },
1011
+ ],
1012
+ },
1013
+ { name: "sub" },
1014
+ ],
1015
+ },
1016
+ ],
1017
+ },
1018
+ { name: "cat" },
1019
+ ],
1020
+ },
1021
+ ],
1022
+ });
1023
+ const result = (0, _1.queryPlanToPrisma)({
1024
+ queryPlan,
1025
+ mapper: {
1026
+ "request.resource.attr.categories": {
1027
+ relation: {
1028
+ name: "categories",
1029
+ type: "many",
1030
+ fields: {
1031
+ subCategories: {
1032
+ relation: {
1033
+ name: "subCategories",
1034
+ type: "many",
1035
+ fields: {
1036
+ name: { field: "name" },
1037
+ },
1038
+ },
1039
+ },
1040
+ },
1041
+ },
1042
+ },
1043
+ },
1044
+ });
1045
+ (0, globals_1.expect)(result).toStrictEqual({
1046
+ kind: _1.PlanKind.CONDITIONAL,
1047
+ filters: {
1048
+ categories: {
1049
+ some: {
1050
+ NOT: {
1051
+ subCategories: {
1052
+ some: {
1053
+ name: { equals: "finance" },
1054
+ },
1055
+ },
1056
+ },
1057
+ },
1058
+ },
1059
+ },
1060
+ });
1061
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1062
+ throw new Error("Expected CONDITIONAL result");
1063
+ }
1064
+ const query = await prisma.resource.findMany({
1065
+ where: { ...result.filters },
1066
+ });
1067
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(fixtureResources
1068
+ .filter((resource) => {
1069
+ const categoryRefs = resource.categories?.connect ??
1070
+ [];
1071
+ return categoryRefs.some((categoryRef) => {
1072
+ const category = fixtureCategories.find((fc) => fc.id === categoryRef.id);
1073
+ if (!category) {
1074
+ return false;
1075
+ }
1076
+ const subCategoryRefs = category.subCategories?.connect ??
1077
+ [];
1078
+ return !subCategoryRefs.some((subCategoryRef) => {
1079
+ const subCategory = fixtureSubCategories.find((fsc) => fsc.id === subCategoryRef.id);
1080
+ return subCategory?.name === "finance";
1081
+ });
1082
+ });
1083
+ })
1084
+ .map((r) => r.id)
1085
+ .sort());
1086
+ });
1087
+ (0, globals_1.test)("conditional - exists single", async () => {
1088
+ const queryPlan = await cerbos.planResources({
1089
+ principal: { id: "user1", roles: ["USER"] },
1090
+ resource: { kind: "resource" },
1091
+ action: "exists-single",
1092
+ });
1093
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1094
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1095
+ operator: "exists",
1096
+ operands: [
1097
+ {
1098
+ name: "request.resource.attr.tags",
1099
+ },
1100
+ {
1101
+ operator: "lambda",
1102
+ operands: [
1103
+ {
1104
+ operator: "eq",
1105
+ operands: [
1106
+ {
1107
+ name: "tag.id",
1108
+ },
1109
+ {
1110
+ value: "tag1",
1111
+ },
1112
+ ],
1113
+ },
1114
+ {
1115
+ name: "tag",
1116
+ },
1117
+ ],
1118
+ },
1119
+ ],
1120
+ });
1121
+ const result = (0, _1.queryPlanToPrisma)({
1122
+ queryPlan,
1123
+ mapper: {
1124
+ "request.resource.attr.tags": {
1125
+ relation: {
1126
+ name: "tags",
1127
+ type: "many",
1128
+ },
1129
+ },
1130
+ },
1131
+ });
1132
+ (0, globals_1.expect)(result).toStrictEqual({
1133
+ kind: _1.PlanKind.CONDITIONAL,
1134
+ filters: {
1135
+ tags: {
1136
+ some: {
1137
+ id: {
1138
+ equals: "tag1",
1139
+ },
1140
+ },
1141
+ },
1142
+ },
1143
+ });
1144
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1145
+ throw new Error("Expected CONDITIONAL result");
1146
+ }
1147
+ const query = await prisma.resource.findMany({
1148
+ where: { ...result.filters },
1149
+ });
1150
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1151
+ .filter((a) => Array.isArray(a.tags?.connect) &&
1152
+ a.tags?.connect
1153
+ .map((t) => {
1154
+ return fixtureTags.find((f) => f.id === t.id);
1155
+ })
1156
+ .filter((t) => t?.id == "tag1").length > 0)
1157
+ .map((r) => r.id));
1158
+ });
1159
+ (0, globals_1.test)("conditional - exists multiple", async () => {
1160
+ const queryPlan = await cerbos.planResources({
1161
+ principal: { id: "user1", roles: ["USER"] },
1162
+ resource: { kind: "resource" },
1163
+ action: "exists-multiple",
1164
+ });
1165
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1166
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1167
+ operator: "exists",
1168
+ operands: [
1169
+ {
1170
+ name: "request.resource.attr.tags",
1171
+ },
1172
+ {
1173
+ operator: "lambda",
1174
+ operands: [
1175
+ {
1176
+ operator: "and",
1177
+ operands: [
1178
+ {
1179
+ operator: "eq",
1180
+ operands: [
1181
+ {
1182
+ name: "tag.id",
1183
+ },
1184
+ {
1185
+ value: "tag1",
1186
+ },
1187
+ ],
1188
+ },
1189
+ {
1190
+ operator: "eq",
1191
+ operands: [
1192
+ {
1193
+ name: "tag.name",
1194
+ },
1195
+ {
1196
+ value: "public",
1197
+ },
1198
+ ],
1199
+ },
1200
+ ],
1201
+ },
1202
+ {
1203
+ name: "tag",
1204
+ },
1205
+ ],
1206
+ },
1207
+ ],
1208
+ });
1209
+ const result = (0, _1.queryPlanToPrisma)({
1210
+ queryPlan,
1211
+ mapper: {
1212
+ "request.resource.attr.tags": {
1213
+ relation: {
1214
+ name: "tags",
1215
+ type: "many",
1216
+ },
1217
+ },
1218
+ },
1219
+ });
1220
+ (0, globals_1.expect)(result).toStrictEqual({
1221
+ kind: _1.PlanKind.CONDITIONAL,
1222
+ filters: {
1223
+ tags: {
1224
+ some: {
1225
+ AND: [
1226
+ {
1227
+ id: {
1228
+ equals: "tag1",
1229
+ },
1230
+ },
1231
+ {
1232
+ name: {
1233
+ equals: "public",
1234
+ },
1235
+ },
1236
+ ],
1237
+ },
1238
+ },
1239
+ },
1240
+ });
1241
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1242
+ throw new Error("Expected CONDITIONAL result");
1243
+ }
1244
+ const query = await prisma.resource.findMany({
1245
+ where: { ...result.filters },
1246
+ });
1247
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1248
+ .filter((a) => Array.isArray(a.tags?.connect) &&
1249
+ a.tags?.connect
1250
+ .map((t) => {
1251
+ return fixtureTags.find((f) => f.id === t.id);
1252
+ })
1253
+ .filter((t) => t?.id === "tag1" && t?.name === "public")
1254
+ .length > 0)
1255
+ .map((r) => r.id));
1256
+ });
1257
+ (0, globals_1.test)("conditional - exists_one", async () => {
1258
+ const queryPlan = await cerbos.planResources({
1259
+ principal: { id: "user1", roles: ["USER"] },
1260
+ resource: { kind: "resource" },
1261
+ action: "exists-one",
1262
+ });
1263
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1264
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1265
+ operator: "exists_one",
1266
+ operands: [
1267
+ {
1268
+ name: "request.resource.attr.tags",
1269
+ },
1270
+ {
1271
+ operator: "lambda",
1272
+ operands: [
1273
+ {
1274
+ operator: "eq",
1275
+ operands: [
1276
+ {
1277
+ name: "tag.name",
1278
+ },
1279
+ {
1280
+ value: "public",
1281
+ },
1282
+ ],
1283
+ },
1284
+ {
1285
+ name: "tag",
1286
+ },
1287
+ ],
1288
+ },
1289
+ ],
1290
+ });
1291
+ const result = (0, _1.queryPlanToPrisma)({
1292
+ queryPlan,
1293
+ mapper: {
1294
+ "request.resource.attr.tags": {
1295
+ relation: {
1296
+ name: "tags",
1297
+ type: "many",
1298
+ },
1299
+ },
1300
+ },
1301
+ });
1302
+ (0, globals_1.expect)(result).toStrictEqual({
1303
+ kind: _1.PlanKind.CONDITIONAL,
1304
+ filters: {
1305
+ tags: {
1306
+ some: {
1307
+ name: { equals: "public" },
1308
+ },
1309
+ },
1310
+ AND: [
1311
+ {
1312
+ tags: {
1313
+ every: {
1314
+ OR: [
1315
+ { name: { equals: "public" } },
1316
+ { NOT: { name: { equals: "public" } } },
1317
+ ],
1318
+ },
1319
+ },
1320
+ },
1321
+ ],
1322
+ },
1323
+ });
1324
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1325
+ throw new Error("Expected CONDITIONAL result");
1326
+ }
1327
+ const query = await prisma.resource.findMany({
1328
+ where: { ...result.filters },
1329
+ });
1330
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1331
+ .filter((a) => Array.isArray(a.tags?.connect) &&
1332
+ a.tags?.connect
1333
+ .map((t) => {
1334
+ return fixtureTags.find((f) => f.id === t.id);
1335
+ })
1336
+ .filter((t) => t?.id === "tag1" && t?.name === "public")
1337
+ .length > 0)
1338
+ .map((r) => r.id));
1339
+ });
1340
+ (0, globals_1.test)("conditional - all", async () => {
1341
+ const queryPlan = await cerbos.planResources({
1342
+ principal: { id: "user1", roles: ["USER"] },
1343
+ resource: { kind: "resource" },
1344
+ action: "all",
1345
+ });
1346
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1347
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1348
+ operator: "all",
1349
+ operands: [
1350
+ {
1351
+ name: "request.resource.attr.tags",
1352
+ },
1353
+ {
1354
+ operator: "lambda",
1355
+ operands: [
1356
+ {
1357
+ operator: "eq",
1358
+ operands: [
1359
+ {
1360
+ name: "tag.name",
1361
+ },
1362
+ {
1363
+ value: "public",
1364
+ },
1365
+ ],
1366
+ },
1367
+ {
1368
+ name: "tag",
1369
+ },
1370
+ ],
1371
+ },
1372
+ ],
1373
+ });
1374
+ const result = (0, _1.queryPlanToPrisma)({
1375
+ queryPlan,
1376
+ mapper: {
1377
+ "request.resource.attr.tags": {
1378
+ relation: {
1379
+ name: "tags",
1380
+ type: "many",
1381
+ },
1382
+ },
1383
+ },
1384
+ });
1385
+ (0, globals_1.expect)(result).toStrictEqual({
1386
+ kind: _1.PlanKind.CONDITIONAL,
1387
+ filters: {
1388
+ tags: {
1389
+ every: {
1390
+ name: { equals: "public" },
1391
+ },
1392
+ },
1393
+ },
1394
+ });
1395
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1396
+ throw new Error("Expected CONDITIONAL result");
1397
+ }
1398
+ const query = await prisma.resource.findMany({
1399
+ where: { ...result.filters },
1400
+ });
1401
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1402
+ .filter((a) => Array.isArray(a.tags?.connect) &&
1403
+ a.tags?.connect
1404
+ .map((t) => {
1405
+ return fixtureTags.find((f) => f.id === t.id);
1406
+ })
1407
+ .every((t) => t?.name === "public"))
1408
+ .map((r) => r.id));
1409
+ });
1410
+ (0, globals_1.test)("conditional - filter", async () => {
1411
+ const queryPlan = await cerbos.planResources({
1412
+ principal: { id: "user1", roles: ["USER"] },
1413
+ resource: { kind: "resource" },
1414
+ action: "filter",
1415
+ });
1416
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1417
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1418
+ operator: "filter",
1419
+ operands: [
1420
+ {
1421
+ name: "request.resource.attr.tags",
1422
+ },
1423
+ {
1424
+ operator: "lambda",
1425
+ operands: [
1426
+ {
1427
+ operator: "eq",
1428
+ operands: [
1429
+ {
1430
+ name: "tag.name",
1431
+ },
1432
+ {
1433
+ value: "public",
1434
+ },
1435
+ ],
1436
+ },
1437
+ {
1438
+ name: "tag",
1439
+ },
1440
+ ],
1441
+ },
1442
+ ],
1443
+ });
1444
+ const result = (0, _1.queryPlanToPrisma)({
1445
+ queryPlan,
1446
+ mapper: {
1447
+ "request.resource.attr.tags": {
1448
+ relation: {
1449
+ name: "tags",
1450
+ type: "many",
1451
+ },
1452
+ },
1453
+ },
1454
+ });
1455
+ (0, globals_1.expect)(result).toStrictEqual({
1456
+ kind: _1.PlanKind.CONDITIONAL,
1457
+ filters: {
1458
+ tags: {
1459
+ some: {
1460
+ name: { equals: "public" },
1461
+ },
1462
+ },
1463
+ },
1464
+ });
1465
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1466
+ throw new Error("Expected CONDITIONAL result");
1467
+ }
1468
+ const query = await prisma.resource.findMany({
1469
+ where: { ...result.filters },
1470
+ });
1471
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1472
+ .filter((a) => Array.isArray(a.tags?.connect) &&
1473
+ a.tags?.connect
1474
+ .map((t) => {
1475
+ return fixtureTags.find((f) => f.id === t.id);
1476
+ })
1477
+ .filter((t) => t?.name === "public").length > 0)
1478
+ .map((r) => r.id));
1479
+ });
1480
+ (0, globals_1.test)("conditional - map collection", async () => {
1481
+ const queryPlan = await cerbos.planResources({
1482
+ principal: { id: "user1", roles: ["USER"] },
1483
+ resource: { kind: "resource" },
1484
+ action: "map-collection",
1485
+ });
1486
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1487
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1488
+ operator: "hasIntersection",
1489
+ operands: [
1490
+ {
1491
+ operator: "map",
1492
+ operands: [
1493
+ { name: "request.resource.attr.tags" },
1494
+ {
1495
+ operator: "lambda",
1496
+ operands: [{ name: "tag.name" }, { name: "tag" }],
1497
+ },
1498
+ ],
1499
+ },
1500
+ { value: ["public", "private"] },
1501
+ ],
1502
+ });
1503
+ const result = (0, _1.queryPlanToPrisma)({
1504
+ queryPlan,
1505
+ mapper: {
1506
+ "request.resource.attr.tags": {
1507
+ relation: {
1508
+ name: "tags",
1509
+ type: "many",
1510
+ fields: {
1511
+ name: { field: "name" },
1512
+ },
1513
+ },
1514
+ },
1515
+ },
1516
+ });
1517
+ (0, globals_1.expect)(result).toStrictEqual({
1518
+ kind: _1.PlanKind.CONDITIONAL,
1519
+ filters: {
1520
+ tags: {
1521
+ some: {
1522
+ name: { in: ["public", "private"] },
1523
+ },
1524
+ },
1525
+ },
1526
+ });
1527
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1528
+ throw new Error("Expected CONDITIONAL result");
1529
+ }
1530
+ const query = await prisma.resource.findMany({
1531
+ where: { ...result.filters },
1532
+ });
1533
+ // Should return resources that have either "public" or "private" tags
1534
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1535
+ .filter((r) => {
1536
+ const tagNames = Array.isArray(r.tags?.connect)
1537
+ ? r.tags.connect.map((t) => fixtureTags.find((ft) => ft.id === t.id)?.name) || []
1538
+ : [];
1539
+ return tagNames.some((name) => ["public", "private"].includes(name || ""));
1540
+ })
1541
+ .map((r) => r.id));
1542
+ });
1543
+ });
1544
+ });
1545
+ // Relations
1546
+ (0, globals_1.describe)("Relations", () => {
1547
+ (0, globals_1.describe)("Simple Relations", () => {
1548
+ (0, globals_1.describe)("One-to-One", () => {
1549
+ (0, globals_1.test)("conditional - relation is", async () => {
1550
+ const queryPlan = await cerbos.planResources({
1551
+ principal: { id: "user1", roles: ["USER"] },
1552
+ resource: { kind: "resource" },
1553
+ action: "relation-is",
1554
+ });
1555
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1556
+ const conditions = queryPlan
1557
+ .condition;
1558
+ (0, globals_1.expect)(conditions).toEqual({
1559
+ operator: "eq",
1560
+ operands: [
1561
+ { name: "request.resource.attr.createdBy" },
1562
+ { value: "user1" },
1563
+ ],
1564
+ });
1565
+ const result = (0, _1.queryPlanToPrisma)({
1566
+ queryPlan,
1567
+ mapper: {
1568
+ "request.resource.attr.createdBy": {
1569
+ relation: {
1570
+ name: "createdBy",
1571
+ type: "one",
1572
+ field: "id",
1573
+ },
1574
+ },
1575
+ },
1576
+ });
1577
+ (0, globals_1.expect)(result).toStrictEqual({
1578
+ kind: _1.PlanKind.CONDITIONAL,
1579
+ filters: {
1580
+ createdBy: {
1581
+ is: {
1582
+ id: {
1583
+ equals: "user1",
1584
+ },
1585
+ },
1586
+ },
1587
+ },
1588
+ });
1589
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1590
+ throw new Error("Expected CONDITIONAL result");
1591
+ }
1592
+ const query = await prisma.resource.findMany({
1593
+ where: { ...result.filters },
1594
+ });
1595
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1596
+ .filter((r) => {
1597
+ if (!r.createdBy?.connect)
1598
+ return false;
1599
+ return r.createdBy.connect.id == "user1";
1600
+ })
1601
+ .map((r) => r.id));
1602
+ });
1603
+ (0, globals_1.test)("conditional - relation is not", async () => {
1604
+ const queryPlan = await cerbos.planResources({
1605
+ principal: { id: "user1", roles: ["USER"] },
1606
+ resource: { kind: "resource" },
1607
+ action: "relation-is-not",
1608
+ });
1609
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1610
+ const conditions = queryPlan
1611
+ .condition;
1612
+ (0, globals_1.expect)(conditions).toEqual({
1613
+ operator: "not",
1614
+ operands: [
1615
+ {
1616
+ operator: "eq",
1617
+ operands: [
1618
+ { name: "request.resource.attr.createdBy" },
1619
+ { value: "user1" },
1620
+ ],
1621
+ },
1622
+ ],
1623
+ });
1624
+ const result = (0, _1.queryPlanToPrisma)({
1625
+ queryPlan,
1626
+ mapper: {
1627
+ "request.resource.attr.createdBy": {
1628
+ relation: {
1629
+ name: "createdBy",
1630
+ type: "one",
1631
+ field: "id",
1632
+ },
1633
+ },
1634
+ },
1635
+ });
1636
+ (0, globals_1.expect)(result).toStrictEqual({
1637
+ kind: _1.PlanKind.CONDITIONAL,
1638
+ filters: {
1639
+ NOT: {
1640
+ createdBy: {
1641
+ is: {
1642
+ id: {
1643
+ equals: "user1",
1644
+ },
1645
+ },
1646
+ },
1647
+ },
1648
+ },
1649
+ });
1650
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1651
+ throw new Error("Expected CONDITIONAL result");
1652
+ }
1653
+ const query = await prisma.resource.findMany({
1654
+ where: { ...result.filters },
1655
+ });
1656
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1657
+ .filter((r) => {
1658
+ if (!r.createdBy?.connect)
1659
+ return false;
1660
+ return r.createdBy.connect.id != "user1";
1661
+ })
1662
+ .map((r) => r.id));
1663
+ });
1664
+ (0, globals_1.test)("conditional - relation is without field", async () => {
1665
+ const queryPlan = await cerbos.planResources({
1666
+ principal: { id: "user1", roles: ["USER"] },
1667
+ resource: { kind: "resource" },
1668
+ action: "relation-is",
1669
+ });
1670
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1671
+ const conditions = queryPlan
1672
+ .condition;
1673
+ (0, globals_1.expect)(conditions).toEqual({
1674
+ operator: "eq",
1675
+ operands: [
1676
+ { name: "request.resource.attr.createdBy" },
1677
+ { value: "user1" },
1678
+ ],
1679
+ });
1680
+ const result = (0, _1.queryPlanToPrisma)({
1681
+ queryPlan,
1682
+ mapper: {
1683
+ "request.resource.attr.createdBy": {
1684
+ relation: {
1685
+ name: "createdBy",
1686
+ type: "one",
1687
+ field: "id",
1688
+ },
1689
+ },
1690
+ },
1691
+ });
1692
+ (0, globals_1.expect)(result).toStrictEqual({
1693
+ kind: _1.PlanKind.CONDITIONAL,
1694
+ filters: {
1695
+ createdBy: {
1696
+ is: {
1697
+ id: { equals: "user1" },
1698
+ },
1699
+ },
1700
+ },
1701
+ });
1702
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1703
+ throw new Error("Expected CONDITIONAL result");
1704
+ }
1705
+ const query = await prisma.resource.findMany({
1706
+ where: { ...result.filters },
1707
+ });
1708
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1709
+ .filter((r) => {
1710
+ if (!r.createdBy?.connect)
1711
+ return false;
1712
+ return r.createdBy.connect.id == "user1";
1713
+ })
1714
+ .map((r) => r.id));
1715
+ });
1716
+ });
1717
+ (0, globals_1.describe)("One-to-Many/Many-to-Many", () => {
1718
+ (0, globals_1.test)("conditional - relation some", async () => {
1719
+ const queryPlan = await cerbos.planResources({
1720
+ principal: { id: "user1", roles: ["USER"] },
1721
+ resource: { kind: "resource" },
1722
+ action: "relation-some",
1723
+ });
1724
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1725
+ const conditions = queryPlan
1726
+ .condition;
1727
+ (0, globals_1.expect)(conditions).toEqual({
1728
+ operator: "in",
1729
+ operands: [
1730
+ { value: "user1" },
1731
+ { name: "request.resource.attr.ownedBy" },
1732
+ ],
1733
+ });
1734
+ const result = (0, _1.queryPlanToPrisma)({
1735
+ queryPlan,
1736
+ mapper: {
1737
+ "request.resource.attr.ownedBy": {
1738
+ relation: {
1739
+ name: "ownedBy",
1740
+ type: "many",
1741
+ field: "id",
1742
+ },
1743
+ },
1744
+ },
1745
+ });
1746
+ (0, globals_1.expect)(result).toStrictEqual({
1747
+ kind: _1.PlanKind.CONDITIONAL,
1748
+ filters: {
1749
+ ownedBy: {
1750
+ some: {
1751
+ id: "user1",
1752
+ },
1753
+ },
1754
+ },
1755
+ });
1756
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1757
+ throw new Error("Expected CONDITIONAL result");
1758
+ }
1759
+ const query = await prisma.resource.findMany({
1760
+ where: { ...result.filters },
1761
+ });
1762
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1763
+ .filter((r) => {
1764
+ if (!r.ownedBy?.connect)
1765
+ return false;
1766
+ return (r.ownedBy.connect.filter((o) => o.id == "user1").length > 0);
1767
+ })
1768
+ .map((r) => r.id));
1769
+ });
1770
+ (0, globals_1.test)("conditional - relation none", async () => {
1771
+ const queryPlan = await cerbos.planResources({
1772
+ principal: { id: "user1", roles: ["USER"] },
1773
+ resource: { kind: "resource" },
1774
+ action: "relation-none",
1775
+ });
1776
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1777
+ const conditions = queryPlan
1778
+ .condition;
1779
+ (0, globals_1.expect)(conditions).toEqual({
1780
+ operator: "not",
1781
+ operands: [
1782
+ {
1783
+ operator: "in",
1784
+ operands: [
1785
+ { value: "user1" },
1786
+ { name: "request.resource.attr.ownedBy" },
1787
+ ],
1788
+ },
1789
+ ],
1790
+ });
1791
+ const result = (0, _1.queryPlanToPrisma)({
1792
+ queryPlan,
1793
+ mapper: {
1794
+ "request.resource.attr.ownedBy": {
1795
+ relation: {
1796
+ name: "ownedBy",
1797
+ type: "many",
1798
+ field: "id",
1799
+ },
1800
+ },
1801
+ },
1802
+ });
1803
+ (0, globals_1.expect)(result).toStrictEqual({
1804
+ kind: _1.PlanKind.CONDITIONAL,
1805
+ filters: {
1806
+ NOT: {
1807
+ ownedBy: {
1808
+ some: {
1809
+ id: "user1",
1810
+ },
1811
+ },
1812
+ },
1813
+ },
1814
+ });
1815
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1816
+ throw new Error("Expected CONDITIONAL result");
1817
+ }
1818
+ const query = await prisma.resource.findMany({
1819
+ where: { ...result.filters },
1820
+ });
1821
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
1822
+ .filter((r) => {
1823
+ if (!r.ownedBy?.connect)
1824
+ return false;
1825
+ return (r.ownedBy.connect.filter((o) => o.id == "user1").length == 0);
1826
+ })
1827
+ .map((r) => r.id));
1828
+ });
1829
+ (0, globals_1.test)("conditional - has-tag", async () => {
1830
+ const queryPlan = await cerbos.planResources({
1831
+ principal: { id: "user1", roles: ["USER"] },
1832
+ resource: { kind: "resource" },
1833
+ action: "has-tag",
1834
+ });
1835
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1836
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1837
+ operator: "in",
1838
+ operands: [
1839
+ { value: "public" },
1840
+ { name: "request.resource.attr.tags" },
1841
+ ],
1842
+ });
1843
+ const result = (0, _1.queryPlanToPrisma)({
1844
+ queryPlan,
1845
+ mapper: {
1846
+ "request.resource.attr.tags": {
1847
+ relation: {
1848
+ name: "tags",
1849
+ type: "many", // This indicates a many-to-many relationship
1850
+ field: "name",
1851
+ },
1852
+ },
1853
+ },
1854
+ });
1855
+ (0, globals_1.expect)(result).toStrictEqual({
1856
+ kind: _1.PlanKind.CONDITIONAL,
1857
+ filters: {
1858
+ tags: {
1859
+ some: {
1860
+ name: "public",
1861
+ },
1862
+ },
1863
+ },
1864
+ });
1865
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1866
+ throw new Error("Expected CONDITIONAL result");
1867
+ }
1868
+ const query = await prisma.resource.findMany({
1869
+ where: { ...result.filters },
1870
+ include: { tags: true },
1871
+ });
1872
+ const expectedIds = fixtureResources
1873
+ .filter((resource) => {
1874
+ const tagRefs = resource.tags?.connect ?? [];
1875
+ return tagRefs.some((tagRef) => {
1876
+ const tag = fixtureTags.find((ft) => ft.id === tagRef.id);
1877
+ return tag?.name === "public";
1878
+ });
1879
+ })
1880
+ .map((r) => r.id)
1881
+ .sort();
1882
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(expectedIds);
1883
+ });
1884
+ (0, globals_1.test)("conditional - has-no-tag", async () => {
1885
+ const queryPlan = await cerbos.planResources({
1886
+ principal: { id: "user1", roles: ["USER"] },
1887
+ resource: { kind: "resource" },
1888
+ action: "has-no-tag",
1889
+ });
1890
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1891
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
1892
+ operator: "not",
1893
+ operands: [
1894
+ {
1895
+ operator: "in",
1896
+ operands: [
1897
+ { value: "private" },
1898
+ { name: "request.resource.attr.tags" },
1899
+ ],
1900
+ },
1901
+ ],
1902
+ });
1903
+ const result = (0, _1.queryPlanToPrisma)({
1904
+ queryPlan,
1905
+ mapper: {
1906
+ "request.resource.attr.tags": {
1907
+ relation: {
1908
+ name: "tags",
1909
+ type: "many",
1910
+ field: "name",
1911
+ },
1912
+ },
1913
+ },
1914
+ });
1915
+ (0, globals_1.expect)(result).toStrictEqual({
1916
+ kind: _1.PlanKind.CONDITIONAL,
1917
+ filters: {
1918
+ NOT: {
1919
+ tags: {
1920
+ some: {
1921
+ name: "private",
1922
+ },
1923
+ },
1924
+ },
1925
+ },
1926
+ });
1927
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1928
+ throw new Error("Expected CONDITIONAL result");
1929
+ }
1930
+ const query = await prisma.resource.findMany({
1931
+ where: { ...result.filters },
1932
+ include: { tags: true },
1933
+ });
1934
+ const expectedIds = fixtureResources
1935
+ .filter((resource) => {
1936
+ const tagRefs = resource.tags?.connect ?? [];
1937
+ return tagRefs.every((tagRef) => {
1938
+ const tag = fixtureTags.find((ft) => ft.id === tagRef.id);
1939
+ return tag?.name !== "private";
1940
+ });
1941
+ })
1942
+ .map((r) => r.id)
1943
+ .sort();
1944
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(expectedIds);
1945
+ });
1946
+ (0, globals_1.test)("conditional - has-intersection", async () => {
1947
+ const queryPlan = await cerbos.planResources({
1948
+ principal: {
1949
+ id: "user1",
1950
+ roles: ["USER"],
1951
+ attr: { tags: ["public", "draft"] },
1952
+ },
1953
+ resource: { kind: "resource" },
1954
+ action: "has-intersection",
1955
+ });
1956
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
1957
+ // expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual({
1958
+ // operator: "hasIntersection",
1959
+ // operands: [
1960
+ // { name: "request.resource.attr.tags" },
1961
+ // { value: ["public", "draft"] },
1962
+ // ],
1963
+ // });
1964
+ const result = (0, _1.queryPlanToPrisma)({
1965
+ queryPlan,
1966
+ mapper: {
1967
+ "request.resource.attr.tags": {
1968
+ relation: {
1969
+ name: "tags",
1970
+ type: "many",
1971
+ },
1972
+ },
1973
+ },
1974
+ });
1975
+ (0, globals_1.expect)(result).toStrictEqual({
1976
+ kind: _1.PlanKind.CONDITIONAL,
1977
+ filters: {
1978
+ tags: {
1979
+ some: {
1980
+ name: { in: ["public", "draft"] },
1981
+ },
1982
+ },
1983
+ },
1984
+ });
1985
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
1986
+ throw new Error("Expected CONDITIONAL result");
1987
+ }
1988
+ const query = await prisma.resource.findMany({
1989
+ where: { ...result.filters },
1990
+ });
1991
+ const expectedIds = fixtureResources
1992
+ .filter((resource) => {
1993
+ const tagRefs = resource.tags?.connect ?? [];
1994
+ return tagRefs.some((tagRef) => {
1995
+ const tag = fixtureTags.find((ft) => ft.id === tagRef.id);
1996
+ return ["public", "draft"].includes(tag?.name ?? "");
1997
+ });
1998
+ })
1999
+ .map((r) => r.id)
2000
+ .sort();
2001
+ (0, globals_1.expect)(query.map((r) => r.id).sort()).toEqual(expectedIds);
2002
+ });
2003
+ (0, globals_1.test)("conditional - hasIntersection with direct value", async () => {
2004
+ const queryPlan = await cerbos.planResources({
2005
+ principal: { id: "user1", roles: ["USER"] },
2006
+ resource: { kind: "resource" },
2007
+ action: "has-intersection-direct",
2008
+ });
2009
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2010
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
2011
+ operator: "hasIntersection",
2012
+ operands: [
2013
+ { name: "request.resource.attr.tags" },
2014
+ { value: ["public", "draft"] },
2015
+ ],
2016
+ });
2017
+ const result = (0, _1.queryPlanToPrisma)({
2018
+ queryPlan,
2019
+ mapper: {
2020
+ "request.resource.attr.tags": {
2021
+ relation: {
2022
+ name: "tags",
2023
+ type: "many",
2024
+ field: "name",
2025
+ },
2026
+ },
2027
+ },
2028
+ });
2029
+ (0, globals_1.expect)(result).toStrictEqual({
2030
+ kind: _1.PlanKind.CONDITIONAL,
2031
+ filters: {
2032
+ tags: {
2033
+ some: {
2034
+ name: { in: ["public", "draft"] },
2035
+ },
2036
+ },
2037
+ },
2038
+ });
2039
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2040
+ throw new Error("Expected CONDITIONAL result");
2041
+ }
2042
+ const query = await prisma.resource.findMany({
2043
+ where: { ...result.filters },
2044
+ });
2045
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2046
+ .filter((r) => Array.isArray(r.tags?.connect) &&
2047
+ r.tags?.connect
2048
+ .map((t) => fixtureTags.find((ft) => ft.id === t.id)?.name)
2049
+ .some((name) => ["public", "draft"].includes(name ?? "")))
2050
+ .map((r) => r.id));
2051
+ });
2052
+ });
2053
+ });
2054
+ (0, globals_1.describe)("Nested Relations", () => {
2055
+ (0, globals_1.describe)("Single Level", () => {
2056
+ (0, globals_1.test)("conditional - nested eq", async () => {
2057
+ const queryPlan = await cerbos.planResources({
2058
+ principal: { id: "user1", roles: ["USER"] },
2059
+ resource: { kind: "resource" },
2060
+ action: "equal-nested",
2061
+ });
2062
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2063
+ const conditions = queryPlan
2064
+ .condition;
2065
+ (0, globals_1.expect)(conditions).toEqual({
2066
+ operator: "eq",
2067
+ operands: [
2068
+ { name: "request.resource.attr.nested.aBool" },
2069
+ { value: true },
2070
+ ],
2071
+ });
2072
+ const result = (0, _1.queryPlanToPrisma)({
2073
+ queryPlan,
2074
+ mapper: {
2075
+ "request.resource.attr.nested": {
2076
+ relation: {
2077
+ name: "nested",
2078
+ type: "one",
2079
+ fields: {
2080
+ aBool: { field: "aBool" },
2081
+ },
2082
+ },
2083
+ },
2084
+ },
2085
+ });
2086
+ (0, globals_1.expect)(result).toStrictEqual({
2087
+ kind: _1.PlanKind.CONDITIONAL,
2088
+ filters: { nested: { is: { aBool: { equals: true } } } },
2089
+ });
2090
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2091
+ throw new Error("Expected CONDITIONAL result");
2092
+ }
2093
+ const query = await prisma.resource.findMany({
2094
+ where: { ...result.filters },
2095
+ });
2096
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2097
+ .filter((a) => fixtureNestedResources.find((f) => f.id === a.nested.connect?.id)?.aBool)
2098
+ .map((r) => r.id));
2099
+ });
2100
+ (0, globals_1.test)("conditional - relation eq with number", async () => {
2101
+ const queryPlan = await cerbos.planResources({
2102
+ principal: { id: "user1", roles: ["USER"] },
2103
+ resource: { kind: "resource" },
2104
+ action: "relation-eq-number",
2105
+ });
2106
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2107
+ const conditions = queryPlan
2108
+ .condition;
2109
+ (0, globals_1.expect)(conditions).toEqual({
2110
+ operator: "eq",
2111
+ operands: [
2112
+ { name: "request.resource.attr.nested.aNumber" },
2113
+ { value: 1 },
2114
+ ],
2115
+ });
2116
+ const result = (0, _1.queryPlanToPrisma)({
2117
+ queryPlan,
2118
+ mapper: {
2119
+ "request.resource.attr.nested": {
2120
+ relation: {
2121
+ name: "nested",
2122
+ type: "one",
2123
+ },
2124
+ },
2125
+ },
2126
+ });
2127
+ (0, globals_1.expect)(result).toStrictEqual({
2128
+ kind: _1.PlanKind.CONDITIONAL,
2129
+ filters: {
2130
+ nested: {
2131
+ is: {
2132
+ aNumber: { equals: 1 },
2133
+ },
2134
+ },
2135
+ },
2136
+ });
2137
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2138
+ throw new Error("Expected CONDITIONAL result");
2139
+ }
2140
+ const query = await prisma.resource.findMany({
2141
+ where: { ...result.filters },
2142
+ });
2143
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2144
+ .filter((r) => {
2145
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2146
+ return (nestedResource?.aNumber ?? 0) == 1;
2147
+ })
2148
+ .map((r) => r.id));
2149
+ });
2150
+ (0, globals_1.test)("conditional - relation eq with number without field", async () => {
2151
+ const queryPlan = await cerbos.planResources({
2152
+ principal: { id: "user1", roles: ["USER"] },
2153
+ resource: { kind: "resource" },
2154
+ action: "relation-eq-number",
2155
+ });
2156
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2157
+ const conditions = queryPlan
2158
+ .condition;
2159
+ (0, globals_1.expect)(conditions).toEqual({
2160
+ operator: "eq",
2161
+ operands: [
2162
+ { name: "request.resource.attr.nested.aNumber" },
2163
+ { value: 1 },
2164
+ ],
2165
+ });
2166
+ const result = (0, _1.queryPlanToPrisma)({
2167
+ queryPlan,
2168
+ mapper: {
2169
+ "request.resource.attr.nested": {
2170
+ relation: {
2171
+ name: "nested",
2172
+ type: "one",
2173
+ },
2174
+ },
2175
+ },
2176
+ });
2177
+ (0, globals_1.expect)(result).toStrictEqual({
2178
+ kind: _1.PlanKind.CONDITIONAL,
2179
+ filters: {
2180
+ nested: {
2181
+ is: {
2182
+ aNumber: {
2183
+ equals: 1,
2184
+ },
2185
+ },
2186
+ },
2187
+ },
2188
+ });
2189
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2190
+ throw new Error("Expected CONDITIONAL result");
2191
+ }
2192
+ const query = await prisma.resource.findMany({
2193
+ where: { ...result.filters },
2194
+ });
2195
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2196
+ .filter((r) => {
2197
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2198
+ return nestedResource && nestedResource.aNumber === 1;
2199
+ })
2200
+ .map((r) => r.id));
2201
+ });
2202
+ (0, globals_1.test)("conditional - relation lt with number", async () => {
2203
+ const queryPlan = await cerbos.planResources({
2204
+ principal: { id: "user1", roles: ["USER"] },
2205
+ resource: { kind: "resource" },
2206
+ action: "relation-lt-number",
2207
+ });
2208
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2209
+ const conditions = queryPlan
2210
+ .condition;
2211
+ (0, globals_1.expect)(conditions).toEqual({
2212
+ operator: "lt",
2213
+ operands: [
2214
+ { name: "request.resource.attr.nested.aNumber" },
2215
+ { value: 2 },
2216
+ ],
2217
+ });
2218
+ const result = (0, _1.queryPlanToPrisma)({
2219
+ queryPlan,
2220
+ mapper: {
2221
+ "request.resource.attr.nested": {
2222
+ relation: {
2223
+ name: "nested",
2224
+ type: "one",
2225
+ },
2226
+ },
2227
+ },
2228
+ });
2229
+ (0, globals_1.expect)(result).toStrictEqual({
2230
+ kind: _1.PlanKind.CONDITIONAL,
2231
+ filters: {
2232
+ nested: {
2233
+ is: {
2234
+ aNumber: { lt: 2 },
2235
+ },
2236
+ },
2237
+ },
2238
+ });
2239
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2240
+ throw new Error("Expected CONDITIONAL result");
2241
+ }
2242
+ const query = await prisma.resource.findMany({
2243
+ where: { ...result.filters },
2244
+ });
2245
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2246
+ .filter((r) => {
2247
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2248
+ return (nestedResource?.aNumber ?? 0) < 2;
2249
+ })
2250
+ .map((r) => r.id));
2251
+ });
2252
+ (0, globals_1.test)("conditional - relation lte with number", async () => {
2253
+ const queryPlan = await cerbos.planResources({
2254
+ principal: { id: "user1", roles: ["USER"] },
2255
+ resource: { kind: "resource" },
2256
+ action: "relation-lte-number",
2257
+ });
2258
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2259
+ const conditions = queryPlan
2260
+ .condition;
2261
+ (0, globals_1.expect)(conditions).toEqual({
2262
+ operator: "le",
2263
+ operands: [
2264
+ { name: "request.resource.attr.nested.aNumber" },
2265
+ { value: 2 },
2266
+ ],
2267
+ });
2268
+ const result = (0, _1.queryPlanToPrisma)({
2269
+ queryPlan,
2270
+ mapper: {
2271
+ "request.resource.attr.nested": {
2272
+ relation: {
2273
+ name: "nested",
2274
+ type: "one",
2275
+ },
2276
+ },
2277
+ },
2278
+ });
2279
+ (0, globals_1.expect)(result).toStrictEqual({
2280
+ kind: _1.PlanKind.CONDITIONAL,
2281
+ filters: {
2282
+ nested: {
2283
+ is: {
2284
+ aNumber: { lte: 2 },
2285
+ },
2286
+ },
2287
+ },
2288
+ });
2289
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2290
+ throw new Error("Expected CONDITIONAL result");
2291
+ }
2292
+ const query = await prisma.resource.findMany({
2293
+ where: { ...result.filters },
2294
+ });
2295
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2296
+ .filter((r) => {
2297
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2298
+ return (nestedResource?.aNumber ?? 0) <= 2;
2299
+ })
2300
+ .map((r) => r.id));
2301
+ });
2302
+ (0, globals_1.test)("conditional - relation gte with number", async () => {
2303
+ const queryPlan = await cerbos.planResources({
2304
+ principal: { id: "user1", roles: ["USER"] },
2305
+ resource: { kind: "resource" },
2306
+ action: "relation-gte-number",
2307
+ });
2308
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2309
+ const conditions = queryPlan
2310
+ .condition;
2311
+ (0, globals_1.expect)(conditions).toEqual({
2312
+ operator: "ge",
2313
+ operands: [
2314
+ { name: "request.resource.attr.nested.aNumber" },
2315
+ { value: 1 },
2316
+ ],
2317
+ });
2318
+ const result = (0, _1.queryPlanToPrisma)({
2319
+ queryPlan,
2320
+ mapper: {
2321
+ "request.resource.attr.nested": {
2322
+ relation: {
2323
+ name: "nested",
2324
+ type: "one",
2325
+ },
2326
+ },
2327
+ },
2328
+ });
2329
+ (0, globals_1.expect)(result).toStrictEqual({
2330
+ kind: _1.PlanKind.CONDITIONAL,
2331
+ filters: {
2332
+ nested: {
2333
+ is: {
2334
+ aNumber: { gte: 1 },
2335
+ },
2336
+ },
2337
+ },
2338
+ });
2339
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2340
+ throw new Error("Expected CONDITIONAL result");
2341
+ }
2342
+ const query = await prisma.resource.findMany({
2343
+ where: { ...result.filters },
2344
+ });
2345
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2346
+ .filter((r) => {
2347
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2348
+ return (nestedResource?.aNumber ?? 0) >= 1;
2349
+ })
2350
+ .map((r) => r.id));
2351
+ });
2352
+ (0, globals_1.test)("conditional - relation-gt-number", async () => {
2353
+ const queryPlan = await cerbos.planResources({
2354
+ principal: { id: "user1", roles: ["USER"] },
2355
+ resource: { kind: "resource" },
2356
+ action: "relation-gt-number",
2357
+ });
2358
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2359
+ const conditions = queryPlan
2360
+ .condition;
2361
+ (0, globals_1.expect)(conditions).toEqual({
2362
+ operator: "gt",
2363
+ operands: [
2364
+ { name: "request.resource.attr.nested.aNumber" },
2365
+ { value: 1 },
2366
+ ],
2367
+ });
2368
+ const result = (0, _1.queryPlanToPrisma)({
2369
+ queryPlan,
2370
+ mapper: {
2371
+ "request.resource.attr.nested": {
2372
+ relation: {
2373
+ name: "nested",
2374
+ type: "one",
2375
+ },
2376
+ },
2377
+ },
2378
+ });
2379
+ (0, globals_1.expect)(result).toStrictEqual({
2380
+ kind: _1.PlanKind.CONDITIONAL,
2381
+ filters: {
2382
+ nested: {
2383
+ is: {
2384
+ aNumber: { gt: 1 },
2385
+ },
2386
+ },
2387
+ },
2388
+ });
2389
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2390
+ throw new Error("Expected CONDITIONAL result");
2391
+ }
2392
+ const query = await prisma.resource.findMany({
2393
+ where: { ...result.filters },
2394
+ });
2395
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2396
+ .filter((r) => {
2397
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2398
+ return (nestedResource?.aNumber ?? 0) > 1;
2399
+ })
2400
+ .map((r) => r.id));
2401
+ });
2402
+ });
2403
+ (0, globals_1.describe)("Deep Nesting", () => {
2404
+ (0, globals_1.test)("conditional - deeply nested eq", async () => {
2405
+ const queryPlan = await cerbos.planResources({
2406
+ principal: { id: "user1", roles: ["USER"] },
2407
+ resource: { kind: "resource" },
2408
+ action: "equal-deeply-nested",
2409
+ });
2410
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2411
+ const conditions = queryPlan
2412
+ .condition;
2413
+ (0, globals_1.expect)(conditions).toEqual({
2414
+ operator: "eq",
2415
+ operands: [
2416
+ { name: "request.resource.attr.nested.nextlevel.aBool" },
2417
+ { value: true },
2418
+ ],
2419
+ });
2420
+ const result = (0, _1.queryPlanToPrisma)({
2421
+ queryPlan,
2422
+ mapper: {
2423
+ "request.resource.attr.nested": {
2424
+ relation: {
2425
+ name: "nested",
2426
+ type: "one",
2427
+ fields: {
2428
+ nextlevel: {
2429
+ relation: {
2430
+ name: "nextlevel",
2431
+ type: "one",
2432
+ fields: {
2433
+ aBool: { field: "aBool" },
2434
+ },
2435
+ },
2436
+ },
2437
+ },
2438
+ },
2439
+ },
2440
+ },
2441
+ });
2442
+ (0, globals_1.expect)(result).toStrictEqual({
2443
+ kind: _1.PlanKind.CONDITIONAL,
2444
+ filters: {
2445
+ nested: {
2446
+ is: {
2447
+ nextlevel: {
2448
+ is: {
2449
+ aBool: {
2450
+ equals: true,
2451
+ },
2452
+ },
2453
+ },
2454
+ },
2455
+ },
2456
+ },
2457
+ });
2458
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2459
+ throw new Error("Expected CONDITIONAL result");
2460
+ }
2461
+ const query = await prisma.resource.findMany({
2462
+ where: { ...result.filters },
2463
+ });
2464
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2465
+ .filter((a) => {
2466
+ const nestedResource = fixtureNestedResources.find((f) => f.id === a.nested.connect?.id);
2467
+ const nextLevelResource = fixtureNextLevelResources.find((f) => f.id === nestedResource?.nextlevel.connect?.id);
2468
+ return nextLevelResource?.aBool === true;
2469
+ })
2470
+ .map((r) => r.id));
2471
+ });
2472
+ (0, globals_1.test)("conditional - deeply nested many to many category label", async () => {
2473
+ const queryPlan = await cerbos.planResources({
2474
+ principal: { id: "user1", roles: ["USER"] },
2475
+ resource: { kind: "resource" },
2476
+ action: "deep-nested-category-label",
2477
+ });
2478
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2479
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
2480
+ operator: "exists",
2481
+ operands: [
2482
+ {
2483
+ name: "request.resource.attr.categories",
2484
+ },
2485
+ {
2486
+ operator: "lambda",
2487
+ operands: [
2488
+ {
2489
+ operator: "exists",
2490
+ operands: [
2491
+ {
2492
+ name: "cat.subCategories",
2493
+ },
2494
+ {
2495
+ operator: "lambda",
2496
+ operands: [
2497
+ {
2498
+ operator: "exists",
2499
+ operands: [
2500
+ {
2501
+ name: "sub.labels",
2502
+ },
2503
+ {
2504
+ operator: "lambda",
2505
+ operands: [
2506
+ {
2507
+ operator: "eq",
2508
+ operands: [
2509
+ {
2510
+ name: "label.name",
2511
+ },
2512
+ {
2513
+ value: "important",
2514
+ },
2515
+ ],
2516
+ },
2517
+ {
2518
+ name: "label",
2519
+ },
2520
+ ],
2521
+ },
2522
+ ],
2523
+ },
2524
+ {
2525
+ name: "sub",
2526
+ },
2527
+ ],
2528
+ },
2529
+ ],
2530
+ },
2531
+ {
2532
+ name: "cat",
2533
+ },
2534
+ ],
2535
+ },
2536
+ ],
2537
+ });
2538
+ const result = (0, _1.queryPlanToPrisma)({
2539
+ queryPlan,
2540
+ mapper: {
2541
+ "request.resource.attr.categories": {
2542
+ relation: {
2543
+ name: "categories",
2544
+ type: "many",
2545
+ fields: {
2546
+ subCategories: {
2547
+ relation: {
2548
+ name: "subCategories",
2549
+ type: "many",
2550
+ fields: {
2551
+ labels: {
2552
+ relation: {
2553
+ name: "labels",
2554
+ type: "many",
2555
+ fields: {
2556
+ name: { field: "name" },
2557
+ },
2558
+ },
2559
+ },
2560
+ },
2561
+ },
2562
+ },
2563
+ },
2564
+ },
2565
+ },
2566
+ },
2567
+ });
2568
+ (0, globals_1.expect)(result).toStrictEqual({
2569
+ kind: _1.PlanKind.CONDITIONAL,
2570
+ filters: {
2571
+ categories: {
2572
+ some: {
2573
+ subCategories: {
2574
+ some: {
2575
+ labels: {
2576
+ some: {
2577
+ name: { equals: "important" },
2578
+ },
2579
+ },
2580
+ },
2581
+ },
2582
+ },
2583
+ },
2584
+ },
2585
+ });
2586
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2587
+ throw new Error("Expected CONDITIONAL result");
2588
+ }
2589
+ const query = await prisma.resource.findMany({
2590
+ where: { ...result.filters },
2591
+ include: {
2592
+ categories: {
2593
+ include: {
2594
+ subCategories: {
2595
+ include: {
2596
+ labels: true,
2597
+ },
2598
+ },
2599
+ },
2600
+ },
2601
+ },
2602
+ });
2603
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2604
+ .filter((r) => {
2605
+ return (r.categories?.connect)
2606
+ .map((c) => {
2607
+ return fixtureCategories.find((fc) => fc.id === c.id);
2608
+ })
2609
+ .some((c) => {
2610
+ return (c?.subCategories
2611
+ ?.connect)
2612
+ .map((sc) => {
2613
+ return fixtureSubCategories.find((fsc) => fsc.id === sc.id);
2614
+ })
2615
+ .some((sc) => {
2616
+ return (sc?.labels?.connect)
2617
+ .map((l) => {
2618
+ return fixtureLabels.find((fl) => fl.id === l.id);
2619
+ })
2620
+ .some((l) => l?.name === "important");
2621
+ });
2622
+ });
2623
+ })
2624
+ .map((r) => r.id));
2625
+ });
2626
+ (0, globals_1.test)("conditional - deep nested exists with multiple conditions", async () => {
2627
+ const queryPlan = await cerbos.planResources({
2628
+ principal: { id: "user1", roles: ["USER"] },
2629
+ resource: { kind: "resource" },
2630
+ action: "deep-nested-exists",
2631
+ });
2632
+ const result = (0, _1.queryPlanToPrisma)({
2633
+ queryPlan,
2634
+ mapper: {
2635
+ "request.resource.attr.categories": {
2636
+ relation: {
2637
+ name: "categories",
2638
+ type: "many",
2639
+ fields: {
2640
+ subCategories: {
2641
+ relation: {
2642
+ name: "subCategories",
2643
+ type: "many",
2644
+ field: "name",
2645
+ },
2646
+ },
2647
+ },
2648
+ },
2649
+ },
2650
+ },
2651
+ });
2652
+ (0, globals_1.expect)(result).toStrictEqual({
2653
+ kind: _1.PlanKind.CONDITIONAL,
2654
+ filters: {
2655
+ categories: {
2656
+ some: {
2657
+ AND: [
2658
+ { name: { equals: "business" } },
2659
+ {
2660
+ subCategories: {
2661
+ some: {
2662
+ name: { equals: "finance" },
2663
+ },
2664
+ },
2665
+ },
2666
+ ],
2667
+ },
2668
+ },
2669
+ },
2670
+ });
2671
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2672
+ throw new Error("Expected CONDITIONAL result");
2673
+ }
2674
+ const query = await prisma.resource.findMany({
2675
+ where: { ...result.filters },
2676
+ });
2677
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2678
+ .filter((r) => {
2679
+ const categoryConnections = r.categories
2680
+ ?.connect;
2681
+ return (categoryConnections?.some((categoryConnection) => {
2682
+ const category = fixtureCategories.find((fc) => fc.id === categoryConnection.id);
2683
+ return (category?.name === "business" &&
2684
+ (category?.subCategories
2685
+ ?.connect).some((sc) => fixtureSubCategories.find((fsc) => fsc.id === sc.id)
2686
+ ?.name === "finance"));
2687
+ }) ?? false);
2688
+ })
2689
+ .map((r) => r.id));
2690
+ });
2691
+ });
2692
+ (0, globals_1.describe)("Nested String Operations", () => {
2693
+ (0, globals_1.test)("conditional - nested contains", async () => {
2694
+ const queryPlan = await cerbos.planResources({
2695
+ principal: { id: "user1", roles: ["USER"] },
2696
+ resource: { kind: "resource" },
2697
+ action: "nested-contains",
2698
+ });
2699
+ const result = (0, _1.queryPlanToPrisma)({
2700
+ queryPlan,
2701
+ mapper: {
2702
+ "request.resource.attr.nested": {
2703
+ relation: {
2704
+ name: "nested",
2705
+ type: "one",
2706
+ fields: {
2707
+ aString: { field: "aString" },
2708
+ },
2709
+ },
2710
+ },
2711
+ },
2712
+ });
2713
+ (0, globals_1.expect)(result).toStrictEqual({
2714
+ kind: _1.PlanKind.CONDITIONAL,
2715
+ filters: {
2716
+ nested: {
2717
+ is: {
2718
+ aString: { contains: "str" },
2719
+ },
2720
+ },
2721
+ },
2722
+ });
2723
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2724
+ throw new Error("Expected CONDITIONAL result");
2725
+ }
2726
+ const query = await prisma.resource.findMany({
2727
+ where: { ...result.filters },
2728
+ });
2729
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2730
+ .filter((r) => {
2731
+ const nested = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2732
+ return nested?.aString.includes("str");
2733
+ })
2734
+ .map((r) => r.id));
2735
+ });
2736
+ (0, globals_1.test)("conditional - deeply nested startsWith", async () => {
2737
+ const queryPlan = await cerbos.planResources({
2738
+ principal: { id: "user1", roles: ["USER"] },
2739
+ resource: { kind: "resource" },
2740
+ action: "deeply-nested-starts-with",
2741
+ });
2742
+ const result = (0, _1.queryPlanToPrisma)({
2743
+ queryPlan,
2744
+ mapper: {
2745
+ "request.resource.attr.nested": {
2746
+ relation: {
2747
+ name: "nested",
2748
+ type: "one",
2749
+ fields: {
2750
+ nextlevel: {
2751
+ relation: {
2752
+ name: "nextlevel",
2753
+ type: "one",
2754
+ fields: {
2755
+ aString: { field: "aString" },
2756
+ },
2757
+ },
2758
+ },
2759
+ },
2760
+ },
2761
+ },
2762
+ },
2763
+ });
2764
+ (0, globals_1.expect)(result).toStrictEqual({
2765
+ kind: _1.PlanKind.CONDITIONAL,
2766
+ filters: {
2767
+ nested: {
2768
+ is: {
2769
+ nextlevel: {
2770
+ is: {
2771
+ aString: { startsWith: "str" },
2772
+ },
2773
+ },
2774
+ },
2775
+ },
2776
+ },
2777
+ });
2778
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2779
+ throw new Error("Expected CONDITIONAL result");
2780
+ }
2781
+ const query = await prisma.resource.findMany({
2782
+ where: { ...result.filters },
2783
+ });
2784
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2785
+ .filter((r) => {
2786
+ const nested = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2787
+ const nextLevel = fixtureNextLevelResources.find((nl) => nl.id === nested?.nextlevel.connect?.id);
2788
+ return nextLevel?.aString.startsWith("str");
2789
+ })
2790
+ .map((r) => r.id));
2791
+ });
2792
+ });
2793
+ });
2794
+ });
2795
+ // Complex Operations
2796
+ (0, globals_1.describe)("Complex Operations", () => {
2797
+ (0, globals_1.describe)("Logical Operations", () => {
2798
+ (0, globals_1.test)("conditional - and", async () => {
2799
+ const queryPlan = await cerbos.planResources({
2800
+ principal: { id: "user1", roles: ["USER"] },
2801
+ resource: { kind: "resource" },
2802
+ action: "and",
2803
+ });
2804
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2805
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
2806
+ operator: "and",
2807
+ operands: [
2808
+ {
2809
+ operator: "eq",
2810
+ operands: [
2811
+ { name: "request.resource.attr.aBool" },
2812
+ { value: true },
2813
+ ],
2814
+ },
2815
+ {
2816
+ operator: "ne",
2817
+ operands: [
2818
+ { name: "request.resource.attr.aString" },
2819
+ { value: "string" },
2820
+ ],
2821
+ },
2822
+ ],
2823
+ });
2824
+ const result = (0, _1.queryPlanToPrisma)({
2825
+ queryPlan,
2826
+ mapper: {
2827
+ "request.resource.attr.aBool": { field: "aBool" },
2828
+ "request.resource.attr.aString": { field: "aString" },
2829
+ },
2830
+ });
2831
+ (0, globals_1.expect)(result).toStrictEqual({
2832
+ kind: _1.PlanKind.CONDITIONAL,
2833
+ filters: {
2834
+ AND: [
2835
+ {
2836
+ aBool: { equals: true },
2837
+ },
2838
+ {
2839
+ aString: { not: "string" },
2840
+ },
2841
+ ],
2842
+ },
2843
+ });
2844
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2845
+ throw new Error("Expected CONDITIONAL result");
2846
+ }
2847
+ const query = await prisma.resource.findMany({
2848
+ where: { ...result.filters },
2849
+ });
2850
+ (0, globals_1.expect)(query).toEqual(fixtureResources
2851
+ .filter((r) => {
2852
+ return r.aBool && r.aString != "string";
2853
+ })
2854
+ .map((f) => ({ ...f, owners: undefined })));
2855
+ });
2856
+ (0, globals_1.test)("conditional - or", async () => {
2857
+ const queryPlan = await cerbos.planResources({
2858
+ principal: { id: "user1", roles: ["USER"] },
2859
+ resource: { kind: "resource" },
2860
+ action: "or",
2861
+ });
2862
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2863
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
2864
+ operator: "or",
2865
+ operands: [
2866
+ {
2867
+ operator: "eq",
2868
+ operands: [
2869
+ { name: "request.resource.attr.aBool" },
2870
+ { value: true },
2871
+ ],
2872
+ },
2873
+ {
2874
+ operator: "ne",
2875
+ operands: [
2876
+ { name: "request.resource.attr.aString" },
2877
+ { value: "string" },
2878
+ ],
2879
+ },
2880
+ ],
2881
+ });
2882
+ const result = (0, _1.queryPlanToPrisma)({
2883
+ queryPlan,
2884
+ mapper: {
2885
+ "request.resource.attr.aBool": { field: "aBool" },
2886
+ "request.resource.attr.aString": { field: "aString" },
2887
+ },
2888
+ });
2889
+ (0, globals_1.expect)(result).toStrictEqual({
2890
+ kind: _1.PlanKind.CONDITIONAL,
2891
+ filters: {
2892
+ OR: [
2893
+ {
2894
+ aBool: { equals: true },
2895
+ },
2896
+ {
2897
+ aString: { not: "string" },
2898
+ },
2899
+ ],
2900
+ },
2901
+ });
2902
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2903
+ throw new Error("Expected CONDITIONAL result");
2904
+ }
2905
+ const query = await prisma.resource.findMany({
2906
+ where: { ...result.filters },
2907
+ });
2908
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2909
+ .filter((r) => {
2910
+ return r.aBool || r.aString != "string";
2911
+ })
2912
+ .map((r) => r.id));
2913
+ });
2914
+ (0, globals_1.test)("conditional - relation multiple all", async () => {
2915
+ const queryPlan = await cerbos.planResources({
2916
+ principal: { id: "user1", roles: ["USER"] },
2917
+ resource: { kind: "resource" },
2918
+ action: "relation-multiple-all",
2919
+ });
2920
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2921
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
2922
+ operator: "and",
2923
+ operands: [
2924
+ {
2925
+ operator: "gt",
2926
+ operands: [
2927
+ { name: "request.resource.attr.nested.aNumber" },
2928
+ { value: 1 },
2929
+ ],
2930
+ },
2931
+ {
2932
+ operator: "lt",
2933
+ operands: [
2934
+ { name: "request.resource.attr.nested.aNumber" },
2935
+ { value: 3 },
2936
+ ],
2937
+ },
2938
+ ],
2939
+ });
2940
+ const result = (0, _1.queryPlanToPrisma)({
2941
+ queryPlan,
2942
+ mapper: {
2943
+ "request.resource.attr.nested": {
2944
+ relation: {
2945
+ name: "nested",
2946
+ type: "one",
2947
+ },
2948
+ },
2949
+ },
2950
+ });
2951
+ (0, globals_1.expect)(result).toStrictEqual({
2952
+ kind: _1.PlanKind.CONDITIONAL,
2953
+ filters: {
2954
+ AND: [
2955
+ {
2956
+ nested: {
2957
+ is: {
2958
+ aNumber: { gt: 1 },
2959
+ },
2960
+ },
2961
+ },
2962
+ {
2963
+ nested: {
2964
+ is: {
2965
+ aNumber: { lt: 3 },
2966
+ },
2967
+ },
2968
+ },
2969
+ ],
2970
+ },
2971
+ });
2972
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
2973
+ throw new Error("Expected CONDITIONAL result");
2974
+ }
2975
+ const query = await prisma.resource.findMany({
2976
+ where: { ...result.filters },
2977
+ });
2978
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
2979
+ .filter((r) => {
2980
+ const nestedResource = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
2981
+ return ((nestedResource?.aNumber ?? 0) > 1 &&
2982
+ (nestedResource?.aNumber ?? 0) < 3);
2983
+ })
2984
+ .map((r) => r.id));
2985
+ });
2986
+ (0, globals_1.test)("conditional - relation multiple or", async () => {
2987
+ const queryPlan = await cerbos.planResources({
2988
+ principal: { id: "user1", roles: ["USER"] },
2989
+ resource: { kind: "resource" },
2990
+ action: "relation-multiple-or",
2991
+ });
2992
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
2993
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
2994
+ operator: "or",
2995
+ operands: [
2996
+ {
2997
+ operator: "eq",
2998
+ operands: [
2999
+ { name: "request.resource.attr.createdBy" },
3000
+ { value: "user1" },
3001
+ ],
3002
+ },
3003
+ {
3004
+ operator: "in",
3005
+ operands: [
3006
+ { value: "user1" },
3007
+ { name: "request.resource.attr.ownedBy" },
3008
+ ],
3009
+ },
3010
+ ],
3011
+ });
3012
+ const result = (0, _1.queryPlanToPrisma)({
3013
+ queryPlan,
3014
+ mapper: {
3015
+ "request.resource.attr.createdBy": {
3016
+ relation: {
3017
+ name: "createdBy",
3018
+ type: "one",
3019
+ field: "id",
3020
+ },
3021
+ },
3022
+ "request.resource.attr.ownedBy": {
3023
+ relation: {
3024
+ name: "ownedBy",
3025
+ type: "many",
3026
+ field: "id",
3027
+ },
3028
+ },
3029
+ },
3030
+ });
3031
+ (0, globals_1.expect)(result).toStrictEqual({
3032
+ kind: _1.PlanKind.CONDITIONAL,
3033
+ filters: {
3034
+ OR: [
3035
+ {
3036
+ createdBy: {
3037
+ is: {
3038
+ id: {
3039
+ equals: "user1",
3040
+ },
3041
+ },
3042
+ },
3043
+ },
3044
+ {
3045
+ ownedBy: {
3046
+ some: {
3047
+ id: "user1",
3048
+ },
3049
+ },
3050
+ },
3051
+ ],
3052
+ },
3053
+ });
3054
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3055
+ throw new Error("Expected CONDITIONAL result");
3056
+ }
3057
+ const query = await prisma.resource.findMany({
3058
+ where: { ...result.filters },
3059
+ });
3060
+ // Should return resources where user1 is either creator or owner
3061
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3062
+ .filter((r) => {
3063
+ const createdBy = r.createdBy?.connect?.id;
3064
+ const ownedBy = Array.isArray(r.ownedBy?.connect)
3065
+ ? r.ownedBy?.connect.find((o) => o.id == "user1")
3066
+ : undefined;
3067
+ return createdBy == "user1" || ownedBy;
3068
+ })
3069
+ .map((r) => r.id));
3070
+ });
3071
+ (0, globals_1.test)("conditional - relation multiple none", async () => {
3072
+ const queryPlan = await cerbos.planResources({
3073
+ principal: { id: "user1", roles: ["USER"] },
3074
+ resource: { kind: "resource" },
3075
+ action: "relation-multiple-none",
3076
+ });
3077
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
3078
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
3079
+ operator: "and",
3080
+ operands: [
3081
+ {
3082
+ operator: "not",
3083
+ operands: [
3084
+ {
3085
+ operator: "eq",
3086
+ operands: [
3087
+ { name: "request.resource.attr.createdBy" },
3088
+ { value: "user1" },
3089
+ ],
3090
+ },
3091
+ ],
3092
+ },
3093
+ {
3094
+ operator: "not",
3095
+ operands: [
3096
+ {
3097
+ operator: "in",
3098
+ operands: [
3099
+ { value: "public" },
3100
+ { name: "request.resource.attr.tags" },
3101
+ ],
3102
+ },
3103
+ ],
3104
+ },
3105
+ ],
3106
+ });
3107
+ const result = (0, _1.queryPlanToPrisma)({
3108
+ queryPlan,
3109
+ mapper: {
3110
+ "request.resource.attr.createdBy": {
3111
+ relation: {
3112
+ name: "createdBy",
3113
+ type: "one",
3114
+ field: "id",
3115
+ },
3116
+ },
3117
+ "request.resource.attr.tags": {
3118
+ relation: {
3119
+ name: "tags",
3120
+ type: "many",
3121
+ field: "name",
3122
+ },
3123
+ },
3124
+ },
3125
+ });
3126
+ (0, globals_1.expect)(result).toStrictEqual({
3127
+ kind: _1.PlanKind.CONDITIONAL,
3128
+ filters: {
3129
+ AND: [
3130
+ {
3131
+ NOT: {
3132
+ createdBy: {
3133
+ is: {
3134
+ id: {
3135
+ equals: "user1",
3136
+ },
3137
+ },
3138
+ },
3139
+ },
3140
+ },
3141
+ {
3142
+ NOT: {
3143
+ tags: {
3144
+ some: {
3145
+ name: "public",
3146
+ },
3147
+ },
3148
+ },
3149
+ },
3150
+ ],
3151
+ },
3152
+ });
3153
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3154
+ throw new Error("Expected CONDITIONAL result");
3155
+ }
3156
+ const query = await prisma.resource.findMany({
3157
+ where: { ...result.filters },
3158
+ });
3159
+ // Should return resources where user1 is creator AND has public tag
3160
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3161
+ .filter((r) => {
3162
+ const createdBy = r.createdBy?.connect?.id;
3163
+ const hasPublicTag = Array.isArray(r.tags?.connect) &&
3164
+ r.tags?.connect.find((t) => t.id === "tag1"); // tag1 is public
3165
+ return !(createdBy === "user1" && hasPublicTag);
3166
+ })
3167
+ .map((r) => r.id));
3168
+ });
3169
+ });
3170
+ (0, globals_1.describe)("Collection Operations", () => {
3171
+ (0, globals_1.test)("conditional - exists on nested collection", async () => {
3172
+ const queryPlan = await cerbos.planResources({
3173
+ principal: { id: "user1", roles: ["USER"] },
3174
+ resource: { kind: "resource" },
3175
+ action: "exists-nested-collection",
3176
+ });
3177
+ const result = (0, _1.queryPlanToPrisma)({
3178
+ queryPlan,
3179
+ mapper: {
3180
+ "request.resource.attr.categories": {
3181
+ relation: {
3182
+ name: "categories",
3183
+ type: "many",
3184
+ fields: {
3185
+ name: { field: "name" },
3186
+ subCategories: {
3187
+ relation: {
3188
+ name: "subCategories",
3189
+ type: "many",
3190
+ fields: {
3191
+ name: { field: "name" },
3192
+ },
3193
+ },
3194
+ },
3195
+ },
3196
+ },
3197
+ },
3198
+ },
3199
+ });
3200
+ (0, globals_1.expect)(result).toStrictEqual({
3201
+ kind: _1.PlanKind.CONDITIONAL,
3202
+ filters: {
3203
+ categories: {
3204
+ some: {
3205
+ AND: [
3206
+ { name: { equals: "business" } },
3207
+ {
3208
+ subCategories: {
3209
+ some: {
3210
+ name: { equals: "finance" },
3211
+ },
3212
+ },
3213
+ },
3214
+ ],
3215
+ },
3216
+ },
3217
+ },
3218
+ });
3219
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3220
+ throw new Error("Expected CONDITIONAL result");
3221
+ }
3222
+ const query = await prisma.resource.findMany({
3223
+ where: { ...result.filters },
3224
+ });
3225
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3226
+ .filter((r) => {
3227
+ const categoryConnections = r.categories
3228
+ ?.connect;
3229
+ return categoryConnections?.some((categoryConnection) => {
3230
+ const category = fixtureCategories.find((fc) => fc.id === categoryConnection.id);
3231
+ return (category?.name === "business" &&
3232
+ category?.subCategories
3233
+ ?.connect?.some((sc) => fixtureSubCategories.find((fsc) => fsc.id === sc.id)
3234
+ ?.name === "finance"));
3235
+ });
3236
+ })
3237
+ .map((r) => r.id));
3238
+ });
3239
+ (0, globals_1.test)("isSet with nested relation", async () => {
3240
+ const queryPlan = await cerbos.planResources({
3241
+ principal: { id: "user1", roles: ["USER"] },
3242
+ resource: { kind: "resource" },
3243
+ action: "is-set-nested",
3244
+ });
3245
+ const result = (0, _1.queryPlanToPrisma)({
3246
+ queryPlan,
3247
+ mapper: {
3248
+ "request.resource.attr.nested.aOptionalString": {
3249
+ relation: {
3250
+ name: "nested",
3251
+ type: "one",
3252
+ fields: {
3253
+ aOptionalString: { field: "aOptionalString" },
3254
+ },
3255
+ },
3256
+ },
3257
+ },
3258
+ });
3259
+ (0, globals_1.expect)(result).toStrictEqual({
3260
+ kind: _1.PlanKind.CONDITIONAL,
3261
+ filters: {
3262
+ nested: {
3263
+ is: {
3264
+ aOptionalString: { not: null },
3265
+ },
3266
+ },
3267
+ },
3268
+ });
3269
+ });
3270
+ (0, globals_1.test)("hasIntersection with nested collection", async () => {
3271
+ const queryPlan = await cerbos.planResources({
3272
+ principal: { id: "user1", roles: ["USER"] },
3273
+ resource: { kind: "resource" },
3274
+ action: "has-intersection-nested",
3275
+ });
3276
+ const result = (0, _1.queryPlanToPrisma)({
3277
+ queryPlan,
3278
+ mapper: {
3279
+ "request.resource.attr.categories": {
3280
+ relation: {
3281
+ name: "categories",
3282
+ type: "many",
3283
+ fields: {
3284
+ subCategories: {
3285
+ relation: {
3286
+ name: "subCategories",
3287
+ type: "many",
3288
+ fields: {
3289
+ name: { field: "name" },
3290
+ },
3291
+ },
3292
+ },
3293
+ },
3294
+ },
3295
+ },
3296
+ },
3297
+ });
3298
+ (0, globals_1.expect)(result).toStrictEqual({
3299
+ kind: _1.PlanKind.CONDITIONAL,
3300
+ filters: {
3301
+ categories: {
3302
+ some: {
3303
+ subCategories: {
3304
+ some: {
3305
+ name: { in: ["finance", "tech"] },
3306
+ },
3307
+ },
3308
+ },
3309
+ },
3310
+ },
3311
+ });
3312
+ });
3313
+ });
3314
+ (0, globals_1.describe)("Deep Nesting", () => {
3315
+ (0, globals_1.test)("filter on deeply nested relation", async () => {
3316
+ const queryPlan = await cerbos.planResources({
3317
+ principal: { id: "user1", roles: ["USER"] },
3318
+ resource: { kind: "resource" },
3319
+ action: "filter-deeply-nested",
3320
+ });
3321
+ const result = (0, _1.queryPlanToPrisma)({
3322
+ queryPlan,
3323
+ mapper: {
3324
+ "request.resource.attr.categories": {
3325
+ relation: {
3326
+ name: "categories",
3327
+ type: "many",
3328
+ fields: {
3329
+ subCategories: {
3330
+ relation: {
3331
+ name: "subCategories",
3332
+ type: "many",
3333
+ fields: {
3334
+ labels: {
3335
+ relation: {
3336
+ name: "labels",
3337
+ type: "many",
3338
+ fields: {
3339
+ name: { field: "name" },
3340
+ },
3341
+ },
3342
+ },
3343
+ },
3344
+ },
3345
+ },
3346
+ },
3347
+ },
3348
+ },
3349
+ },
3350
+ });
3351
+ (0, globals_1.expect)(result).toStrictEqual({
3352
+ kind: _1.PlanKind.CONDITIONAL,
3353
+ filters: {
3354
+ categories: {
3355
+ some: {
3356
+ subCategories: {
3357
+ some: {
3358
+ labels: {
3359
+ some: {
3360
+ name: { equals: "important" },
3361
+ },
3362
+ },
3363
+ },
3364
+ },
3365
+ },
3366
+ },
3367
+ },
3368
+ });
3369
+ });
3370
+ (0, globals_1.test)("map on deeply nested relation", async () => {
3371
+ const queryPlan = await cerbos.planResources({
3372
+ principal: { id: "user1", roles: ["USER"] },
3373
+ resource: { kind: "resource" },
3374
+ action: "map-deeply-nested",
3375
+ });
3376
+ const result = (0, _1.queryPlanToPrisma)({
3377
+ queryPlan,
3378
+ mapper: {
3379
+ "request.resource.attr.categories": {
3380
+ relation: {
3381
+ name: "categories",
3382
+ type: "many",
3383
+ fields: {
3384
+ subCategories: {
3385
+ relation: {
3386
+ name: "subCategories",
3387
+ type: "many",
3388
+ fields: {
3389
+ name: { field: "name" },
3390
+ },
3391
+ },
3392
+ },
3393
+ },
3394
+ },
3395
+ },
3396
+ },
3397
+ });
3398
+ (0, globals_1.expect)(result).toStrictEqual({
3399
+ kind: _1.PlanKind.CONDITIONAL,
3400
+ filters: {
3401
+ categories: {
3402
+ some: {
3403
+ subCategories: {
3404
+ some: {
3405
+ name: { in: ["finance", "tech"] },
3406
+ },
3407
+ },
3408
+ },
3409
+ },
3410
+ },
3411
+ });
3412
+ });
3413
+ });
3414
+ });
3415
+ // Error Handling
3416
+ (0, globals_1.describe)("Error Cases", () => {
3417
+ (0, globals_1.test)("throws error for invalid query plan", () => {
3418
+ const invalidQueryPlan = {
3419
+ kind: "INVALID_KIND",
3420
+ };
3421
+ (0, globals_1.expect)(() => (0, _1.queryPlanToPrisma)({
3422
+ queryPlan: invalidQueryPlan,
3423
+ mapper: {},
3424
+ })).toThrow("Invalid query plan.");
3425
+ });
3426
+ (0, globals_1.test)("throws error for invalid expression structure", () => {
3427
+ const invalidQueryPlan = {
3428
+ kind: _1.PlanKind.CONDITIONAL,
3429
+ condition: {
3430
+ // Missing operator and operands
3431
+ },
3432
+ };
3433
+ (0, globals_1.expect)(() => (0, _1.queryPlanToPrisma)({
3434
+ queryPlan: invalidQueryPlan,
3435
+ mapper: {},
3436
+ })).toThrow("Invalid Cerbos expression structure");
3437
+ });
3438
+ (0, globals_1.test)("throws error for unsupported operator", () => {
3439
+ const invalidQueryPlan = {
3440
+ kind: _1.PlanKind.CONDITIONAL,
3441
+ condition: {
3442
+ operator: "unsupported",
3443
+ operands: [],
3444
+ },
3445
+ };
3446
+ (0, globals_1.expect)(() => (0, _1.queryPlanToPrisma)({
3447
+ queryPlan: invalidQueryPlan,
3448
+ mapper: {},
3449
+ })).toThrow("Unsupported operator: unsupported");
3450
+ });
3451
+ (0, globals_1.test)("throws error for invalid operand structure", () => {
3452
+ const invalidQueryPlan = {
3453
+ kind: _1.PlanKind.CONDITIONAL,
3454
+ condition: {
3455
+ operator: "eq",
3456
+ operands: [
3457
+ {}, // Invalid operand without name or value
3458
+ { value: "test" },
3459
+ ],
3460
+ },
3461
+ };
3462
+ (0, globals_1.expect)(() => (0, _1.queryPlanToPrisma)({
3463
+ queryPlan: invalidQueryPlan,
3464
+ mapper: {},
3465
+ })).toThrow("No valid left operand found");
3466
+ });
3467
+ });
3468
+ // Utility Tests
3469
+ (0, globals_1.describe)("Mapper Functions", () => {
3470
+ (0, globals_1.test)("function mapper for field names", async () => {
3471
+ const queryPlan = await cerbos.planResources({
3472
+ principal: { id: "user1", roles: ["USER"] },
3473
+ resource: { kind: "resource" },
3474
+ action: "equal",
3475
+ });
3476
+ const result = (0, _1.queryPlanToPrisma)({
3477
+ queryPlan,
3478
+ mapper: (key) => ({
3479
+ field: key.replace("request.resource.attr.", ""),
3480
+ }),
3481
+ });
3482
+ (0, globals_1.expect)(result).toStrictEqual({
3483
+ kind: _1.PlanKind.CONDITIONAL,
3484
+ filters: { aBool: { equals: true } },
3485
+ });
3486
+ });
3487
+ (0, globals_1.test)("function mapper for relations", async () => {
3488
+ const queryPlan = await cerbos.planResources({
3489
+ principal: { id: "user1", roles: ["USER"] },
3490
+ resource: { kind: "resource" },
3491
+ action: "relation-is",
3492
+ });
3493
+ const result = (0, _1.queryPlanToPrisma)({
3494
+ queryPlan,
3495
+ mapper: () => ({
3496
+ relation: {
3497
+ name: "createdBy",
3498
+ type: "one",
3499
+ field: "id",
3500
+ },
3501
+ }),
3502
+ });
3503
+ (0, globals_1.expect)(result).toStrictEqual({
3504
+ kind: _1.PlanKind.CONDITIONAL,
3505
+ filters: {
3506
+ createdBy: {
3507
+ is: {
3508
+ id: {
3509
+ equals: "user1",
3510
+ },
3511
+ },
3512
+ },
3513
+ },
3514
+ });
3515
+ });
3516
+ });
3517
+ // Integration
3518
+ (0, globals_1.describe)("Integration", () => {
3519
+ (0, globals_1.test)("conditional - kitchen sink", async () => {
3520
+ const queryPlan = await cerbos.planResources({
3521
+ principal: {
3522
+ id: "user1",
3523
+ roles: ["USER"],
3524
+ attr: { tags: ["public", "draft"] },
3525
+ },
3526
+ resource: { kind: "resource" },
3527
+ action: "kitchensink",
3528
+ });
3529
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
3530
+ // expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual({
3531
+ // operator: "filter",
3532
+ // operands: [
3533
+ // {
3534
+ // name: "request.resource.attr.tags",
3535
+ // },
3536
+ // {
3537
+ // operator: "lambda",
3538
+ // operands: [
3539
+ // {
3540
+ // operator: "eq",
3541
+ // operands: [
3542
+ // {
3543
+ // name: "tag.name",
3544
+ // },
3545
+ // {
3546
+ // value: "public",
3547
+ // },
3548
+ // ],
3549
+ // },
3550
+ // {
3551
+ // name: "tag",
3552
+ // },
3553
+ // ],
3554
+ // },
3555
+ // ],
3556
+ // });
3557
+ const result = (0, _1.queryPlanToPrisma)({
3558
+ queryPlan,
3559
+ mapper: {
3560
+ "request.resource.attr.aOptionalString": { field: "aOptionalString" },
3561
+ "request.resource.attr.aBool": { field: "aBool" },
3562
+ "request.resource.attr.aString": { field: "aString" },
3563
+ "request.resource.attr.tags": {
3564
+ relation: {
3565
+ name: "tags",
3566
+ type: "many",
3567
+ },
3568
+ },
3569
+ "request.resource.attr.nested": {
3570
+ relation: {
3571
+ name: "nested",
3572
+ type: "one",
3573
+ fields: {
3574
+ nextlevel: {
3575
+ relation: {
3576
+ name: "nextlevel",
3577
+ type: "one",
3578
+ fields: {
3579
+ aBool: { field: "aBool" },
3580
+ },
3581
+ },
3582
+ },
3583
+ },
3584
+ },
3585
+ },
3586
+ },
3587
+ });
3588
+ // expect(result).toStrictEqual({
3589
+ // kind: PlanKind.CONDITIONAL,
3590
+ // filters: {
3591
+ // tags: {
3592
+ // some: {
3593
+ // name: { equals: "public" },
3594
+ // },
3595
+ // },
3596
+ // },
3597
+ // });
3598
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3599
+ throw new Error("Expected CONDITIONAL result");
3600
+ }
3601
+ // console.log(JSON.stringify(result, null, 2));
3602
+ await prisma.resource.findMany({
3603
+ where: { ...result.filters },
3604
+ });
3605
+ // expect(query.map((r) => r.id)).toEqual(
3606
+ // fixtureResources
3607
+ // .filter(
3608
+ // (a) =>
3609
+ // Array.isArray(a.tags?.connect) &&
3610
+ // a.tags?.connect
3611
+ // .map((t) => {
3612
+ // return fixtureTags.find((f) => f.id === t.id);
3613
+ // })
3614
+ // .filter((t) => t?.name === "public").length > 0
3615
+ // )
3616
+ // .map((r) => r.id)
3617
+ // );
3618
+ });
3619
+ });
3620
+ (0, globals_1.describe)("Deep Nested Relations", () => {
3621
+ (0, globals_1.test)("conditional - deeply nested many to many category label", async () => {
3622
+ const queryPlan = await cerbos.planResources({
3623
+ principal: { id: "user1", roles: ["USER"] },
3624
+ resource: { kind: "resource" },
3625
+ action: "deep-nested-category-label",
3626
+ });
3627
+ (0, globals_1.expect)(queryPlan.kind).toEqual(_1.PlanKind.CONDITIONAL);
3628
+ (0, globals_1.expect)(queryPlan.condition).toEqual({
3629
+ operator: "exists",
3630
+ operands: [
3631
+ {
3632
+ name: "request.resource.attr.categories",
3633
+ },
3634
+ {
3635
+ operator: "lambda",
3636
+ operands: [
3637
+ {
3638
+ operator: "exists",
3639
+ operands: [
3640
+ {
3641
+ name: "cat.subCategories",
3642
+ },
3643
+ {
3644
+ operator: "lambda",
3645
+ operands: [
3646
+ {
3647
+ operator: "exists",
3648
+ operands: [
3649
+ {
3650
+ name: "sub.labels",
3651
+ },
3652
+ {
3653
+ operator: "lambda",
3654
+ operands: [
3655
+ {
3656
+ operator: "eq",
3657
+ operands: [
3658
+ {
3659
+ name: "label.name",
3660
+ },
3661
+ {
3662
+ value: "important",
3663
+ },
3664
+ ],
3665
+ },
3666
+ {
3667
+ name: "label",
3668
+ },
3669
+ ],
3670
+ },
3671
+ ],
3672
+ },
3673
+ {
3674
+ name: "sub",
3675
+ },
3676
+ ],
3677
+ },
3678
+ ],
3679
+ },
3680
+ {
3681
+ name: "cat",
3682
+ },
3683
+ ],
3684
+ },
3685
+ ],
3686
+ });
3687
+ const result = (0, _1.queryPlanToPrisma)({
3688
+ queryPlan,
3689
+ mapper: {
3690
+ "request.resource.attr.categories": {
3691
+ relation: {
3692
+ name: "categories",
3693
+ type: "many",
3694
+ fields: {
3695
+ subCategories: {
3696
+ relation: {
3697
+ name: "subCategories",
3698
+ type: "many",
3699
+ fields: {
3700
+ labels: {
3701
+ relation: {
3702
+ name: "labels",
3703
+ type: "many",
3704
+ fields: {
3705
+ name: { field: "name" },
3706
+ },
3707
+ },
3708
+ },
3709
+ },
3710
+ },
3711
+ },
3712
+ },
3713
+ },
3714
+ },
3715
+ },
3716
+ });
3717
+ (0, globals_1.expect)(result).toStrictEqual({
3718
+ kind: _1.PlanKind.CONDITIONAL,
3719
+ filters: {
3720
+ categories: {
3721
+ some: {
3722
+ subCategories: {
3723
+ some: {
3724
+ labels: {
3725
+ some: {
3726
+ name: { equals: "important" },
3727
+ },
3728
+ },
3729
+ },
3730
+ },
3731
+ },
3732
+ },
3733
+ },
3734
+ });
3735
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3736
+ throw new Error("Expected CONDITIONAL result");
3737
+ }
3738
+ const query = await prisma.resource.findMany({
3739
+ where: { ...result.filters },
3740
+ include: {
3741
+ categories: {
3742
+ include: {
3743
+ subCategories: {
3744
+ include: {
3745
+ labels: true,
3746
+ },
3747
+ },
3748
+ },
3749
+ },
3750
+ },
3751
+ });
3752
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3753
+ .filter((r) => {
3754
+ return (r.categories?.connect)
3755
+ .map((c) => {
3756
+ return fixtureCategories.find((fc) => fc.id === c.id);
3757
+ })
3758
+ .some((c) => {
3759
+ return (c?.subCategories
3760
+ ?.connect)
3761
+ .map((sc) => {
3762
+ return fixtureSubCategories.find((fsc) => fsc.id === sc.id);
3763
+ })
3764
+ .some((sc) => {
3765
+ return (sc?.labels?.connect)
3766
+ .map((l) => {
3767
+ return fixtureLabels.find((fl) => fl.id === l.id);
3768
+ })
3769
+ .some((l) => l?.name === "important");
3770
+ });
3771
+ });
3772
+ })
3773
+ .map((r) => r.id));
3774
+ });
3775
+ (0, globals_1.test)("conditional - deep nested exists with multiple conditions", async () => {
3776
+ const queryPlan = await cerbos.planResources({
3777
+ principal: { id: "user1", roles: ["USER"] },
3778
+ resource: { kind: "resource" },
3779
+ action: "deep-nested-exists",
3780
+ });
3781
+ const result = (0, _1.queryPlanToPrisma)({
3782
+ queryPlan,
3783
+ mapper: {
3784
+ "request.resource.attr.categories": {
3785
+ relation: {
3786
+ name: "categories",
3787
+ type: "many",
3788
+ fields: {
3789
+ subCategories: {
3790
+ relation: {
3791
+ name: "subCategories",
3792
+ type: "many",
3793
+ field: "name",
3794
+ },
3795
+ },
3796
+ },
3797
+ },
3798
+ },
3799
+ },
3800
+ });
3801
+ (0, globals_1.expect)(result).toStrictEqual({
3802
+ kind: _1.PlanKind.CONDITIONAL,
3803
+ filters: {
3804
+ categories: {
3805
+ some: {
3806
+ AND: [
3807
+ { name: { equals: "business" } },
3808
+ {
3809
+ subCategories: {
3810
+ some: {
3811
+ name: { equals: "finance" },
3812
+ },
3813
+ },
3814
+ },
3815
+ ],
3816
+ },
3817
+ },
3818
+ },
3819
+ });
3820
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3821
+ throw new Error("Expected CONDITIONAL result");
3822
+ }
3823
+ const query = await prisma.resource.findMany({
3824
+ where: { ...result.filters },
3825
+ });
3826
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3827
+ .filter((r) => {
3828
+ const categoryConnections = r.categories
3829
+ ?.connect;
3830
+ return (categoryConnections?.some((categoryConnection) => {
3831
+ const category = fixtureCategories.find((fc) => fc.id === categoryConnection.id);
3832
+ return (category?.name === "business" &&
3833
+ category?.subCategories
3834
+ ?.connect?.some((sc) => fixtureSubCategories.find((fsc) => fsc.id === sc.id)
3835
+ ?.name === "finance"));
3836
+ }) ?? false);
3837
+ })
3838
+ .map((r) => r.id));
3839
+ });
3840
+ });
3841
+ (0, globals_1.describe)("Nested Relations String Operations", () => {
3842
+ (0, globals_1.test)("conditional - nested contains", async () => {
3843
+ const queryPlan = await cerbos.planResources({
3844
+ principal: { id: "user1", roles: ["USER"] },
3845
+ resource: { kind: "resource" },
3846
+ action: "nested-contains",
3847
+ });
3848
+ const result = (0, _1.queryPlanToPrisma)({
3849
+ queryPlan,
3850
+ mapper: {
3851
+ "request.resource.attr.nested": {
3852
+ relation: {
3853
+ name: "nested",
3854
+ type: "one",
3855
+ fields: {
3856
+ aString: { field: "aString" },
3857
+ },
3858
+ },
3859
+ },
3860
+ },
3861
+ });
3862
+ (0, globals_1.expect)(result).toStrictEqual({
3863
+ kind: _1.PlanKind.CONDITIONAL,
3864
+ filters: {
3865
+ nested: {
3866
+ is: {
3867
+ aString: { contains: "str" },
3868
+ },
3869
+ },
3870
+ },
3871
+ });
3872
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3873
+ throw new Error("Expected CONDITIONAL result");
3874
+ }
3875
+ const query = await prisma.resource.findMany({
3876
+ where: { ...result.filters },
3877
+ });
3878
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3879
+ .filter((r) => {
3880
+ const nested = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
3881
+ return nested?.aString.includes("str");
3882
+ })
3883
+ .map((r) => r.id));
3884
+ });
3885
+ (0, globals_1.test)("conditional - deeply nested startsWith", async () => {
3886
+ const queryPlan = await cerbos.planResources({
3887
+ principal: { id: "user1", roles: ["USER"] },
3888
+ resource: { kind: "resource" },
3889
+ action: "deeply-nested-starts-with",
3890
+ });
3891
+ const result = (0, _1.queryPlanToPrisma)({
3892
+ queryPlan,
3893
+ mapper: {
3894
+ "request.resource.attr.nested": {
3895
+ relation: {
3896
+ name: "nested",
3897
+ type: "one",
3898
+ fields: {
3899
+ nextlevel: {
3900
+ relation: {
3901
+ name: "nextlevel",
3902
+ type: "one",
3903
+ fields: {
3904
+ aString: { field: "aString" },
3905
+ },
3906
+ },
3907
+ },
3908
+ },
3909
+ },
3910
+ },
3911
+ },
3912
+ });
3913
+ (0, globals_1.expect)(result).toStrictEqual({
3914
+ kind: _1.PlanKind.CONDITIONAL,
3915
+ filters: {
3916
+ nested: {
3917
+ is: {
3918
+ nextlevel: {
3919
+ is: {
3920
+ aString: { startsWith: "str" },
3921
+ },
3922
+ },
3923
+ },
3924
+ },
3925
+ },
3926
+ });
3927
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3928
+ throw new Error("Expected CONDITIONAL result");
3929
+ }
3930
+ const query = await prisma.resource.findMany({
3931
+ where: { ...result.filters },
3932
+ });
3933
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3934
+ .filter((r) => {
3935
+ const nested = fixtureNestedResources.find((n) => n.id === r.nested.connect?.id);
3936
+ const nextLevel = fixtureNextLevelResources.find((nl) => nl.id === nested?.nextlevel.connect?.id);
3937
+ return nextLevel?.aString.startsWith("str");
3938
+ })
3939
+ .map((r) => r.id));
3940
+ });
3941
+ });
3942
+ (0, globals_1.describe)("Collection Operations with Nested Relations", () => {
3943
+ (0, globals_1.test)("conditional - exists on nested collection", async () => {
3944
+ const queryPlan = await cerbos.planResources({
3945
+ principal: { id: "user1", roles: ["USER"] },
3946
+ resource: { kind: "resource" },
3947
+ action: "exists-nested-collection",
3948
+ });
3949
+ const result = (0, _1.queryPlanToPrisma)({
3950
+ queryPlan,
3951
+ mapper: {
3952
+ "request.resource.attr.categories": {
3953
+ relation: {
3954
+ name: "categories",
3955
+ type: "many",
3956
+ fields: {
3957
+ name: { field: "name" },
3958
+ subCategories: {
3959
+ relation: {
3960
+ name: "subCategories",
3961
+ type: "many",
3962
+ fields: {
3963
+ name: { field: "name" },
3964
+ },
3965
+ },
3966
+ },
3967
+ },
3968
+ },
3969
+ },
3970
+ },
3971
+ });
3972
+ (0, globals_1.expect)(result).toStrictEqual({
3973
+ kind: _1.PlanKind.CONDITIONAL,
3974
+ filters: {
3975
+ categories: {
3976
+ some: {
3977
+ AND: [
3978
+ { name: { equals: "business" } },
3979
+ {
3980
+ subCategories: {
3981
+ some: {
3982
+ name: { equals: "finance" },
3983
+ },
3984
+ },
3985
+ },
3986
+ ],
3987
+ },
3988
+ },
3989
+ },
3990
+ });
3991
+ if (result.kind !== _1.PlanKind.CONDITIONAL) {
3992
+ throw new Error("Expected CONDITIONAL result");
3993
+ }
3994
+ const query = await prisma.resource.findMany({
3995
+ where: { ...result.filters },
3996
+ });
3997
+ (0, globals_1.expect)(query.map((r) => r.id)).toEqual(fixtureResources
3998
+ .filter((r) => {
3999
+ const categoryConnections = r.categories
4000
+ ?.connect;
4001
+ return categoryConnections?.some((categoryConnection) => {
4002
+ const category = fixtureCategories.find((fc) => fc.id === categoryConnection.id);
4003
+ return (category?.name === "business" &&
4004
+ category?.subCategories
4005
+ ?.connect?.some((sc) => fixtureSubCategories.find((fsc) => fsc.id === sc.id)?.name ===
4006
+ "finance"));
4007
+ });
4008
+ })
4009
+ .map((r) => r.id));
4010
+ });
4011
+ });
4012
+ // Types
4013
+ (0, globals_1.describe)("Return Types", () => {
4014
+ (0, globals_1.test)("returns ALWAYS_ALLOWED type", async () => {
4015
+ const queryPlan = await cerbos.planResources({
4016
+ principal: { id: "user1", roles: ["USER"] },
4017
+ resource: { kind: "resource" },
4018
+ action: "always-allow",
4019
+ });
4020
+ const result = (0, _1.queryPlanToPrisma)({
4021
+ queryPlan,
4022
+ });
4023
+ (0, globals_1.expect)(result).toStrictEqual({
4024
+ kind: _1.PlanKind.ALWAYS_ALLOWED,
4025
+ });
4026
+ // Type assertion check
4027
+ if (result.kind === _1.PlanKind.ALWAYS_ALLOWED) {
4028
+ (0, globals_1.expect)(Object.keys(result)).toEqual(["kind"]);
4029
+ }
4030
+ else {
4031
+ throw new Error("Expected ALWAYS_ALLOWED result");
4032
+ }
4033
+ });
4034
+ (0, globals_1.test)("returns ALWAYS_DENIED type", async () => {
4035
+ const queryPlan = await cerbos.planResources({
4036
+ principal: { id: "user1", roles: ["USER"] },
4037
+ resource: { kind: "resource" },
4038
+ action: "always-deny",
4039
+ });
4040
+ const result = (0, _1.queryPlanToPrisma)({
4041
+ queryPlan,
4042
+ });
4043
+ (0, globals_1.expect)(result).toStrictEqual({
4044
+ kind: _1.PlanKind.ALWAYS_DENIED,
4045
+ });
4046
+ // Type assertion check
4047
+ if (result.kind === _1.PlanKind.ALWAYS_DENIED) {
4048
+ (0, globals_1.expect)(Object.keys(result)).toEqual(["kind"]);
4049
+ }
4050
+ else {
4051
+ throw new Error("Expected ALWAYS_DENIED result");
4052
+ }
4053
+ });
4054
+ (0, globals_1.test)("returns CONDITIONAL type with filters", async () => {
4055
+ const queryPlan = await cerbos.planResources({
4056
+ principal: { id: "user1", roles: ["USER"] },
4057
+ resource: { kind: "resource" },
4058
+ action: "equal",
4059
+ });
4060
+ const result = (0, _1.queryPlanToPrisma)({
4061
+ queryPlan,
4062
+ mapper: {
4063
+ "request.resource.attr.aBool": { field: "aBool" },
4064
+ },
4065
+ });
4066
+ (0, globals_1.expect)(result).toStrictEqual({
4067
+ kind: _1.PlanKind.CONDITIONAL,
4068
+ filters: { aBool: { equals: true } },
4069
+ });
4070
+ // Type assertion check
4071
+ if (result.kind === _1.PlanKind.CONDITIONAL) {
4072
+ (0, globals_1.expect)(Object.keys(result).sort()).toEqual(["filters", "kind"].sort());
4073
+ (0, globals_1.expect)(typeof result.filters).toBe("object");
4074
+ }
4075
+ else {
4076
+ throw new Error("Expected CONDITIONAL result");
4077
+ }
4078
+ });
4079
+ (0, globals_1.test)("validates result type structure at compile time", () => {
4080
+ // These should compile
4081
+ const allowed = { kind: _1.PlanKind.ALWAYS_ALLOWED };
4082
+ const denied = { kind: _1.PlanKind.ALWAYS_DENIED };
4083
+ const conditional = {
4084
+ kind: _1.PlanKind.CONDITIONAL,
4085
+ filters: { someField: { equals: true } },
4086
+ };
4087
+ // Verify the objects exist to prevent unused variable warnings
4088
+ (0, globals_1.expect)(allowed.kind).toBe(_1.PlanKind.ALWAYS_ALLOWED);
4089
+ (0, globals_1.expect)(denied.kind).toBe(_1.PlanKind.ALWAYS_DENIED);
4090
+ (0, globals_1.expect)(conditional.kind).toBe(_1.PlanKind.CONDITIONAL);
4091
+ });
4092
+ });
4093
+ //# sourceMappingURL=index.test.js.map