@cerbos/orm-prisma 3.2.1 → 3.3.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.
- package/README.md +11 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +400 -103
- package/lib/index.js.map +1 -1
- package/package.json +13 -6
- package/src/generated/prisma/client.ts +3 -1
- package/src/generated/prisma/internal/class.ts +17 -5
- package/src/generated/prisma/internal/prismaNamespace.ts +4 -4
- package/src/generated/prisma/models/Category.ts +5 -0
- package/src/generated/prisma/models/Label.ts +5 -0
- package/src/generated/prisma/models/NestedResource.ts +5 -0
- package/src/generated/prisma/models/NextLevelNestedResource.ts +5 -0
- package/src/generated/prisma/models/Resource.ts +5 -0
- package/src/generated/prisma/models/SubCategory.ts +5 -0
- package/src/generated/prisma/models/Tag.ts +5 -0
- package/src/generated/prisma/models/User.ts +5 -0
- package/src/index.test.ts +807 -0
- package/src/index.ts +586 -123
package/src/index.test.ts
CHANGED
|
@@ -5513,3 +5513,810 @@ describe("Return Types", () => {
|
|
|
5513
5513
|
expect(conditional.kind).toBe(PlanKind.CONDITIONAL);
|
|
5514
5514
|
});
|
|
5515
5515
|
});
|
|
5516
|
+
|
|
5517
|
+
// Issue #156: add operator (string concatenation)
|
|
5518
|
+
describe("Add Operator", () => {
|
|
5519
|
+
test("add with two values (constant folding)", () => {
|
|
5520
|
+
// #given eq(field, add("prefix:", "suffix"))
|
|
5521
|
+
const plan = createConditionalPlan({
|
|
5522
|
+
operator: "eq",
|
|
5523
|
+
operands: [
|
|
5524
|
+
{ name: "request.resource.attr.aString" },
|
|
5525
|
+
{
|
|
5526
|
+
operator: "add",
|
|
5527
|
+
operands: [{ value: "prefix:" }, { value: "suffix" }],
|
|
5528
|
+
},
|
|
5529
|
+
],
|
|
5530
|
+
});
|
|
5531
|
+
|
|
5532
|
+
// #when
|
|
5533
|
+
const result = queryPlanToPrisma({
|
|
5534
|
+
queryPlan: plan,
|
|
5535
|
+
mapper: {
|
|
5536
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
5537
|
+
},
|
|
5538
|
+
});
|
|
5539
|
+
|
|
5540
|
+
// #then constant fold: "prefix:" + "suffix" = "prefix:suffix"
|
|
5541
|
+
expect(result).toEqual({
|
|
5542
|
+
kind: PlanKind.CONDITIONAL,
|
|
5543
|
+
filters: { aString: { equals: "prefix:suffix" } },
|
|
5544
|
+
});
|
|
5545
|
+
});
|
|
5546
|
+
|
|
5547
|
+
test("add with value prefix and field reference in eq", () => {
|
|
5548
|
+
// #given eq("projects:123", add("projects:", R.attr.id))
|
|
5549
|
+
// This is the exact plan Cerbos produces for:
|
|
5550
|
+
// P.attr.context == "projects:" + request.resource.attr.id
|
|
5551
|
+
const plan = createConditionalPlan({
|
|
5552
|
+
operator: "eq",
|
|
5553
|
+
operands: [
|
|
5554
|
+
{ value: "projects:123" },
|
|
5555
|
+
{
|
|
5556
|
+
operator: "add",
|
|
5557
|
+
operands: [
|
|
5558
|
+
{ value: "projects:" },
|
|
5559
|
+
{ name: "request.resource.attr.id" },
|
|
5560
|
+
],
|
|
5561
|
+
},
|
|
5562
|
+
],
|
|
5563
|
+
});
|
|
5564
|
+
|
|
5565
|
+
// #when
|
|
5566
|
+
const result = queryPlanToPrisma({
|
|
5567
|
+
queryPlan: plan,
|
|
5568
|
+
mapper: {
|
|
5569
|
+
"request.resource.attr.id": { field: "id" },
|
|
5570
|
+
},
|
|
5571
|
+
});
|
|
5572
|
+
|
|
5573
|
+
// #then solve: "projects:123" == "projects:" + id → id == "123"
|
|
5574
|
+
expect(result).toEqual({
|
|
5575
|
+
kind: PlanKind.CONDITIONAL,
|
|
5576
|
+
filters: { id: { equals: "123" } },
|
|
5577
|
+
});
|
|
5578
|
+
});
|
|
5579
|
+
|
|
5580
|
+
test("add with field reference and value suffix in eq", () => {
|
|
5581
|
+
// #given eq("hello_world", add(R.attr.aString, "_world"))
|
|
5582
|
+
const plan = createConditionalPlan({
|
|
5583
|
+
operator: "eq",
|
|
5584
|
+
operands: [
|
|
5585
|
+
{ value: "hello_world" },
|
|
5586
|
+
{
|
|
5587
|
+
operator: "add",
|
|
5588
|
+
operands: [
|
|
5589
|
+
{ name: "request.resource.attr.aString" },
|
|
5590
|
+
{ value: "_world" },
|
|
5591
|
+
],
|
|
5592
|
+
},
|
|
5593
|
+
],
|
|
5594
|
+
});
|
|
5595
|
+
|
|
5596
|
+
// #when
|
|
5597
|
+
const result = queryPlanToPrisma({
|
|
5598
|
+
queryPlan: plan,
|
|
5599
|
+
mapper: {
|
|
5600
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
5601
|
+
},
|
|
5602
|
+
});
|
|
5603
|
+
|
|
5604
|
+
// #then solve: "hello_world" == aString + "_world" → aString == "hello"
|
|
5605
|
+
expect(result).toEqual({
|
|
5606
|
+
kind: PlanKind.CONDITIONAL,
|
|
5607
|
+
filters: { aString: { equals: "hello" } },
|
|
5608
|
+
});
|
|
5609
|
+
});
|
|
5610
|
+
|
|
5611
|
+
test("add with value prefix and field reference in ne", () => {
|
|
5612
|
+
// #given ne("projects:123", add("projects:", R.attr.id))
|
|
5613
|
+
const plan = createConditionalPlan({
|
|
5614
|
+
operator: "ne",
|
|
5615
|
+
operands: [
|
|
5616
|
+
{ value: "projects:123" },
|
|
5617
|
+
{
|
|
5618
|
+
operator: "add",
|
|
5619
|
+
operands: [
|
|
5620
|
+
{ value: "projects:" },
|
|
5621
|
+
{ name: "request.resource.attr.id" },
|
|
5622
|
+
],
|
|
5623
|
+
},
|
|
5624
|
+
],
|
|
5625
|
+
});
|
|
5626
|
+
|
|
5627
|
+
// #when
|
|
5628
|
+
const result = queryPlanToPrisma({
|
|
5629
|
+
queryPlan: plan,
|
|
5630
|
+
mapper: {
|
|
5631
|
+
"request.resource.attr.id": { field: "id" },
|
|
5632
|
+
},
|
|
5633
|
+
});
|
|
5634
|
+
|
|
5635
|
+
// #then solve: "projects:123" != "projects:" + id → id != "123"
|
|
5636
|
+
expect(result).toEqual({
|
|
5637
|
+
kind: PlanKind.CONDITIONAL,
|
|
5638
|
+
filters: { id: { not: "123" } },
|
|
5639
|
+
});
|
|
5640
|
+
});
|
|
5641
|
+
|
|
5642
|
+
test("add with prefix mismatch yields impossible filter", () => {
|
|
5643
|
+
// #given eq("other:123", add("projects:", R.attr.id))
|
|
5644
|
+
// "other:123" doesn't start with "projects:", so no value of id can match
|
|
5645
|
+
const plan = createConditionalPlan({
|
|
5646
|
+
operator: "eq",
|
|
5647
|
+
operands: [
|
|
5648
|
+
{ value: "other:123" },
|
|
5649
|
+
{
|
|
5650
|
+
operator: "add",
|
|
5651
|
+
operands: [
|
|
5652
|
+
{ value: "projects:" },
|
|
5653
|
+
{ name: "request.resource.attr.id" },
|
|
5654
|
+
],
|
|
5655
|
+
},
|
|
5656
|
+
],
|
|
5657
|
+
});
|
|
5658
|
+
|
|
5659
|
+
// #when
|
|
5660
|
+
const result = queryPlanToPrisma({
|
|
5661
|
+
queryPlan: plan,
|
|
5662
|
+
mapper: {
|
|
5663
|
+
"request.resource.attr.id": { field: "id" },
|
|
5664
|
+
},
|
|
5665
|
+
});
|
|
5666
|
+
|
|
5667
|
+
// #then impossible: produces a filter that matches nothing
|
|
5668
|
+
// Use an impossible equality that Prisma won't match
|
|
5669
|
+
expect(result).toEqual({
|
|
5670
|
+
kind: PlanKind.CONDITIONAL,
|
|
5671
|
+
filters: {
|
|
5672
|
+
id: { in: [] },
|
|
5673
|
+
},
|
|
5674
|
+
});
|
|
5675
|
+
});
|
|
5676
|
+
|
|
5677
|
+
test("add with relation mapping", () => {
|
|
5678
|
+
// #given eq("projects:123", add("projects:", R.attr.nested.id))
|
|
5679
|
+
const plan = createConditionalPlan({
|
|
5680
|
+
operator: "eq",
|
|
5681
|
+
operands: [
|
|
5682
|
+
{ value: "projects:123" },
|
|
5683
|
+
{
|
|
5684
|
+
operator: "add",
|
|
5685
|
+
operands: [
|
|
5686
|
+
{ value: "projects:" },
|
|
5687
|
+
{ name: "request.resource.attr.nested.id" },
|
|
5688
|
+
],
|
|
5689
|
+
},
|
|
5690
|
+
],
|
|
5691
|
+
});
|
|
5692
|
+
|
|
5693
|
+
// #when
|
|
5694
|
+
const result = queryPlanToPrisma({
|
|
5695
|
+
queryPlan: plan,
|
|
5696
|
+
mapper: {
|
|
5697
|
+
"request.resource.attr.nested": {
|
|
5698
|
+
relation: {
|
|
5699
|
+
name: "nested",
|
|
5700
|
+
type: "one",
|
|
5701
|
+
field: "id",
|
|
5702
|
+
},
|
|
5703
|
+
},
|
|
5704
|
+
},
|
|
5705
|
+
});
|
|
5706
|
+
|
|
5707
|
+
// #then
|
|
5708
|
+
expect(result).toEqual({
|
|
5709
|
+
kind: PlanKind.CONDITIONAL,
|
|
5710
|
+
filters: { nested: { is: { id: { equals: "123" } } } },
|
|
5711
|
+
});
|
|
5712
|
+
});
|
|
5713
|
+
|
|
5714
|
+
test("add via cerbos integration", async () => {
|
|
5715
|
+
const queryPlan = await cerbos.planResources({
|
|
5716
|
+
principal: {
|
|
5717
|
+
id: "user1",
|
|
5718
|
+
roles: ["USER"],
|
|
5719
|
+
attr: { context: "projects:123" },
|
|
5720
|
+
},
|
|
5721
|
+
resource: { kind: "resource" },
|
|
5722
|
+
action: "string-concat-principal",
|
|
5723
|
+
});
|
|
5724
|
+
|
|
5725
|
+
expect(queryPlan.kind).toBe(PlanKind.CONDITIONAL);
|
|
5726
|
+
|
|
5727
|
+
const result = queryPlanToPrisma({
|
|
5728
|
+
queryPlan,
|
|
5729
|
+
mapper: {
|
|
5730
|
+
"request.resource.attr.id": { field: "id" },
|
|
5731
|
+
},
|
|
5732
|
+
});
|
|
5733
|
+
|
|
5734
|
+
expect(result).toEqual({
|
|
5735
|
+
kind: PlanKind.CONDITIONAL,
|
|
5736
|
+
filters: { id: { equals: "123" } },
|
|
5737
|
+
});
|
|
5738
|
+
});
|
|
5739
|
+
|
|
5740
|
+
test("add with numeric values", () => {
|
|
5741
|
+
// #given add with numbers: eq(10, add(3, R.attr.aNumber))
|
|
5742
|
+
const plan = createConditionalPlan({
|
|
5743
|
+
operator: "eq",
|
|
5744
|
+
operands: [
|
|
5745
|
+
{ value: 10 },
|
|
5746
|
+
{
|
|
5747
|
+
operator: "add",
|
|
5748
|
+
operands: [
|
|
5749
|
+
{ value: 3 },
|
|
5750
|
+
{ name: "request.resource.attr.aNumber" },
|
|
5751
|
+
],
|
|
5752
|
+
},
|
|
5753
|
+
],
|
|
5754
|
+
});
|
|
5755
|
+
|
|
5756
|
+
// #when
|
|
5757
|
+
const result = queryPlanToPrisma({
|
|
5758
|
+
queryPlan: plan,
|
|
5759
|
+
mapper: {
|
|
5760
|
+
"request.resource.attr.aNumber": { field: "aNumber" },
|
|
5761
|
+
},
|
|
5762
|
+
});
|
|
5763
|
+
|
|
5764
|
+
// #then solve: 10 == 3 + aNumber → aNumber == 7
|
|
5765
|
+
expect(result).toEqual({
|
|
5766
|
+
kind: PlanKind.CONDITIONAL,
|
|
5767
|
+
filters: { aNumber: { equals: 7 } },
|
|
5768
|
+
});
|
|
5769
|
+
});
|
|
5770
|
+
});
|
|
5771
|
+
|
|
5772
|
+
// Issue #155: hierarchy overlaps operator
|
|
5773
|
+
// overlaps = one hierarchy is a prefix of the other (segment-wise comparison)
|
|
5774
|
+
describe("Hierarchy Overlaps", () => {
|
|
5775
|
+
test("overlaps - equal length hierarchies, field must match", () => {
|
|
5776
|
+
// #given hierarchy("projects:123", ":").overlaps(hierarchy(["projects", R.id]))
|
|
5777
|
+
// Left segments: ["projects", "123"], Right segments: ["projects", R.id]
|
|
5778
|
+
// Same length: prefix check both directions requires all segments match
|
|
5779
|
+
// segments[0]: "projects" == "projects" ✓
|
|
5780
|
+
// segments[1]: "123" == R.id → R.id == "123"
|
|
5781
|
+
const plan = createConditionalPlan({
|
|
5782
|
+
operator: "overlaps",
|
|
5783
|
+
operands: [
|
|
5784
|
+
{
|
|
5785
|
+
operator: "hierarchy",
|
|
5786
|
+
operands: [{ value: "projects:123" }, { value: ":" }],
|
|
5787
|
+
},
|
|
5788
|
+
{
|
|
5789
|
+
operator: "hierarchy",
|
|
5790
|
+
operands: [
|
|
5791
|
+
{
|
|
5792
|
+
operator: "list",
|
|
5793
|
+
operands: [
|
|
5794
|
+
{ value: "projects" },
|
|
5795
|
+
{ name: "request.resource.id" },
|
|
5796
|
+
],
|
|
5797
|
+
},
|
|
5798
|
+
],
|
|
5799
|
+
},
|
|
5800
|
+
],
|
|
5801
|
+
});
|
|
5802
|
+
|
|
5803
|
+
// #when
|
|
5804
|
+
const result = queryPlanToPrisma({
|
|
5805
|
+
queryPlan: plan,
|
|
5806
|
+
mapper: {
|
|
5807
|
+
"request.resource.id": { field: "id" },
|
|
5808
|
+
},
|
|
5809
|
+
});
|
|
5810
|
+
|
|
5811
|
+
// #then
|
|
5812
|
+
expect(result).toEqual({
|
|
5813
|
+
kind: PlanKind.CONDITIONAL,
|
|
5814
|
+
filters: { id: { equals: "123" } },
|
|
5815
|
+
});
|
|
5816
|
+
});
|
|
5817
|
+
|
|
5818
|
+
test("overlaps - left shorter than right, left is always prefix", () => {
|
|
5819
|
+
// #given hierarchy("admin", ":").overlaps(hierarchy(["admin", R.attr.scope]))
|
|
5820
|
+
// Left segments: ["admin"] (length 1), Right segments: ["admin", R.attr.scope] (length 2)
|
|
5821
|
+
// Left prefix of right: "admin" == "admin" ✓ → always true
|
|
5822
|
+
const plan = createConditionalPlan({
|
|
5823
|
+
operator: "overlaps",
|
|
5824
|
+
operands: [
|
|
5825
|
+
{
|
|
5826
|
+
operator: "hierarchy",
|
|
5827
|
+
operands: [{ value: "admin" }, { value: ":" }],
|
|
5828
|
+
},
|
|
5829
|
+
{
|
|
5830
|
+
operator: "hierarchy",
|
|
5831
|
+
operands: [
|
|
5832
|
+
{
|
|
5833
|
+
operator: "list",
|
|
5834
|
+
operands: [
|
|
5835
|
+
{ value: "admin" },
|
|
5836
|
+
{ name: "request.resource.attr.scope" },
|
|
5837
|
+
],
|
|
5838
|
+
},
|
|
5839
|
+
],
|
|
5840
|
+
},
|
|
5841
|
+
],
|
|
5842
|
+
});
|
|
5843
|
+
|
|
5844
|
+
// #when
|
|
5845
|
+
const result = queryPlanToPrisma({
|
|
5846
|
+
queryPlan: plan,
|
|
5847
|
+
mapper: {
|
|
5848
|
+
"request.resource.attr.scope": { field: "scope" },
|
|
5849
|
+
},
|
|
5850
|
+
});
|
|
5851
|
+
|
|
5852
|
+
// #then always true
|
|
5853
|
+
expect(result).toEqual({
|
|
5854
|
+
kind: PlanKind.CONDITIONAL,
|
|
5855
|
+
filters: {},
|
|
5856
|
+
});
|
|
5857
|
+
});
|
|
5858
|
+
|
|
5859
|
+
test("overlaps - left longer, right is prefix when field matches", () => {
|
|
5860
|
+
// #given hierarchy("dept:engineering:frontend", ":").overlaps(hierarchy(["dept", R.attr.department]))
|
|
5861
|
+
// Left segments: ["dept", "engineering", "frontend"] (3)
|
|
5862
|
+
// Right segments: ["dept", R.attr.department] (2)
|
|
5863
|
+
// Left prefix of right: len 3 > len 2 → impossible
|
|
5864
|
+
// Right prefix of left: "dept" == "dept" ✓, R.attr.department == "engineering"
|
|
5865
|
+
const plan = createConditionalPlan({
|
|
5866
|
+
operator: "overlaps",
|
|
5867
|
+
operands: [
|
|
5868
|
+
{
|
|
5869
|
+
operator: "hierarchy",
|
|
5870
|
+
operands: [
|
|
5871
|
+
{ value: "dept:engineering:frontend" },
|
|
5872
|
+
{ value: ":" },
|
|
5873
|
+
],
|
|
5874
|
+
},
|
|
5875
|
+
{
|
|
5876
|
+
operator: "hierarchy",
|
|
5877
|
+
operands: [
|
|
5878
|
+
{
|
|
5879
|
+
operator: "list",
|
|
5880
|
+
operands: [
|
|
5881
|
+
{ value: "dept" },
|
|
5882
|
+
{ name: "request.resource.attr.department" },
|
|
5883
|
+
],
|
|
5884
|
+
},
|
|
5885
|
+
],
|
|
5886
|
+
},
|
|
5887
|
+
],
|
|
5888
|
+
});
|
|
5889
|
+
|
|
5890
|
+
// #when
|
|
5891
|
+
const result = queryPlanToPrisma({
|
|
5892
|
+
queryPlan: plan,
|
|
5893
|
+
mapper: {
|
|
5894
|
+
"request.resource.attr.department": { field: "department" },
|
|
5895
|
+
},
|
|
5896
|
+
});
|
|
5897
|
+
|
|
5898
|
+
// #then
|
|
5899
|
+
expect(result).toEqual({
|
|
5900
|
+
kind: PlanKind.CONDITIONAL,
|
|
5901
|
+
filters: { department: { equals: "engineering" } },
|
|
5902
|
+
});
|
|
5903
|
+
});
|
|
5904
|
+
|
|
5905
|
+
test("overlaps - different roots, impossible", () => {
|
|
5906
|
+
// #given hierarchy("org:acme:team1", ":").overlaps(hierarchy(["company", R.attr.team]))
|
|
5907
|
+
// Left: ["org", "acme", "team1"], Right: ["company", R.attr.team]
|
|
5908
|
+
// Both directions fail at segment[0]: "org" ≠ "company"
|
|
5909
|
+
const plan = createConditionalPlan({
|
|
5910
|
+
operator: "overlaps",
|
|
5911
|
+
operands: [
|
|
5912
|
+
{
|
|
5913
|
+
operator: "hierarchy",
|
|
5914
|
+
operands: [{ value: "org:acme:team1" }, { value: ":" }],
|
|
5915
|
+
},
|
|
5916
|
+
{
|
|
5917
|
+
operator: "hierarchy",
|
|
5918
|
+
operands: [
|
|
5919
|
+
{
|
|
5920
|
+
operator: "list",
|
|
5921
|
+
operands: [
|
|
5922
|
+
{ value: "company" },
|
|
5923
|
+
{ name: "request.resource.attr.team" },
|
|
5924
|
+
],
|
|
5925
|
+
},
|
|
5926
|
+
],
|
|
5927
|
+
},
|
|
5928
|
+
],
|
|
5929
|
+
});
|
|
5930
|
+
|
|
5931
|
+
// #when
|
|
5932
|
+
const result = queryPlanToPrisma({
|
|
5933
|
+
queryPlan: plan,
|
|
5934
|
+
mapper: {
|
|
5935
|
+
"request.resource.attr.team": { field: "team" },
|
|
5936
|
+
},
|
|
5937
|
+
});
|
|
5938
|
+
|
|
5939
|
+
// #then impossible
|
|
5940
|
+
expect(result).toEqual({
|
|
5941
|
+
kind: PlanKind.CONDITIONAL,
|
|
5942
|
+
filters: {
|
|
5943
|
+
team: { in: [] },
|
|
5944
|
+
},
|
|
5945
|
+
});
|
|
5946
|
+
});
|
|
5947
|
+
|
|
5948
|
+
test("overlaps - equal length with field in middle, field must match", () => {
|
|
5949
|
+
// #given hierarchy("region:us:west", ":").overlaps(hierarchy(["region", "us", R.attr.zone]))
|
|
5950
|
+
// Left: ["region", "us", "west"], Right: ["region", "us", R.attr.zone]
|
|
5951
|
+
// Same length, prefix check: "region"=="region" ✓, "us"=="us" ✓, "west"==R.attr.zone
|
|
5952
|
+
const plan = createConditionalPlan({
|
|
5953
|
+
operator: "overlaps",
|
|
5954
|
+
operands: [
|
|
5955
|
+
{
|
|
5956
|
+
operator: "hierarchy",
|
|
5957
|
+
operands: [{ value: "region:us:west" }, { value: ":" }],
|
|
5958
|
+
},
|
|
5959
|
+
{
|
|
5960
|
+
operator: "hierarchy",
|
|
5961
|
+
operands: [
|
|
5962
|
+
{
|
|
5963
|
+
operator: "list",
|
|
5964
|
+
operands: [
|
|
5965
|
+
{ value: "region" },
|
|
5966
|
+
{ value: "us" },
|
|
5967
|
+
{ name: "request.resource.attr.zone" },
|
|
5968
|
+
],
|
|
5969
|
+
},
|
|
5970
|
+
],
|
|
5971
|
+
},
|
|
5972
|
+
],
|
|
5973
|
+
});
|
|
5974
|
+
|
|
5975
|
+
// #when
|
|
5976
|
+
const result = queryPlanToPrisma({
|
|
5977
|
+
queryPlan: plan,
|
|
5978
|
+
mapper: {
|
|
5979
|
+
"request.resource.attr.zone": { field: "zone" },
|
|
5980
|
+
},
|
|
5981
|
+
});
|
|
5982
|
+
|
|
5983
|
+
// #then
|
|
5984
|
+
expect(result).toEqual({
|
|
5985
|
+
kind: PlanKind.CONDITIONAL,
|
|
5986
|
+
filters: { zone: { equals: "west" } },
|
|
5987
|
+
});
|
|
5988
|
+
});
|
|
5989
|
+
|
|
5990
|
+
test("overlaps - single-field right hierarchy as prefix of left", () => {
|
|
5991
|
+
// #given hierarchy("team:alpha", ":").overlaps(hierarchy([R.attr.group]))
|
|
5992
|
+
// Left: ["team", "alpha"] (2), Right: [R.attr.group] (1)
|
|
5993
|
+
// Left prefix of right: len 2 > len 1 → impossible
|
|
5994
|
+
// Right prefix of left: R.attr.group == "team" (first segment)
|
|
5995
|
+
const plan = createConditionalPlan({
|
|
5996
|
+
operator: "overlaps",
|
|
5997
|
+
operands: [
|
|
5998
|
+
{
|
|
5999
|
+
operator: "hierarchy",
|
|
6000
|
+
operands: [{ value: "team:alpha" }, { value: ":" }],
|
|
6001
|
+
},
|
|
6002
|
+
{
|
|
6003
|
+
operator: "hierarchy",
|
|
6004
|
+
operands: [
|
|
6005
|
+
{
|
|
6006
|
+
operator: "list",
|
|
6007
|
+
operands: [{ name: "request.resource.attr.group" }],
|
|
6008
|
+
},
|
|
6009
|
+
],
|
|
6010
|
+
},
|
|
6011
|
+
],
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
// #when
|
|
6015
|
+
const result = queryPlanToPrisma({
|
|
6016
|
+
queryPlan: plan,
|
|
6017
|
+
mapper: {
|
|
6018
|
+
"request.resource.attr.group": { field: "group" },
|
|
6019
|
+
},
|
|
6020
|
+
});
|
|
6021
|
+
|
|
6022
|
+
// #then
|
|
6023
|
+
expect(result).toEqual({
|
|
6024
|
+
kind: PlanKind.CONDITIONAL,
|
|
6025
|
+
filters: { group: { equals: "team" } },
|
|
6026
|
+
});
|
|
6027
|
+
});
|
|
6028
|
+
|
|
6029
|
+
test("overlaps - hierarchy(dotted string) without delimiter", () => {
|
|
6030
|
+
// #given hierarchy("a.b.c").overlaps(hierarchy(["a", "b", R.attr.zone]))
|
|
6031
|
+
// hierarchy("a.b.c") uses default "." delimiter → ["a", "b", "c"]
|
|
6032
|
+
const plan = createConditionalPlan({
|
|
6033
|
+
operator: "overlaps",
|
|
6034
|
+
operands: [
|
|
6035
|
+
{
|
|
6036
|
+
operator: "hierarchy",
|
|
6037
|
+
operands: [{ value: "a.b.c" }],
|
|
6038
|
+
},
|
|
6039
|
+
{
|
|
6040
|
+
operator: "hierarchy",
|
|
6041
|
+
operands: [
|
|
6042
|
+
{
|
|
6043
|
+
operator: "list",
|
|
6044
|
+
operands: [
|
|
6045
|
+
{ value: "a" },
|
|
6046
|
+
{ value: "b" },
|
|
6047
|
+
{ name: "request.resource.attr.zone" },
|
|
6048
|
+
],
|
|
6049
|
+
},
|
|
6050
|
+
],
|
|
6051
|
+
},
|
|
6052
|
+
],
|
|
6053
|
+
});
|
|
6054
|
+
|
|
6055
|
+
// #when
|
|
6056
|
+
const result = queryPlanToPrisma({
|
|
6057
|
+
queryPlan: plan,
|
|
6058
|
+
mapper: { "request.resource.attr.zone": { field: "zone" } },
|
|
6059
|
+
});
|
|
6060
|
+
|
|
6061
|
+
// #then
|
|
6062
|
+
expect(result).toEqual({
|
|
6063
|
+
kind: PlanKind.CONDITIONAL,
|
|
6064
|
+
filters: { zone: { equals: "c" } },
|
|
6065
|
+
});
|
|
6066
|
+
});
|
|
6067
|
+
|
|
6068
|
+
test("overlaps via cerbos integration", async () => {
|
|
6069
|
+
const queryPlan = await cerbos.planResources({
|
|
6070
|
+
principal: {
|
|
6071
|
+
id: "user1",
|
|
6072
|
+
roles: ["USER"],
|
|
6073
|
+
attr: { context: "projects:123" },
|
|
6074
|
+
},
|
|
6075
|
+
resource: { kind: "resource" },
|
|
6076
|
+
action: "hierarchy-overlaps",
|
|
6077
|
+
});
|
|
6078
|
+
|
|
6079
|
+
expect(queryPlan.kind).toBe(PlanKind.CONDITIONAL);
|
|
6080
|
+
|
|
6081
|
+
const result = queryPlanToPrisma({
|
|
6082
|
+
queryPlan,
|
|
6083
|
+
mapper: {
|
|
6084
|
+
"request.resource.id": { field: "id" },
|
|
6085
|
+
},
|
|
6086
|
+
});
|
|
6087
|
+
|
|
6088
|
+
// hierarchy("projects:123", ":") segments = ["projects", "123"]
|
|
6089
|
+
// hierarchy(["projects", R.id]) segments = ["projects", R.id]
|
|
6090
|
+
// Same length, segments[1]: "123" == R.id → R.id == "123"
|
|
6091
|
+
expect(result).toEqual({
|
|
6092
|
+
kind: PlanKind.CONDITIONAL,
|
|
6093
|
+
filters: { id: { equals: "123" } },
|
|
6094
|
+
});
|
|
6095
|
+
});
|
|
6096
|
+
});
|
|
6097
|
+
|
|
6098
|
+
const hierarchyMapper = {
|
|
6099
|
+
"request.resource.attr.scope": { field: "scope" },
|
|
6100
|
+
"request.resource.attr.path": { field: "path" },
|
|
6101
|
+
};
|
|
6102
|
+
|
|
6103
|
+
// ancestorOf = first hierarchy is a strict prefix of second
|
|
6104
|
+
// hierarchy("a.b").ancestorOf(hierarchy("a.b.c.d")) == true
|
|
6105
|
+
describe("Hierarchy ancestorOf", () => {
|
|
6106
|
+
test("ancestorOf - constant is ancestor, field is descendant", () => {
|
|
6107
|
+
// #given hierarchy("a.b.c").ancestorOf(hierarchy(R.attr.scope))
|
|
6108
|
+
const plan = createConditionalPlan({
|
|
6109
|
+
operator: "ancestorOf",
|
|
6110
|
+
operands: [
|
|
6111
|
+
{
|
|
6112
|
+
operator: "hierarchy",
|
|
6113
|
+
operands: [{ value: "a.b.c" }],
|
|
6114
|
+
},
|
|
6115
|
+
{
|
|
6116
|
+
operator: "hierarchy",
|
|
6117
|
+
operands: [{ name: "request.resource.attr.scope" }],
|
|
6118
|
+
},
|
|
6119
|
+
],
|
|
6120
|
+
});
|
|
6121
|
+
|
|
6122
|
+
// #when
|
|
6123
|
+
const result = queryPlanToPrisma({ queryPlan: plan, mapper: hierarchyMapper });
|
|
6124
|
+
|
|
6125
|
+
// #then - R.attr.scope must start with "a.b.c." (ancestor means strict prefix)
|
|
6126
|
+
expect(result).toEqual({
|
|
6127
|
+
kind: PlanKind.CONDITIONAL,
|
|
6128
|
+
filters: { scope: { startsWith: "a.b.c." } },
|
|
6129
|
+
});
|
|
6130
|
+
});
|
|
6131
|
+
|
|
6132
|
+
test("ancestorOf - field is ancestor of constant", () => {
|
|
6133
|
+
// #given hierarchy(R.attr.scope).ancestorOf(hierarchy("a.b.c.d"))
|
|
6134
|
+
// R.attr.scope must be a prefix of "a.b.c.d"
|
|
6135
|
+
// Possible prefixes: "a", "a.b", "a.b.c"
|
|
6136
|
+
const plan = createConditionalPlan({
|
|
6137
|
+
operator: "ancestorOf",
|
|
6138
|
+
operands: [
|
|
6139
|
+
{
|
|
6140
|
+
operator: "hierarchy",
|
|
6141
|
+
operands: [{ name: "request.resource.attr.scope" }],
|
|
6142
|
+
},
|
|
6143
|
+
{
|
|
6144
|
+
operator: "hierarchy",
|
|
6145
|
+
operands: [{ value: "a.b.c.d" }],
|
|
6146
|
+
},
|
|
6147
|
+
],
|
|
6148
|
+
});
|
|
6149
|
+
|
|
6150
|
+
// #when
|
|
6151
|
+
const result = queryPlanToPrisma({ queryPlan: plan, mapper: hierarchyMapper });
|
|
6152
|
+
|
|
6153
|
+
// #then - scope must be one of the strict prefixes of "a.b.c.d"
|
|
6154
|
+
expect(result).toEqual({
|
|
6155
|
+
kind: PlanKind.CONDITIONAL,
|
|
6156
|
+
filters: { scope: { in: ["a", "a.b", "a.b.c"] } },
|
|
6157
|
+
});
|
|
6158
|
+
});
|
|
6159
|
+
|
|
6160
|
+
test("ancestorOf - single segment constant ancestor", () => {
|
|
6161
|
+
// #given hierarchy("root").ancestorOf(hierarchy(R.attr.scope))
|
|
6162
|
+
const plan = createConditionalPlan({
|
|
6163
|
+
operator: "ancestorOf",
|
|
6164
|
+
operands: [
|
|
6165
|
+
{
|
|
6166
|
+
operator: "hierarchy",
|
|
6167
|
+
operands: [{ value: "root" }],
|
|
6168
|
+
},
|
|
6169
|
+
{
|
|
6170
|
+
operator: "hierarchy",
|
|
6171
|
+
operands: [{ name: "request.resource.attr.scope" }],
|
|
6172
|
+
},
|
|
6173
|
+
],
|
|
6174
|
+
});
|
|
6175
|
+
|
|
6176
|
+
// #when
|
|
6177
|
+
const result = queryPlanToPrisma({ queryPlan: plan, mapper: hierarchyMapper });
|
|
6178
|
+
|
|
6179
|
+
// #then
|
|
6180
|
+
expect(result).toEqual({
|
|
6181
|
+
kind: PlanKind.CONDITIONAL,
|
|
6182
|
+
filters: { scope: { startsWith: "root." } },
|
|
6183
|
+
});
|
|
6184
|
+
});
|
|
6185
|
+
|
|
6186
|
+
test("ancestorOf - with custom delimiter", () => {
|
|
6187
|
+
// #given hierarchy(P.attr.scope, ":").ancestorOf(hierarchy(R.attr.path, ":"))
|
|
6188
|
+
// P.attr.scope is resolved at plan time → constant "org:team"
|
|
6189
|
+
const plan = createConditionalPlan({
|
|
6190
|
+
operator: "ancestorOf",
|
|
6191
|
+
operands: [
|
|
6192
|
+
{
|
|
6193
|
+
operator: "hierarchy",
|
|
6194
|
+
operands: [{ value: "org:team" }, { value: ":" }],
|
|
6195
|
+
},
|
|
6196
|
+
{
|
|
6197
|
+
operator: "hierarchy",
|
|
6198
|
+
operands: [{ name: "request.resource.attr.path" }, { value: ":" }],
|
|
6199
|
+
},
|
|
6200
|
+
],
|
|
6201
|
+
});
|
|
6202
|
+
|
|
6203
|
+
// #when
|
|
6204
|
+
const result = queryPlanToPrisma({ queryPlan: plan, mapper: hierarchyMapper });
|
|
6205
|
+
|
|
6206
|
+
// #then - R.attr.path must start with "org:team:" (using the ":" delimiter)
|
|
6207
|
+
expect(result).toEqual({
|
|
6208
|
+
kind: PlanKind.CONDITIONAL,
|
|
6209
|
+
filters: { path: { startsWith: "org:team:" } },
|
|
6210
|
+
});
|
|
6211
|
+
});
|
|
6212
|
+
|
|
6213
|
+
test("ancestorOf via cerbos integration", async () => {
|
|
6214
|
+
const plan = await cerbos.planResources({
|
|
6215
|
+
principal: {
|
|
6216
|
+
id: "user1",
|
|
6217
|
+
roles: ["USER"],
|
|
6218
|
+
attr: { scope: "a.b.c" },
|
|
6219
|
+
},
|
|
6220
|
+
resource: { kind: "resource" },
|
|
6221
|
+
action: "hierarchy-ancestor-of",
|
|
6222
|
+
});
|
|
6223
|
+
|
|
6224
|
+
const result = queryPlanToPrisma({
|
|
6225
|
+
queryPlan: plan,
|
|
6226
|
+
mapper: {
|
|
6227
|
+
"request.resource.attr.scope": { field: "scope" },
|
|
6228
|
+
},
|
|
6229
|
+
});
|
|
6230
|
+
|
|
6231
|
+
// hierarchy("a.b.c").ancestorOf(hierarchy(R.attr.scope))
|
|
6232
|
+
// "a.b.c" is ancestor of R.attr.scope → R.attr.scope starts with "a.b.c."
|
|
6233
|
+
expect(result).toEqual({
|
|
6234
|
+
kind: PlanKind.CONDITIONAL,
|
|
6235
|
+
filters: { scope: { startsWith: "a.b.c." } },
|
|
6236
|
+
});
|
|
6237
|
+
});
|
|
6238
|
+
});
|
|
6239
|
+
|
|
6240
|
+
// descendentOf = mirror of ancestorOf
|
|
6241
|
+
// hierarchy("a.b.c.d").descendentOf(hierarchy("a.b")) == true
|
|
6242
|
+
describe("Hierarchy descendentOf", () => {
|
|
6243
|
+
test("descendentOf - field is descendant of constant", () => {
|
|
6244
|
+
// #given hierarchy(R.attr.scope).descendentOf(hierarchy("a.b"))
|
|
6245
|
+
// R.attr.scope must be a descendant of "a.b" → "a.b" is a prefix of R.attr.scope
|
|
6246
|
+
const plan = createConditionalPlan({
|
|
6247
|
+
operator: "descendentOf",
|
|
6248
|
+
operands: [
|
|
6249
|
+
{
|
|
6250
|
+
operator: "hierarchy",
|
|
6251
|
+
operands: [{ name: "request.resource.attr.scope" }],
|
|
6252
|
+
},
|
|
6253
|
+
{
|
|
6254
|
+
operator: "hierarchy",
|
|
6255
|
+
operands: [{ value: "a.b" }],
|
|
6256
|
+
},
|
|
6257
|
+
],
|
|
6258
|
+
});
|
|
6259
|
+
|
|
6260
|
+
// #when
|
|
6261
|
+
const result = queryPlanToPrisma({ queryPlan: plan, mapper: hierarchyMapper });
|
|
6262
|
+
|
|
6263
|
+
// #then - scope must start with "a.b." (descendant = strict, must be longer)
|
|
6264
|
+
expect(result).toEqual({
|
|
6265
|
+
kind: PlanKind.CONDITIONAL,
|
|
6266
|
+
filters: { scope: { startsWith: "a.b." } },
|
|
6267
|
+
});
|
|
6268
|
+
});
|
|
6269
|
+
|
|
6270
|
+
test("descendentOf - constant is descendant of field", () => {
|
|
6271
|
+
// #given hierarchy("a.b.c.d").descendentOf(hierarchy(R.attr.scope))
|
|
6272
|
+
// "a.b.c.d" descends from R.attr.scope → R.attr.scope is a prefix of "a.b.c.d"
|
|
6273
|
+
const plan = createConditionalPlan({
|
|
6274
|
+
operator: "descendentOf",
|
|
6275
|
+
operands: [
|
|
6276
|
+
{
|
|
6277
|
+
operator: "hierarchy",
|
|
6278
|
+
operands: [{ value: "a.b.c.d" }],
|
|
6279
|
+
},
|
|
6280
|
+
{
|
|
6281
|
+
operator: "hierarchy",
|
|
6282
|
+
operands: [{ name: "request.resource.attr.scope" }],
|
|
6283
|
+
},
|
|
6284
|
+
],
|
|
6285
|
+
});
|
|
6286
|
+
|
|
6287
|
+
// #when
|
|
6288
|
+
const result = queryPlanToPrisma({ queryPlan: plan, mapper: hierarchyMapper });
|
|
6289
|
+
|
|
6290
|
+
// #then - scope must be one of the strict prefixes of "a.b.c.d"
|
|
6291
|
+
expect(result).toEqual({
|
|
6292
|
+
kind: PlanKind.CONDITIONAL,
|
|
6293
|
+
filters: { scope: { in: ["a", "a.b", "a.b.c"] } },
|
|
6294
|
+
});
|
|
6295
|
+
});
|
|
6296
|
+
|
|
6297
|
+
test("descendentOf via cerbos integration", async () => {
|
|
6298
|
+
const plan = await cerbos.planResources({
|
|
6299
|
+
principal: {
|
|
6300
|
+
id: "user1",
|
|
6301
|
+
roles: ["USER"],
|
|
6302
|
+
attr: { scope: "a.b.c" },
|
|
6303
|
+
},
|
|
6304
|
+
resource: { kind: "resource" },
|
|
6305
|
+
action: "hierarchy-descendent-of",
|
|
6306
|
+
});
|
|
6307
|
+
|
|
6308
|
+
const result = queryPlanToPrisma({
|
|
6309
|
+
queryPlan: plan,
|
|
6310
|
+
mapper: {
|
|
6311
|
+
"request.resource.attr.scope": { field: "scope" },
|
|
6312
|
+
},
|
|
6313
|
+
});
|
|
6314
|
+
|
|
6315
|
+
// hierarchy(R.attr.scope).descendentOf(hierarchy("a.b.c"))
|
|
6316
|
+
// R.attr.scope descends from "a.b.c" → R.attr.scope starts with "a.b.c."
|
|
6317
|
+
expect(result).toEqual({
|
|
6318
|
+
kind: PlanKind.CONDITIONAL,
|
|
6319
|
+
filters: { scope: { startsWith: "a.b.c." } },
|
|
6320
|
+
});
|
|
6321
|
+
});
|
|
6322
|
+
});
|