@reventlessdev/reventless-local 3.0.0-alpha.233 → 3.0.0-alpha.235
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/CHANGELOG.md +23 -0
- package/package.json +10 -10
- package/src/adapter/DomainGraphQL_Server.res +36 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +40 -1
- package/src/adapter/GraphQL_Server.res.mjs +6 -0
- package/src/adapter/PlatformGraphQL_Server.res +5 -0
- package/src/adapter/PlatformGraphQL_Server.res.mjs +4 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +171 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +118 -1
- package/tests/adapter/QueryDbListResolverTest.res +150 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +133 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res +253 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res.mjs +291 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +64 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +55 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +10 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +1 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +5 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +5 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +135 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +202 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +15 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +5 -0
|
@@ -574,3 +574,153 @@ describe("QueryDb by-index resolver", () => {
|
|
|
574
574
|
}
|
|
575
575
|
})
|
|
576
576
|
})
|
|
577
|
+
|
|
578
|
+
// ─────────────────────────────────────────────────────────────
|
|
579
|
+
// Sub-id view — the `{name}Items` door
|
|
580
|
+
// ─────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
// The door only exists when a view declares a sub-id, and no fixture declared one
|
|
583
|
+
// alongside a union field, so it was the last door in the mechanism plan's table
|
|
584
|
+
// carrying a union nowhere.
|
|
585
|
+
|
|
586
|
+
@schema
|
|
587
|
+
type lineState = {
|
|
588
|
+
orderId: @s.matches(Reventless.DcbTag.string) string,
|
|
589
|
+
lineNo: string,
|
|
590
|
+
label: string,
|
|
591
|
+
geolocation?: geolocation,
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let lineStateSchemaWithAnnotations =
|
|
595
|
+
lineStateSchema->S.Metadata.set(
|
|
596
|
+
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
597
|
+
{
|
|
598
|
+
ids: ["orderId"],
|
|
599
|
+
compositeIds: [],
|
|
600
|
+
subIds: ["lineNo"],
|
|
601
|
+
compositeSubIds: [],
|
|
602
|
+
indexes: [],
|
|
603
|
+
hidden: [],
|
|
604
|
+
summary: [],
|
|
605
|
+
drillTargets: [],
|
|
606
|
+
drillTargetKeys: [],
|
|
607
|
+
collapsed: [],
|
|
608
|
+
scan: [],
|
|
609
|
+
scanSort: [],
|
|
610
|
+
semantic: [],
|
|
611
|
+
metric: [],
|
|
612
|
+
lifecycle: None,
|
|
613
|
+
groupBy: None,
|
|
614
|
+
visibility: None,
|
|
615
|
+
live: None,
|
|
616
|
+
retired: None,
|
|
617
|
+
},
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
let buildSubIdFixture = async (~name: string) => {
|
|
621
|
+
module Bus = LocalBus.Make()
|
|
622
|
+
module Storage = LocalQueryDbStorage.Make(Bus)
|
|
623
|
+
module Resolvers = QueryDbResolvers_GraphQL.Make(Bus)
|
|
624
|
+
|
|
625
|
+
module Spec = {
|
|
626
|
+
module Id = Reventless.Id.StringPure
|
|
627
|
+
let name = name
|
|
628
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
629
|
+
@schema
|
|
630
|
+
type state = lineState
|
|
631
|
+
let config = Reventless.ReadModel.config()
|
|
632
|
+
let subIdConfig = Some({
|
|
633
|
+
Reventless.ReadModel.subIdField: "lineNo",
|
|
634
|
+
getSubId: (state: lineState) => state.lineNo,
|
|
635
|
+
})
|
|
636
|
+
let authorization: Reventless.Authorization.permission = AllowAuthenticated
|
|
637
|
+
let visibility: Reventless.Visibility.t = Public
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
module QDbResolversAdapter = ReventlessCore.QueryDb_Adapter.NoResolvers(Storage)
|
|
641
|
+
module Maker = ReventlessCore.QueryDb_Builder.Make(Spec, Storage, QDbResolversAdapter)
|
|
642
|
+
|
|
643
|
+
ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry->Dict.set(
|
|
644
|
+
name,
|
|
645
|
+
{
|
|
646
|
+
singleFieldName: name,
|
|
647
|
+
listFieldName: name ++ "s",
|
|
648
|
+
returnTypeName: name,
|
|
649
|
+
pluralTypeName: "[" ++ name ++ "]",
|
|
650
|
+
includeIdParam: true,
|
|
651
|
+
connectionSpec: true,
|
|
652
|
+
},
|
|
653
|
+
)
|
|
654
|
+
ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.set(
|
|
655
|
+
name,
|
|
656
|
+
lineStateSchemaWithAnnotations->S.castToUnknown,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
let queryDb = Maker.make(~api=(), ~apiRole=())
|
|
660
|
+
let ops = await queryDb->ReventlessCore.Component.operations->TestRunner.resolve
|
|
661
|
+
|
|
662
|
+
let _: ReventlessCore.QueryDb_Adapter.resolvers = Resolvers.make(
|
|
663
|
+
~name,
|
|
664
|
+
~api=(),
|
|
665
|
+
~apiRole=(),
|
|
666
|
+
~dataSourceName=""->Pulumi.Output.make,
|
|
667
|
+
~indexes=[],
|
|
668
|
+
~subIdField=Some("lineNo"),
|
|
669
|
+
~idResolverConfigs=[],
|
|
670
|
+
~idsResolverConfigs=[],
|
|
671
|
+
~authorization=Reventless.Authorization.AllowAuthenticated,
|
|
672
|
+
~opts=({}: Pulumi.CustomResourceOptions.t),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
// Two lines under one order: the first carries the union, the second leaves the
|
|
676
|
+
// optional field absent.
|
|
677
|
+
let _ = await ops.save(
|
|
678
|
+
"o-1",
|
|
679
|
+
{orderId: "o-1", lineNo: "l-1", label: "First", geolocation: Located({lat: 48.2082, lng: 16.3738})},
|
|
680
|
+
Init,
|
|
681
|
+
None,
|
|
682
|
+
)
|
|
683
|
+
let _ = await ops.save("o-1", {orderId: "o-1", lineNo: "l-2", label: "Second"}, Init, None)
|
|
684
|
+
|
|
685
|
+
switch DomainGraphQL_Server.getQueryResolver(name ++ "Items") {
|
|
686
|
+
| Some(r) => r
|
|
687
|
+
| None => JsError.throwWithMessage("items resolver not registered: " ++ name ++ "Items")
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
describe("QueryDb sub-id resolver — the Items door", () => {
|
|
692
|
+
beforeEach(() => {
|
|
693
|
+
DomainGraphQL_Server.reset()
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
testPromise("answers the sub-items of one id, as edges", async () => {
|
|
697
|
+
let resolver = await buildSubIdFixture(~name="ItemA")
|
|
698
|
+
let response = await resolver(JSON.Encode.null, argsOf([("id", strJson("o-1"))]), emptyCtx)
|
|
699
|
+
let edges = getEdges(response)
|
|
700
|
+
expect(edges->Array.length)->toBe(2)
|
|
701
|
+
expect(edgeNodeField(edges->Array.getUnsafe(0), "lineNo"))->toBe("l-1")
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// The gap this fixture exists to close, mirroring the by-index case above: a
|
|
705
|
+
// union reaching a caller through the Items door with the member type the
|
|
706
|
+
// write-time stamp put on it. Without it the member resolves to null and takes
|
|
707
|
+
// its non-nullable parent, so the row leaves the connection rather than erroring.
|
|
708
|
+
testPromise("carries a union field's member type through", async () => {
|
|
709
|
+
let resolver = await buildSubIdFixture(~name="ItemB")
|
|
710
|
+
let response = await resolver(JSON.Encode.null, argsOf([("id", strJson("o-1"))]), emptyCtx)
|
|
711
|
+
let first = getEdges(response)->Array.getUnsafe(0)
|
|
712
|
+
let geo = edgeNodeJson(first, "geolocation")->Option.flatMap(JSON.Decode.object)
|
|
713
|
+
expect(geo->Option.flatMap(o => o->Dict.get("__typename"))->Option.flatMap(JSON.Decode.string))
|
|
714
|
+
->toEqual(Some("GeolocationLocated"))
|
|
715
|
+
expect(geo->Option.flatMap(o => o->Dict.get("lat"))->Option.flatMap(JSON.Decode.float))
|
|
716
|
+
->toEqual(Some(48.2082))
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
testPromise("leaves an absent union absent", async () => {
|
|
720
|
+
let resolver = await buildSubIdFixture(~name="ItemC")
|
|
721
|
+
let response = await resolver(JSON.Encode.null, argsOf([("id", strJson("o-1"))]), emptyCtx)
|
|
722
|
+
let second = getEdges(response)->Array.getUnsafe(1)
|
|
723
|
+
expect(edgeNodeField(second, "label"))->toBe("Second")
|
|
724
|
+
expect(edgeNodeJson(second, "geolocation")->Option.isNone)->toBe(true)
|
|
725
|
+
})
|
|
726
|
+
})
|
|
@@ -648,6 +648,136 @@ globalThis.describe("QueryDb by-index resolver", () => {
|
|
|
648
648
|
});
|
|
649
649
|
});
|
|
650
650
|
|
|
651
|
+
let lineStateSchema = Sury.$schema(s => ({
|
|
652
|
+
orderId: s.m(DcbTag$Reventless.string),
|
|
653
|
+
lineNo: s.m(Sury.string),
|
|
654
|
+
label: s.m(Sury.string),
|
|
655
|
+
geolocation: s.m(Sury.$option(geolocationSchema$1))
|
|
656
|
+
}));
|
|
657
|
+
|
|
658
|
+
let lineStateSchemaWithAnnotations = Sury.$Metadata_set(lineStateSchema, StateAnnotations$Reventless.stateAnnotationsId, {
|
|
659
|
+
ids: ["orderId"],
|
|
660
|
+
compositeIds: [],
|
|
661
|
+
subIds: ["lineNo"],
|
|
662
|
+
compositeSubIds: [],
|
|
663
|
+
indexes: [],
|
|
664
|
+
hidden: [],
|
|
665
|
+
summary: [],
|
|
666
|
+
drillTargets: [],
|
|
667
|
+
drillTargetKeys: [],
|
|
668
|
+
collapsed: [],
|
|
669
|
+
scan: [],
|
|
670
|
+
scanSort: [],
|
|
671
|
+
semantic: [],
|
|
672
|
+
metric: [],
|
|
673
|
+
lifecycle: undefined,
|
|
674
|
+
groupBy: undefined,
|
|
675
|
+
visibility: undefined,
|
|
676
|
+
live: undefined,
|
|
677
|
+
retired: undefined
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
async function buildSubIdFixture(name) {
|
|
681
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
682
|
+
let Storage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
|
|
683
|
+
let Resolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
|
|
684
|
+
let moduleUrl = import.meta.url;
|
|
685
|
+
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
686
|
+
let subIdConfig = {
|
|
687
|
+
subIdField: "lineNo",
|
|
688
|
+
getSubId: state => state.lineNo
|
|
689
|
+
};
|
|
690
|
+
let QDbResolversAdapter = QueryDb_Adapter$ReventlessCore.NoResolvers({
|
|
691
|
+
make: Storage.make
|
|
692
|
+
});
|
|
693
|
+
let Maker = QueryDb_Builder$ReventlessCore.Make({
|
|
694
|
+
Id: {
|
|
695
|
+
schema: Id$Reventless.StringPure.schema,
|
|
696
|
+
make: prim => prim,
|
|
697
|
+
makeFromString: prim => prim,
|
|
698
|
+
toString: prim => prim,
|
|
699
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
700
|
+
},
|
|
701
|
+
name: name,
|
|
702
|
+
moduleUrl: moduleUrl,
|
|
703
|
+
stateSchema: lineStateSchema,
|
|
704
|
+
config: config,
|
|
705
|
+
subIdConfig: subIdConfig,
|
|
706
|
+
authorization: "AllowAuthenticated",
|
|
707
|
+
visibility: "Public"
|
|
708
|
+
})({
|
|
709
|
+
make: Storage.make
|
|
710
|
+
})(QDbResolversAdapter);
|
|
711
|
+
Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name] = {
|
|
712
|
+
singleFieldName: name,
|
|
713
|
+
listFieldName: name + "s",
|
|
714
|
+
returnTypeName: name,
|
|
715
|
+
pluralTypeName: "[" + name + "]",
|
|
716
|
+
includeIdParam: true,
|
|
717
|
+
connectionSpec: true
|
|
718
|
+
};
|
|
719
|
+
Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name] = lineStateSchemaWithAnnotations;
|
|
720
|
+
let queryDb = Maker.make(undefined, undefined, undefined, undefined, undefined);
|
|
721
|
+
let ops = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(queryDb));
|
|
722
|
+
Resolvers.make(name, undefined, undefined, Pulumi.output(""), [], "lineNo", [], [], "AllowAuthenticated", {});
|
|
723
|
+
await ops.save("o-1", {
|
|
724
|
+
orderId: "o-1",
|
|
725
|
+
lineNo: "l-1",
|
|
726
|
+
label: "First",
|
|
727
|
+
geolocation: {
|
|
728
|
+
TAG: "Located",
|
|
729
|
+
lat: 48.2082,
|
|
730
|
+
lng: 16.3738
|
|
731
|
+
}
|
|
732
|
+
}, "Init", undefined);
|
|
733
|
+
await ops.save("o-1", {
|
|
734
|
+
orderId: "o-1",
|
|
735
|
+
lineNo: "l-2",
|
|
736
|
+
label: "Second"
|
|
737
|
+
}, "Init", undefined);
|
|
738
|
+
let r = DomainGraphQL_Server$ReventlessLocal.getQueryResolver(name + "Items");
|
|
739
|
+
if (r !== undefined) {
|
|
740
|
+
return r;
|
|
741
|
+
} else {
|
|
742
|
+
return Stdlib_JsError.throwWithMessage("items resolver not registered: " + name + "Items");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
globalThis.describe("QueryDb sub-id resolver — the Items door", () => {
|
|
747
|
+
globalThis.beforeEach(() => DomainGraphQL_Server$ReventlessLocal.reset());
|
|
748
|
+
globalThis.test("answers the sub-items of one id, as edges", async () => {
|
|
749
|
+
let resolver = await buildSubIdFixture("ItemA");
|
|
750
|
+
let response = await resolver(null, Object.fromEntries([[
|
|
751
|
+
"id",
|
|
752
|
+
"o-1"
|
|
753
|
+
]]), emptyCtx);
|
|
754
|
+
let edges = getEdges(response);
|
|
755
|
+
globalThis.expect(edges.length).toBe(2);
|
|
756
|
+
globalThis.expect(edgeNodeField(edges[0], "lineNo")).toBe("l-1");
|
|
757
|
+
});
|
|
758
|
+
globalThis.test("carries a union field's member type through", async () => {
|
|
759
|
+
let resolver = await buildSubIdFixture("ItemB");
|
|
760
|
+
let response = await resolver(null, Object.fromEntries([[
|
|
761
|
+
"id",
|
|
762
|
+
"o-1"
|
|
763
|
+
]]), emptyCtx);
|
|
764
|
+
let first = getEdges(response)[0];
|
|
765
|
+
let geo = Stdlib_Option.flatMap(edgeNodeJson(first, "geolocation"), Stdlib_JSON.Decode.object);
|
|
766
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(geo, o => o["__typename"]), Stdlib_JSON.Decode.string)).toEqual("GeolocationLocated");
|
|
767
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(geo, o => o["lat"]), Stdlib_JSON.Decode.float)).toEqual(48.2082);
|
|
768
|
+
});
|
|
769
|
+
globalThis.test("leaves an absent union absent", async () => {
|
|
770
|
+
let resolver = await buildSubIdFixture("ItemC");
|
|
771
|
+
let response = await resolver(null, Object.fromEntries([[
|
|
772
|
+
"id",
|
|
773
|
+
"o-1"
|
|
774
|
+
]]), emptyCtx);
|
|
775
|
+
let second = getEdges(response)[1];
|
|
776
|
+
globalThis.expect(edgeNodeField(second, "label")).toBe("Second");
|
|
777
|
+
globalThis.expect(Stdlib_Option.isNone(edgeNodeJson(second, "geolocation"))).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
651
781
|
export {
|
|
652
782
|
geolocationSchema$1 as geolocationSchema,
|
|
653
783
|
rowStateSchema,
|
|
@@ -667,5 +797,8 @@ export {
|
|
|
667
797
|
pageInfoBool,
|
|
668
798
|
pageInfoString,
|
|
669
799
|
buildFixture,
|
|
800
|
+
lineStateSchema,
|
|
801
|
+
lineStateSchemaWithAnnotations,
|
|
802
|
+
buildSubIdFixture,
|
|
670
803
|
}
|
|
671
804
|
/* Not a pure module */
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Behavioural tests for the in-memory cross-table field resolvers
|
|
2
|
+
// (`@resolves` / `@resolvesMany`). An Orders view carries a foreign `productId`
|
|
3
|
+
// and a `productIds` array; both follow into a Products view's rows.
|
|
4
|
+
//
|
|
5
|
+
// The rows handed back are the TARGET's, so the target's `@retired` rule narrows
|
|
6
|
+
// them — the same reading the AppSync response bakes in. A nested field takes no
|
|
7
|
+
// `includeRetired` argument, so a retired row never travels through one.
|
|
8
|
+
|
|
9
|
+
@@warning("-44")
|
|
10
|
+
|
|
11
|
+
open JestGlobals
|
|
12
|
+
|
|
13
|
+
let _ = TestRunner.setup()
|
|
14
|
+
|
|
15
|
+
@schema
|
|
16
|
+
type productRow = {productId: string, name: string, archived: bool}
|
|
17
|
+
|
|
18
|
+
@schema
|
|
19
|
+
type orderRow = {orderId: string, productId: string, productIds: array<string>}
|
|
20
|
+
|
|
21
|
+
let annotations = (~retired: option<Reventless.StateAnnotations.retiredSpec>) => {
|
|
22
|
+
Reventless.StateAnnotations.ids: [],
|
|
23
|
+
compositeIds: [],
|
|
24
|
+
subIds: [],
|
|
25
|
+
compositeSubIds: [],
|
|
26
|
+
indexes: [],
|
|
27
|
+
hidden: [],
|
|
28
|
+
summary: [],
|
|
29
|
+
drillTargets: [],
|
|
30
|
+
drillTargetKeys: [],
|
|
31
|
+
collapsed: [],
|
|
32
|
+
scan: [],
|
|
33
|
+
scanSort: [],
|
|
34
|
+
semantic: [],
|
|
35
|
+
metric: [],
|
|
36
|
+
lifecycle: None,
|
|
37
|
+
groupBy: None,
|
|
38
|
+
visibility: None,
|
|
39
|
+
live: None,
|
|
40
|
+
retired,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let ctxFor = (~groups: array<string>): JSON.t =>
|
|
44
|
+
JSON.Encode.object(
|
|
45
|
+
Dict.fromArray([
|
|
46
|
+
(
|
|
47
|
+
"request",
|
|
48
|
+
JSON.Encode.object(Dict.fromArray([("headers", JSON.Encode.object(Dict.make()))])),
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
"identity",
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
...Reventless.Identity.anonymous,
|
|
55
|
+
userId: "test-user",
|
|
56
|
+
username: "test-user",
|
|
57
|
+
groups,
|
|
58
|
+
}: Reventless.Identity.t
|
|
59
|
+
)->Obj.magic,
|
|
60
|
+
),
|
|
61
|
+
]),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
let plainCtx = ctxFor(~groups=["User"])
|
|
65
|
+
|
|
66
|
+
let noArgs: JSON.t = JSON.Encode.object(Dict.make())
|
|
67
|
+
|
|
68
|
+
let getString = (json: JSON.t, key: string): option<string> =>
|
|
69
|
+
json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
|
|
70
|
+
|
|
71
|
+
let names = (json: JSON.t): array<string> =>
|
|
72
|
+
json->JSON.Decode.array->Option.getOr([])->Array.filterMap(item => item->getString("name"))
|
|
73
|
+
|
|
74
|
+
// One Bus carrying both views, resolvers registered for the Orders view with the
|
|
75
|
+
// two configs the PPX writes from `@resolves` / `@resolvesMany`.
|
|
76
|
+
let buildFixture = async (~suffix: string, ~retired: option<Reventless.StateAnnotations.retiredSpec>) => {
|
|
77
|
+
module Bus = LocalBus.Make()
|
|
78
|
+
module Storage = LocalQueryDbStorage.Make(Bus)
|
|
79
|
+
module Resolvers = QueryDbResolvers_GraphQL.Make(Bus)
|
|
80
|
+
|
|
81
|
+
let productsName = "Products" ++ suffix
|
|
82
|
+
let ordersName = "Orders" ++ suffix
|
|
83
|
+
let orderTypeName = "Shop_Order" ++ suffix
|
|
84
|
+
|
|
85
|
+
module ProductsSpec = {
|
|
86
|
+
module Id = Reventless.Id.StringPure
|
|
87
|
+
let name = productsName
|
|
88
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
89
|
+
@schema
|
|
90
|
+
type state = productRow
|
|
91
|
+
let config = Reventless.ReadModel.config()
|
|
92
|
+
let subIdConfig = None
|
|
93
|
+
let authorization: Reventless.Authorization.permission = AllowAuthenticated
|
|
94
|
+
let visibility: Reventless.Visibility.t = Public
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module OrdersSpec = {
|
|
98
|
+
module Id = Reventless.Id.StringPure
|
|
99
|
+
let name = ordersName
|
|
100
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
101
|
+
@schema
|
|
102
|
+
type state = orderRow
|
|
103
|
+
let config = Reventless.ReadModel.config(
|
|
104
|
+
~idResolvers=[
|
|
105
|
+
{
|
|
106
|
+
source: {
|
|
107
|
+
Reventless.ReadModel.idField: "productId",
|
|
108
|
+
subId: NoSubId,
|
|
109
|
+
resolvedField: Single("product"),
|
|
110
|
+
},
|
|
111
|
+
target: {Reventless.ReadModel.tableName: productsName, idField: Id},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
~idsResolvers=[
|
|
115
|
+
{
|
|
116
|
+
source: {Reventless.ReadModel.idsField: "productIds", resolvedField: "products"},
|
|
117
|
+
target: {Reventless.ReadModel.tableName: productsName},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
let subIdConfig = None
|
|
122
|
+
let authorization: Reventless.Authorization.permission = AllowAuthenticated
|
|
123
|
+
let visibility: Reventless.Visibility.t = Public
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module NoResolvers = ReventlessCore.QueryDb_Adapter.NoResolvers(Storage)
|
|
127
|
+
module ProductsDb = ReventlessCore.QueryDb_Builder.Make(ProductsSpec, Storage, NoResolvers)
|
|
128
|
+
module OrdersDb = ReventlessCore.QueryDb_Builder.Make(OrdersSpec, Storage, NoResolvers)
|
|
129
|
+
|
|
130
|
+
let register = (~viewName, ~returnTypeName) =>
|
|
131
|
+
ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry->Dict.set(
|
|
132
|
+
viewName,
|
|
133
|
+
{
|
|
134
|
+
singleFieldName: returnTypeName,
|
|
135
|
+
listFieldName: returnTypeName ++ "s",
|
|
136
|
+
returnTypeName,
|
|
137
|
+
pluralTypeName: returnTypeName ++ "s",
|
|
138
|
+
includeIdParam: true,
|
|
139
|
+
connectionSpec: true,
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
register(~viewName=productsName, ~returnTypeName="Shop_Product" ++ suffix)
|
|
143
|
+
register(~viewName=ordersName, ~returnTypeName=orderTypeName)
|
|
144
|
+
ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.set(
|
|
145
|
+
productsName,
|
|
146
|
+
productRowSchema
|
|
147
|
+
->S.Metadata.set(
|
|
148
|
+
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
149
|
+
annotations(~retired),
|
|
150
|
+
)
|
|
151
|
+
->S.castToUnknown,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
let productsDb = ProductsDb.make(~api=(), ~apiRole=())
|
|
155
|
+
let productOps = await productsDb->ReventlessCore.Component.operations->TestRunner.resolve
|
|
156
|
+
let ordersDb = OrdersDb.make(~api=(), ~apiRole=())
|
|
157
|
+
let orderOps = await ordersDb->ReventlessCore.Component.operations->TestRunner.resolve
|
|
158
|
+
|
|
159
|
+
let _: ReventlessCore.QueryDb_Adapter.resolvers = Resolvers.make(
|
|
160
|
+
~name=ordersName,
|
|
161
|
+
~api=(),
|
|
162
|
+
~apiRole=(),
|
|
163
|
+
~dataSourceName=""->Pulumi.Output.make,
|
|
164
|
+
~indexes=[],
|
|
165
|
+
~subIdField=None,
|
|
166
|
+
~idResolverConfigs=OrdersSpec.config.idResolvers,
|
|
167
|
+
~idsResolverConfigs=OrdersSpec.config.idsResolvers,
|
|
168
|
+
~authorization=Reventless.Authorization.AllowAuthenticated,
|
|
169
|
+
~opts=({}: Pulumi.CustomResourceOptions.t),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
let _ = await productOps.save("p-1", {productId: "p-1", name: "Book", archived: false}, Init, None)
|
|
173
|
+
let _ = await productOps.save("p-2", {productId: "p-2", name: "Pen", archived: true}, Init, None)
|
|
174
|
+
let _ = await orderOps.save(
|
|
175
|
+
"o-1",
|
|
176
|
+
{orderId: "o-1", productId: "p-1", productIds: ["p-1", "gone", "p-2"]},
|
|
177
|
+
Init,
|
|
178
|
+
None,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
let resolverFor = field =>
|
|
182
|
+
switch DomainGraphQL_Server.getFieldResolver(~typeName=orderTypeName, field) {
|
|
183
|
+
| Some(r) => r
|
|
184
|
+
| None => JsError.throwWithMessage("field resolver not registered: " ++ field)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let order = JSON.Encode.object(
|
|
188
|
+
Dict.fromArray([
|
|
189
|
+
("orderId", JSON.Encode.string("o-1")),
|
|
190
|
+
("productId", JSON.Encode.string("p-1")),
|
|
191
|
+
(
|
|
192
|
+
"productIds",
|
|
193
|
+
["p-1", "gone", "p-2"]->Array.map(JSON.Encode.string)->JSON.Encode.array,
|
|
194
|
+
),
|
|
195
|
+
]),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
(resolverFor, order)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
describe("QueryDb cross-table field resolvers", () => {
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
DomainGraphQL_Server.reset()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
testPromise("@resolves follows the foreign key into the target's row", async () => {
|
|
207
|
+
let (resolverFor, order) = await buildFixture(~suffix="A", ~retired=None)
|
|
208
|
+
let product = await resolverFor("product")(order, noArgs, plainCtx)
|
|
209
|
+
expect((product->getString("name"), product->getString("id")))->toEqual((
|
|
210
|
+
Some("Book"),
|
|
211
|
+
Some("p-1"),
|
|
212
|
+
))
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
testPromise("@resolvesMany batch-follows the id array", async () => {
|
|
216
|
+
let (resolverFor, order) = await buildFixture(~suffix="B", ~retired=None)
|
|
217
|
+
let products = await resolverFor("products")(order, noArgs, plainCtx)
|
|
218
|
+
// "gone" names no row and drops out rather than becoming a null, matching
|
|
219
|
+
// BatchGetItem and the by-ids door built on it.
|
|
220
|
+
expect(products->names)->toEqual(["Book", "Pen"])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
testPromise("a foreign key naming no row resolves to nothing", async () => {
|
|
224
|
+
let (resolverFor, _) = await buildFixture(~suffix="C", ~retired=None)
|
|
225
|
+
let orphan = JSON.Encode.object(
|
|
226
|
+
Dict.fromArray([("productId", JSON.Encode.string("nope"))]),
|
|
227
|
+
)
|
|
228
|
+
let product = await resolverFor("product")(orphan, noArgs, plainCtx)
|
|
229
|
+
expect(product)->toBe(JSON.Encode.null)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// The narrowing is the target's: Products declares the retirement, Orders
|
|
233
|
+
// declares the field, and the caller reading through Orders is answered by
|
|
234
|
+
// the Products rule.
|
|
235
|
+
testPromise("a retired target row is withheld from both forms", async () => {
|
|
236
|
+
let retired: Reventless.StateAnnotations.retiredSpec = {
|
|
237
|
+
field: "archived",
|
|
238
|
+
label: "",
|
|
239
|
+
showWhenFalse: false,
|
|
240
|
+
values: None,
|
|
241
|
+
namedWhenRetired: false,
|
|
242
|
+
}
|
|
243
|
+
let (resolverFor, order) = await buildFixture(~suffix="D", ~retired=Some(retired))
|
|
244
|
+
let products = await resolverFor("products")(order, noArgs, plainCtx)
|
|
245
|
+
expect(products->names)->toEqual(["Book"])
|
|
246
|
+
|
|
247
|
+
let archivedOnly = JSON.Encode.object(
|
|
248
|
+
Dict.fromArray([("productId", JSON.Encode.string("p-2"))]),
|
|
249
|
+
)
|
|
250
|
+
let product = await resolverFor("product")(archivedOnly, noArgs, plainCtx)
|
|
251
|
+
expect(product)->toBe(JSON.Encode.null)
|
|
252
|
+
})
|
|
253
|
+
})
|