@cerbos/orm-prisma 2.0.1 → 3.1.0

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