@cerbos/orm-prisma 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +2 -2
  2. package/lib/index.js +54 -1
  3. package/lib/index.js.map +1 -1
  4. package/package.json +17 -12
  5. package/src/generated/prisma/browser.ts +59 -0
  6. package/src/generated/prisma/client.ts +79 -0
  7. package/src/generated/prisma/commonInputTypes.ts +248 -0
  8. package/src/generated/prisma/enums.ts +15 -0
  9. package/src/generated/prisma/internal/class.ts +262 -0
  10. package/src/generated/prisma/internal/prismaNamespace.ts +1333 -0
  11. package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +168 -0
  12. package/src/generated/prisma/models/Category.ts +1407 -0
  13. package/src/generated/prisma/models/Label.ts +1271 -0
  14. package/src/generated/prisma/models/NestedResource.ts +1557 -0
  15. package/src/generated/prisma/models/NextLevelNestedResource.ts +1332 -0
  16. package/src/generated/prisma/models/Resource.ts +2131 -0
  17. package/src/generated/prisma/models/SubCategory.ts +1407 -0
  18. package/src/generated/prisma/models/Tag.ts +1271 -0
  19. package/src/generated/prisma/models/User.ts +1498 -0
  20. package/src/generated/prisma/models.ts +19 -0
  21. package/src/generated/prisma-v6/client.d.ts +1 -0
  22. package/src/generated/prisma-v6/default.d.ts +1 -0
  23. package/src/generated/prisma-v6/edge.d.ts +1 -0
  24. package/src/generated/prisma-v6/index.d.ts +13639 -0
  25. package/src/generated/prisma-v6/runtime/index-browser.d.ts +370 -0
  26. package/src/generated/prisma-v6/runtime/library.d.ts +3982 -0
  27. package/src/generated/prisma-v6/wasm.d.ts +1 -0
  28. package/src/index.test.ts +702 -2
  29. package/src/index.ts +79 -1
  30. package/src/test-setup.v6.ts +5 -0
  31. package/src/test-setup.v7.ts +7 -0
  32. package/lib/index.test.d.ts +0 -2
  33. package/lib/index.test.d.ts.map +0 -1
  34. package/lib/index.test.js +0 -4093
  35. package/lib/index.test.js.map +0 -1
package/src/index.test.ts CHANGED
@@ -13,10 +13,9 @@ import {
13
13
  PlanResourcesConditionalResponse,
14
14
  PlanResourcesResponse,
15
15
  } from "@cerbos/core";
16
- import { Prisma, PrismaClient } from "@prisma/client";
17
16
  import { GRPC as Cerbos } from "@cerbos/grpc";
17
+ import { prisma, Prisma } from "./test-setup";
18
18
 
19
- const prisma = new PrismaClient();
20
19
  const cerbos = new Cerbos("127.0.0.1:3593", { tls: false });
21
20
 
22
21
  function createConditionalPlan(
@@ -541,6 +540,457 @@ describe("Field Operations", () => {
541
540
  });
542
541
  });
543
542
 
543
+ describe("Bare Boolean Tests", () => {
544
+ test("conditional - bare boolean true (no value operand)", () => {
545
+ // #given
546
+ const queryPlan = createConditionalPlan({
547
+ name: "request.resource.attr.aBool",
548
+ });
549
+
550
+ // #when
551
+ const result = queryPlanToPrisma({
552
+ queryPlan,
553
+ mapper: {
554
+ "request.resource.attr.aBool": { field: "aBool" },
555
+ },
556
+ });
557
+
558
+ // #then
559
+ expect(result).toStrictEqual({
560
+ kind: PlanKind.CONDITIONAL,
561
+ filters: { aBool: { equals: true } },
562
+ });
563
+ });
564
+
565
+ test("conditional - bare boolean negated (not with single named operand)", () => {
566
+ // #given
567
+ const queryPlan = createConditionalPlan({
568
+ operator: "not",
569
+ operands: [{ name: "request.resource.attr.aBool" }],
570
+ });
571
+
572
+ // #when
573
+ const result = queryPlanToPrisma({
574
+ queryPlan,
575
+ mapper: {
576
+ "request.resource.attr.aBool": { field: "aBool" },
577
+ },
578
+ });
579
+
580
+ // #then
581
+ expect(result).toStrictEqual({
582
+ kind: PlanKind.CONDITIONAL,
583
+ filters: { aBool: { equals: false } },
584
+ });
585
+ });
586
+
587
+ test("conditional - bare boolean without mapper", () => {
588
+ // #given
589
+ const queryPlan = createConditionalPlan({
590
+ name: "request.resource.attr.aBool",
591
+ });
592
+
593
+ // #when
594
+ const result = queryPlanToPrisma({ queryPlan });
595
+
596
+ // #then
597
+ expect(result).toStrictEqual({
598
+ kind: PlanKind.CONDITIONAL,
599
+ filters: { "request.resource.attr.aBool": { equals: true } },
600
+ });
601
+ });
602
+
603
+ test("conditional - negated bare boolean without mapper", () => {
604
+ // #given
605
+ const queryPlan = createConditionalPlan({
606
+ operator: "not",
607
+ operands: [{ name: "request.resource.attr.aBool" }],
608
+ });
609
+
610
+ // #when
611
+ const result = queryPlanToPrisma({ queryPlan });
612
+
613
+ // #then
614
+ expect(result).toStrictEqual({
615
+ kind: PlanKind.CONDITIONAL,
616
+ filters: { "request.resource.attr.aBool": { equals: false } },
617
+ });
618
+ });
619
+
620
+ test("conditional - bare boolean inside AND", () => {
621
+ // #given
622
+ const queryPlan = createConditionalPlan({
623
+ operator: "and",
624
+ operands: [
625
+ { name: "request.resource.attr.aBool" },
626
+ {
627
+ operator: "eq",
628
+ operands: [
629
+ { name: "request.resource.attr.aString" },
630
+ { value: "string" },
631
+ ],
632
+ },
633
+ ],
634
+ });
635
+
636
+ // #when
637
+ const result = queryPlanToPrisma({
638
+ queryPlan,
639
+ mapper: {
640
+ "request.resource.attr.aBool": { field: "aBool" },
641
+ "request.resource.attr.aString": { field: "aString" },
642
+ },
643
+ });
644
+
645
+ // #then
646
+ expect(result).toStrictEqual({
647
+ kind: PlanKind.CONDITIONAL,
648
+ filters: {
649
+ AND: [
650
+ { aBool: { equals: true } },
651
+ { aString: { equals: "string" } },
652
+ ],
653
+ },
654
+ });
655
+ });
656
+
657
+ test("conditional - bare boolean inside OR", () => {
658
+ // #given
659
+ const queryPlan = createConditionalPlan({
660
+ operator: "or",
661
+ operands: [
662
+ { name: "request.resource.attr.aBool" },
663
+ {
664
+ operator: "eq",
665
+ operands: [
666
+ { name: "request.resource.attr.aNumber" },
667
+ { value: 1 },
668
+ ],
669
+ },
670
+ ],
671
+ });
672
+
673
+ // #when
674
+ const result = queryPlanToPrisma({
675
+ queryPlan,
676
+ mapper: {
677
+ "request.resource.attr.aBool": { field: "aBool" },
678
+ "request.resource.attr.aNumber": { field: "aNumber" },
679
+ },
680
+ });
681
+
682
+ // #then
683
+ expect(result).toStrictEqual({
684
+ kind: PlanKind.CONDITIONAL,
685
+ filters: {
686
+ OR: [
687
+ { aBool: { equals: true } },
688
+ { aNumber: { equals: 1 } },
689
+ ],
690
+ },
691
+ });
692
+ });
693
+
694
+ test("conditional - double negation bare boolean", () => {
695
+ // #given
696
+ const queryPlan = createConditionalPlan({
697
+ operator: "not",
698
+ operands: [
699
+ {
700
+ operator: "not",
701
+ operands: [{ name: "request.resource.attr.aBool" }],
702
+ },
703
+ ],
704
+ });
705
+
706
+ // #when
707
+ const result = queryPlanToPrisma({
708
+ queryPlan,
709
+ mapper: {
710
+ "request.resource.attr.aBool": { field: "aBool" },
711
+ },
712
+ });
713
+
714
+ // #then
715
+ expect(result).toStrictEqual({
716
+ kind: PlanKind.CONDITIONAL,
717
+ filters: { NOT: { aBool: { equals: false } } },
718
+ });
719
+ });
720
+
721
+ test("conditional - bare boolean with relation", () => {
722
+ // #given
723
+ const queryPlan = createConditionalPlan({
724
+ name: "request.resource.attr.nested.aBool",
725
+ });
726
+
727
+ // #when
728
+ const result = queryPlanToPrisma({
729
+ queryPlan,
730
+ mapper: {
731
+ "request.resource.attr.nested": {
732
+ relation: {
733
+ name: "nestedResource",
734
+ type: "one",
735
+ },
736
+ },
737
+ "request.resource.attr.nested.aBool": {
738
+ relation: {
739
+ name: "nestedResource",
740
+ type: "one",
741
+ },
742
+ field: "aBool",
743
+ },
744
+ },
745
+ });
746
+
747
+ // #then
748
+ expect(result).toStrictEqual({
749
+ kind: PlanKind.CONDITIONAL,
750
+ filters: {
751
+ nestedResource: { is: { aBool: { equals: true } } },
752
+ },
753
+ });
754
+ });
755
+
756
+ test("conditional - negated bare boolean with relation", () => {
757
+ // #given
758
+ const queryPlan = createConditionalPlan({
759
+ operator: "not",
760
+ operands: [{ name: "request.resource.attr.nested.aBool" }],
761
+ });
762
+
763
+ // #when
764
+ const result = queryPlanToPrisma({
765
+ queryPlan,
766
+ mapper: {
767
+ "request.resource.attr.nested": {
768
+ relation: {
769
+ name: "nestedResource",
770
+ type: "one",
771
+ },
772
+ },
773
+ "request.resource.attr.nested.aBool": {
774
+ relation: {
775
+ name: "nestedResource",
776
+ type: "one",
777
+ },
778
+ field: "aBool",
779
+ },
780
+ },
781
+ });
782
+
783
+ // #then
784
+ expect(result).toStrictEqual({
785
+ kind: PlanKind.CONDITIONAL,
786
+ filters: {
787
+ NOT: { nestedResource: { is: { aBool: { equals: true } } } },
788
+ },
789
+ });
790
+ });
791
+
792
+ test("conditional - negated bare boolean with many relation", () => {
793
+ // #given
794
+ const queryPlan = createConditionalPlan({
795
+ operator: "not",
796
+ operands: [{ name: "request.resource.attr.tags.active" }],
797
+ });
798
+
799
+ // #when
800
+ const result = queryPlanToPrisma({
801
+ queryPlan,
802
+ mapper: {
803
+ "request.resource.attr.tags.active": {
804
+ relation: {
805
+ name: "tags",
806
+ type: "many",
807
+ },
808
+ field: "active",
809
+ },
810
+ },
811
+ });
812
+
813
+ // #then — must be NOT { some { equals: true } }, not some { equals: false }
814
+ expect(result).toStrictEqual({
815
+ kind: PlanKind.CONDITIONAL,
816
+ filters: {
817
+ NOT: { tags: { some: { active: { equals: true } } } },
818
+ },
819
+ });
820
+ });
821
+
822
+ test("conditional - bare boolean via cerbos policy", async () => {
823
+ const queryPlan = await cerbos.planResources({
824
+ principal: { id: "user1", roles: ["USER"] },
825
+ resource: { kind: "resource" },
826
+ action: "bare-bool",
827
+ });
828
+
829
+ expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
830
+ expect(
831
+ (queryPlan as PlanResourcesConditionalResponse).condition
832
+ ).toEqual({ name: "request.resource.attr.aBool" });
833
+
834
+ const result = queryPlanToPrisma({
835
+ queryPlan,
836
+ mapper: {
837
+ "request.resource.attr.aBool": { field: "aBool" },
838
+ },
839
+ });
840
+
841
+ expect(result).toStrictEqual({
842
+ kind: PlanKind.CONDITIONAL,
843
+ filters: { aBool: { equals: true } },
844
+ });
845
+
846
+ if (result.kind !== PlanKind.CONDITIONAL) {
847
+ throw new Error("Expected CONDITIONAL result");
848
+ }
849
+
850
+ const query = await prisma.resource.findMany({
851
+ where: { ...result.filters },
852
+ });
853
+ expect(query.map((r) => r.id)).toEqual(
854
+ fixtureResources.filter((a) => a.aBool).map((r) => r.id)
855
+ );
856
+ });
857
+
858
+ test("conditional - negated bare boolean via cerbos policy", async () => {
859
+ const queryPlan = await cerbos.planResources({
860
+ principal: { id: "user1", roles: ["USER"] },
861
+ resource: { kind: "resource" },
862
+ action: "bare-bool-negated",
863
+ });
864
+
865
+ expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
866
+ expect(
867
+ (queryPlan as PlanResourcesConditionalResponse).condition
868
+ ).toEqual({
869
+ operator: "not",
870
+ operands: [{ name: "request.resource.attr.aBool" }],
871
+ });
872
+
873
+ const result = queryPlanToPrisma({
874
+ queryPlan,
875
+ mapper: {
876
+ "request.resource.attr.aBool": { field: "aBool" },
877
+ },
878
+ });
879
+
880
+ expect(result).toStrictEqual({
881
+ kind: PlanKind.CONDITIONAL,
882
+ filters: { aBool: { equals: false } },
883
+ });
884
+
885
+ if (result.kind !== PlanKind.CONDITIONAL) {
886
+ throw new Error("Expected CONDITIONAL result");
887
+ }
888
+
889
+ const query = await prisma.resource.findMany({
890
+ where: { ...result.filters },
891
+ });
892
+ expect(query.map((r) => r.id)).toEqual(
893
+ fixtureResources.filter((a) => !a.aBool).map((r) => r.id)
894
+ );
895
+ });
896
+
897
+ test("conditional - bare boolean nested via cerbos policy", async () => {
898
+ const queryPlan = await cerbos.planResources({
899
+ principal: { id: "user1", roles: ["USER"] },
900
+ resource: { kind: "resource" },
901
+ action: "bare-bool-nested",
902
+ });
903
+
904
+ expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
905
+ expect(
906
+ (queryPlan as PlanResourcesConditionalResponse).condition
907
+ ).toEqual({ name: "request.resource.attr.nested.aBool" });
908
+
909
+ const result = queryPlanToPrisma({
910
+ queryPlan,
911
+ mapper: {
912
+ "request.resource.attr.nested": {
913
+ relation: {
914
+ name: "nested",
915
+ type: "one",
916
+ fields: {
917
+ aBool: { field: "aBool" },
918
+ },
919
+ },
920
+ },
921
+ },
922
+ });
923
+
924
+ expect(result).toStrictEqual({
925
+ kind: PlanKind.CONDITIONAL,
926
+ filters: { nested: { is: { aBool: { equals: true } } } },
927
+ });
928
+
929
+ if (result.kind !== PlanKind.CONDITIONAL) {
930
+ throw new Error("Expected CONDITIONAL result");
931
+ }
932
+
933
+ const query = await prisma.resource.findMany({
934
+ where: { ...result.filters },
935
+ include: { nested: true },
936
+ });
937
+ expect(query.length).toBeGreaterThan(0);
938
+ for (const resource of query) {
939
+ expect(resource.nested.aBool).toBe(true);
940
+ }
941
+ });
942
+
943
+ test("conditional - negated bare boolean nested via cerbos policy", async () => {
944
+ const queryPlan = await cerbos.planResources({
945
+ principal: { id: "user1", roles: ["USER"] },
946
+ resource: { kind: "resource" },
947
+ action: "bare-bool-nested-negated",
948
+ });
949
+
950
+ expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
951
+ expect(
952
+ (queryPlan as PlanResourcesConditionalResponse).condition
953
+ ).toEqual({
954
+ operator: "not",
955
+ operands: [{ name: "request.resource.attr.nested.aBool" }],
956
+ });
957
+
958
+ const result = queryPlanToPrisma({
959
+ queryPlan,
960
+ mapper: {
961
+ "request.resource.attr.nested": {
962
+ relation: {
963
+ name: "nested",
964
+ type: "one",
965
+ fields: {
966
+ aBool: { field: "aBool" },
967
+ },
968
+ },
969
+ },
970
+ },
971
+ });
972
+
973
+ expect(result).toStrictEqual({
974
+ kind: PlanKind.CONDITIONAL,
975
+ filters: {
976
+ NOT: { nested: { is: { aBool: { equals: true } } } },
977
+ },
978
+ });
979
+
980
+ if (result.kind !== PlanKind.CONDITIONAL) {
981
+ throw new Error("Expected CONDITIONAL result");
982
+ }
983
+
984
+ const query = await prisma.resource.findMany({
985
+ where: { ...result.filters },
986
+ include: { nested: true },
987
+ });
988
+ for (const resource of query) {
989
+ expect(resource.nested.aBool).not.toBe(true);
990
+ }
991
+ });
992
+ });
993
+
544
994
  describe("Comparison Tests", () => {
545
995
  test("conditional - gt", async () => {
546
996
  const queryPlan = await cerbos.planResources({
@@ -2464,6 +2914,174 @@ describe("Relations", () => {
2464
2914
  .map((r) => r.id)
2465
2915
  );
2466
2916
  });
2917
+
2918
+ test("conditional - size(field) > 0 produces some", () => {
2919
+ const result = queryPlanToPrisma({
2920
+ queryPlan: createConditionalPlan({
2921
+ operator: "gt",
2922
+ operands: [
2923
+ {
2924
+ operator: "size",
2925
+ operands: [{ name: "request.resource.attr.ownedBy" }],
2926
+ },
2927
+ { value: 0 },
2928
+ ],
2929
+ }),
2930
+ mapper: {
2931
+ "request.resource.attr.ownedBy": {
2932
+ relation: { name: "ownedBy", type: "many", field: "id" },
2933
+ },
2934
+ },
2935
+ });
2936
+
2937
+ expect(result).toStrictEqual({
2938
+ kind: PlanKind.CONDITIONAL,
2939
+ filters: { ownedBy: { some: {} } },
2940
+ });
2941
+ });
2942
+
2943
+ test("conditional - size(field) == 0 produces none", () => {
2944
+ const result = queryPlanToPrisma({
2945
+ queryPlan: createConditionalPlan({
2946
+ operator: "eq",
2947
+ operands: [
2948
+ {
2949
+ operator: "size",
2950
+ operands: [{ name: "request.resource.attr.ownedBy" }],
2951
+ },
2952
+ { value: 0 },
2953
+ ],
2954
+ }),
2955
+ mapper: {
2956
+ "request.resource.attr.ownedBy": {
2957
+ relation: { name: "ownedBy", type: "many", field: "id" },
2958
+ },
2959
+ },
2960
+ });
2961
+
2962
+ expect(result).toStrictEqual({
2963
+ kind: PlanKind.CONDITIONAL,
2964
+ filters: { ownedBy: { none: {} } },
2965
+ });
2966
+ });
2967
+
2968
+ test("conditional - not(size(field) > 0) produces NOT some", () => {
2969
+ const result = queryPlanToPrisma({
2970
+ queryPlan: createConditionalPlan({
2971
+ operator: "not",
2972
+ operands: [
2973
+ {
2974
+ operator: "gt",
2975
+ operands: [
2976
+ {
2977
+ operator: "size",
2978
+ operands: [{ name: "request.resource.attr.ownedBy" }],
2979
+ },
2980
+ { value: 0 },
2981
+ ],
2982
+ },
2983
+ ],
2984
+ }),
2985
+ mapper: {
2986
+ "request.resource.attr.ownedBy": {
2987
+ relation: { name: "ownedBy", type: "many", field: "id" },
2988
+ },
2989
+ },
2990
+ });
2991
+
2992
+ expect(result).toStrictEqual({
2993
+ kind: PlanKind.CONDITIONAL,
2994
+ filters: { NOT: { ownedBy: { some: {} } } },
2995
+ });
2996
+ });
2997
+
2998
+ test("conditional - size(field) >= 1 produces some", () => {
2999
+ const result = queryPlanToPrisma({
3000
+ queryPlan: createConditionalPlan({
3001
+ operator: "ge",
3002
+ operands: [
3003
+ {
3004
+ operator: "size",
3005
+ operands: [{ name: "request.resource.attr.ownedBy" }],
3006
+ },
3007
+ { value: 1 },
3008
+ ],
3009
+ }),
3010
+ mapper: {
3011
+ "request.resource.attr.ownedBy": {
3012
+ relation: { name: "ownedBy", type: "many", field: "id" },
3013
+ },
3014
+ },
3015
+ });
3016
+
3017
+ expect(result).toStrictEqual({
3018
+ kind: PlanKind.CONDITIONAL,
3019
+ filters: { ownedBy: { some: {} } },
3020
+ });
3021
+ });
3022
+
3023
+ test("conditional - size on nested relation walks full chain", () => {
3024
+ const result = queryPlanToPrisma({
3025
+ queryPlan: createConditionalPlan({
3026
+ operator: "gt",
3027
+ operands: [
3028
+ {
3029
+ operator: "size",
3030
+ operands: [
3031
+ { name: "request.resource.attr.team.members" },
3032
+ ],
3033
+ },
3034
+ { value: 0 },
3035
+ ],
3036
+ }),
3037
+ mapper: {
3038
+ "request.resource.attr.team": {
3039
+ relation: {
3040
+ name: "team",
3041
+ type: "one",
3042
+ fields: {
3043
+ members: {
3044
+ relation: { name: "members", type: "many" },
3045
+ },
3046
+ },
3047
+ },
3048
+ },
3049
+ },
3050
+ });
3051
+
3052
+ expect(result).toStrictEqual({
3053
+ kind: PlanKind.CONDITIONAL,
3054
+ filters: {
3055
+ team: {
3056
+ is: {
3057
+ members: { some: {} },
3058
+ },
3059
+ },
3060
+ },
3061
+ });
3062
+ });
3063
+
3064
+ test("conditional - unsupported size comparison throws", () => {
3065
+ expect(() =>
3066
+ queryPlanToPrisma({
3067
+ queryPlan: createConditionalPlan({
3068
+ operator: "gt",
3069
+ operands: [
3070
+ {
3071
+ operator: "size",
3072
+ operands: [{ name: "request.resource.attr.ownedBy" }],
3073
+ },
3074
+ { value: 5 },
3075
+ ],
3076
+ }),
3077
+ mapper: {
3078
+ "request.resource.attr.ownedBy": {
3079
+ relation: { name: "ownedBy", type: "many", field: "id" },
3080
+ },
3081
+ },
3082
+ })
3083
+ ).toThrow("Unsupported size comparison: size(...) gt 5");
3084
+ });
2467
3085
  });
2468
3086
  });
2469
3087
 
@@ -4310,6 +4928,88 @@ describe("Integration", () => {
4310
4928
  // .map((r) => r.id)
4311
4929
  // );
4312
4930
  });
4931
+
4932
+ test("conditional - relation-has-members (size > 0)", async () => {
4933
+ const queryPlan = await cerbos.planResources({
4934
+ principal: { id: "user1", roles: ["USER"] },
4935
+ resource: { kind: "resource" },
4936
+ action: "relation-has-members",
4937
+ });
4938
+
4939
+ expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
4940
+
4941
+ const result = queryPlanToPrisma({
4942
+ queryPlan,
4943
+ mapper: {
4944
+ "request.resource.attr.ownedBy": {
4945
+ relation: { name: "ownedBy", type: "many", field: "id" },
4946
+ },
4947
+ },
4948
+ });
4949
+
4950
+ expect(result).toStrictEqual({
4951
+ kind: PlanKind.CONDITIONAL,
4952
+ filters: { ownedBy: { some: {} } },
4953
+ });
4954
+
4955
+ if (result.kind !== PlanKind.CONDITIONAL) {
4956
+ throw new Error("Expected CONDITIONAL result");
4957
+ }
4958
+
4959
+ const query = await prisma.resource.findMany({
4960
+ where: { ...result.filters },
4961
+ });
4962
+
4963
+ expect(query.map((r) => r.id)).toEqual(
4964
+ fixtureResources
4965
+ .filter((r) => {
4966
+ if (!r.ownedBy?.connect) return false;
4967
+ return (r.ownedBy.connect as { id: string }[]).length > 0;
4968
+ })
4969
+ .map((r) => r.id)
4970
+ );
4971
+ });
4972
+
4973
+ test("conditional - relation-has-no-members (negated size > 0)", async () => {
4974
+ const queryPlan = await cerbos.planResources({
4975
+ principal: { id: "user1", roles: ["USER"] },
4976
+ resource: { kind: "resource" },
4977
+ action: "relation-has-no-members",
4978
+ });
4979
+
4980
+ expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
4981
+
4982
+ const result = queryPlanToPrisma({
4983
+ queryPlan,
4984
+ mapper: {
4985
+ "request.resource.attr.ownedBy": {
4986
+ relation: { name: "ownedBy", type: "many", field: "id" },
4987
+ },
4988
+ },
4989
+ });
4990
+
4991
+ expect(result).toStrictEqual({
4992
+ kind: PlanKind.CONDITIONAL,
4993
+ filters: { NOT: { ownedBy: { some: {} } } },
4994
+ });
4995
+
4996
+ if (result.kind !== PlanKind.CONDITIONAL) {
4997
+ throw new Error("Expected CONDITIONAL result");
4998
+ }
4999
+
5000
+ const query = await prisma.resource.findMany({
5001
+ where: { ...result.filters },
5002
+ });
5003
+
5004
+ expect(query.map((r) => r.id)).toEqual(
5005
+ fixtureResources
5006
+ .filter((r) => {
5007
+ if (!r.ownedBy?.connect) return true;
5008
+ return (r.ownedBy.connect as { id: string }[]).length === 0;
5009
+ })
5010
+ .map((r) => r.id)
5011
+ );
5012
+ });
4313
5013
  });
4314
5014
 
4315
5015
  describe("Deep Nested Relations", () => {