@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.
- package/README.md +2 -2
- package/lib/index.js +54 -1
- package/lib/index.js.map +1 -1
- package/package.json +17 -12
- package/src/generated/prisma/browser.ts +59 -0
- package/src/generated/prisma/client.ts +79 -0
- package/src/generated/prisma/commonInputTypes.ts +248 -0
- package/src/generated/prisma/enums.ts +15 -0
- package/src/generated/prisma/internal/class.ts +262 -0
- package/src/generated/prisma/internal/prismaNamespace.ts +1333 -0
- package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +168 -0
- package/src/generated/prisma/models/Category.ts +1407 -0
- package/src/generated/prisma/models/Label.ts +1271 -0
- package/src/generated/prisma/models/NestedResource.ts +1557 -0
- package/src/generated/prisma/models/NextLevelNestedResource.ts +1332 -0
- package/src/generated/prisma/models/Resource.ts +2131 -0
- package/src/generated/prisma/models/SubCategory.ts +1407 -0
- package/src/generated/prisma/models/Tag.ts +1271 -0
- package/src/generated/prisma/models/User.ts +1498 -0
- package/src/generated/prisma/models.ts +19 -0
- package/src/generated/prisma-v6/client.d.ts +1 -0
- package/src/generated/prisma-v6/default.d.ts +1 -0
- package/src/generated/prisma-v6/edge.d.ts +1 -0
- package/src/generated/prisma-v6/index.d.ts +13639 -0
- package/src/generated/prisma-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-v6/wasm.d.ts +1 -0
- package/src/index.test.ts +702 -2
- package/src/index.ts +79 -1
- package/src/test-setup.v6.ts +5 -0
- package/src/test-setup.v7.ts +7 -0
- package/lib/index.test.d.ts +0 -2
- package/lib/index.test.d.ts.map +0 -1
- package/lib/index.test.js +0 -4093
- 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", () => {
|