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