@reventlessdev/reventless-core 3.0.0-alpha.156 → 3.0.0-alpha.157
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 +15 -0
- package/package.json +6 -6
- package/src/admin/AdminApi.res +80 -10
- package/src/admin/AdminApi.res.mjs +45 -7
- package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res +65 -0
- package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res.mjs +87 -0
- package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res +58 -0
- package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res.mjs +135 -0
- package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res +44 -0
- package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res.mjs +74 -0
- package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res +50 -0
- package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res.mjs +80 -0
- package/src/admin/Platform_Admin.res +37 -1
- package/src/admin/Platform_Admin.res.mjs +18 -5
- package/src/admin/Platform_ApiFragmentsApi.res +32 -0
- package/src/admin/Platform_ApiFragmentsApi.res.mjs +52 -0
- package/src/admin/Platform_UIFragmentsApi.res +1 -1
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +50 -0
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +70 -0
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res +33 -0
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res.mjs +80 -0
- package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res +33 -0
- package/src/admin/{UIFragmentRegistryReadModelSpec.res.mjs → UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs} +24 -2
- package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res +34 -0
- package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs +54 -0
- package/src/components/Api/Api_Naming.res +1 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res +7 -11
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +8 -22
- package/src/components/Api/GraphQL_PushPlanner.res +65 -0
- package/src/components/Api/GraphQL_PushPlanner.res.mjs +46 -0
- package/src/components/Api/GraphQL_Stitcher.res +72 -1
- package/src/components/Api/GraphQL_Stitcher.res.mjs +53 -2
- package/src/components/Dcb/Dcb_Builder.res +16 -6
- package/src/components/Dcb/Dcb_Builder.res.mjs +10 -7
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +6 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
- package/src/plugin/api/PluginBaseFragment.res +5 -6
- package/src/plugin/api/PluginBaseFragment.res.mjs +0 -1
- package/src/plugin/component/Plugin_Builder.res +4 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +7 -6
- package/src/plugin/component/Plugin_Helpers.res +30 -0
- package/src/plugin/component/Plugin_Helpers.res.mjs +13 -3
- package/src/plugin/component/Plugin_SubscriptionSchema.res +19 -9
- package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +23 -14
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +28 -6
- package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +34 -14
- package/src/plugin/connect/PluginExtensionPoint_Builder.res +7 -1
- package/src/plugin/connect/PluginExtensionPoint_Builder.res.mjs +5 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res +3 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +4 -2
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +52 -0
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +74 -0
- package/src/plugin/lifecycle/PluginBehavior.res +9 -51
- package/src/plugin/lifecycle/PluginBehavior.res.mjs +39 -68
- package/src/plugin/lifecycle/PluginSpec.res +0 -18
- package/src/plugin/lifecycle/PluginSpec.res.mjs +0 -30
- package/src/plugin/lifecycle/PluginsProjection.res +2 -6
- package/src/plugin/lifecycle/PluginsProjection.res.mjs +0 -2
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +0 -1
- package/tests/admin/Platform_ApiFragmentsApiTest.res +66 -0
- package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +68 -0
- package/tests/admin/Platform_UIFragmentsApiTest.res +1 -1
- package/tests/api/GraphQL_PushPlannerTest.res +62 -0
- package/tests/api/GraphQL_PushPlannerTest.res.mjs +80 -0
- package/tests/api/GraphQL_StitcherTest.res +63 -0
- package/tests/api/GraphQL_StitcherTest.res.mjs +75 -0
- package/tests/logger/LogFormatTest.res +1 -2
- package/tests/logger/LogFormatTest.res.mjs +1 -2
- package/tests/message/MessageTest.res +0 -3
- package/tests/message/MessageTest.res.mjs +0 -3
- package/src/admin/UIFragmentRegistryProjection.res +0 -43
- package/src/admin/UIFragmentRegistryProjection.res.mjs +0 -81
- package/src/admin/UIFragmentRegistryReadModelSpec.res +0 -13
|
@@ -28,7 +28,6 @@ type state = {
|
|
|
28
28
|
// API target for split-API schema routing. Absent/None means "Domain" (backward compat).
|
|
29
29
|
// "Platform" → excluded from DomainApi runtime schema stitching in updateApiSchema.
|
|
30
30
|
apiTarget?: string,
|
|
31
|
-
uiFragments: @s.matches(Reventless.Plugin.uiFragmentManifestOptionSchema) option<Reventless.Plugin.uiFragmentManifest>,
|
|
32
31
|
// Plugin structure (component metadata) — surfaced via Platform_ComponentDefinitions.
|
|
33
32
|
// None for older plugins whose protocol version did not carry the field.
|
|
34
33
|
structure: @s.matches(Reventless.Plugin.pluginStructureOptionSchema) option<Reventless.Plugin.pluginStructure>,
|
|
@@ -75,7 +74,6 @@ type queryResult = {
|
|
|
75
74
|
status: status,
|
|
76
75
|
apiSchemaFragment: option<Reventless.Plugin.apiSchemaFragment>,
|
|
77
76
|
apiTarget?: string,
|
|
78
|
-
uiFragments: option<Reventless.Plugin.uiFragmentManifest>,
|
|
79
77
|
structure: option<Reventless.Plugin.pluginStructure>,
|
|
80
78
|
dcbEventLog: option<Reventless.Plugin.dcbEventLogDefinition>,
|
|
81
79
|
kind: option<Reventless.Plugin.pluginKind>,
|
|
@@ -25,7 +25,6 @@ let stateSchema = S.schema(s => ({
|
|
|
25
25
|
statusChange: s.m(Message$ReventlessCore.statusChangeSchema),
|
|
26
26
|
apiSchemaFragment: s.m(Plugin$Reventless.apiSchemaFragmentOptionSchema),
|
|
27
27
|
apiTarget: s.m(S.option(S.string)),
|
|
28
|
-
uiFragments: s.m(Plugin$Reventless.uiFragmentManifestOptionSchema),
|
|
29
28
|
structure: s.m(Plugin$Reventless.pluginStructureOptionSchema),
|
|
30
29
|
dcbEventLog: s.m(Plugin$Reventless.dcbEventLogOptionSchema),
|
|
31
30
|
kind: s.m(S.option(Plugin$Reventless.pluginKindSchema)),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Asserts the canonical JSON shape produced by encodeApiFragmentEntry — the
|
|
2
|
+
// deploy-facing status surface of the API-schema fragment registry, shared by
|
|
3
|
+
// the in-memory and AWS adapters. Status only (no encoded SDL). If this test
|
|
4
|
+
// changes, any deploy waiter parsing Platform_ApiFragments is affected.
|
|
5
|
+
|
|
6
|
+
open JestGlobals
|
|
7
|
+
|
|
8
|
+
let state: ApiFragments.state = {
|
|
9
|
+
pluginId: "Catalog",
|
|
10
|
+
encoded: `{"types":[],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
11
|
+
protocol: "graphql",
|
|
12
|
+
apiTarget: Reventless.Plugin.Domain,
|
|
13
|
+
registeredAt: "2026-05-14T00:00:00.000Z",
|
|
14
|
+
updatedAt: "2026-05-14T00:00:00.000Z",
|
|
15
|
+
pushStatus: "ok",
|
|
16
|
+
pushMessage: "",
|
|
17
|
+
pushedAt: "2026-05-14T00:00:01.000Z",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let encoded = Platform_ApiFragmentsApi.encodeApiFragmentEntry(state)
|
|
21
|
+
let json = encoded->JSON.stringify
|
|
22
|
+
|
|
23
|
+
describe("encodeApiFragmentEntry", () => {
|
|
24
|
+
testSync("produces a JSON object with the expected top-level keys (status only, no SDL)", () => {
|
|
25
|
+
let dict = encoded->JSON.Decode.object->Option.getOr(Dict.make())
|
|
26
|
+
let keys = dict->Dict.keysToArray->Array.toSorted(String.compare)
|
|
27
|
+
expect(keys)->toEqual([
|
|
28
|
+
"apiTarget",
|
|
29
|
+
"pluginId",
|
|
30
|
+
"pushMessage",
|
|
31
|
+
"pushStatus",
|
|
32
|
+
"pushedAt",
|
|
33
|
+
"registeredAt",
|
|
34
|
+
"updatedAt",
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
testSync("encodes pluginId", () =>
|
|
39
|
+
expect(json->String.includes("\"pluginId\":\"Catalog\""))->toEqual(true)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
testSync("encodes the apiTarget variant as its bare string", () =>
|
|
43
|
+
expect(json->String.includes("\"apiTarget\":\"Domain\""))->toEqual(true)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
testSync("encodes pushStatus", () =>
|
|
47
|
+
expect(json->String.includes("\"pushStatus\":\"ok\""))->toEqual(true)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
testSync("does not leak the encoded SDL into the status shape", () =>
|
|
51
|
+
expect(json->String.includes("subscriptionSources"))->toEqual(false)
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("Platform_ApiFragmentsApi.sdl", () => {
|
|
56
|
+
testSync("query field declares the list type", () =>
|
|
57
|
+
expect(Platform_ApiFragmentsApi.sdlQueryField->String.includes("[Platform_ApiFragmentEntry!]!"))
|
|
58
|
+
->toEqual(true)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
testSync("sdlTypes includes Platform_ApiFragmentEntry with pushStatus field", () => {
|
|
62
|
+
let allTypes = Platform_ApiFragmentsApi.sdlTypes->Array.join("\n")
|
|
63
|
+
expect(allTypes->String.includes("Platform_ApiFragmentEntry"))->toEqual(true)
|
|
64
|
+
expect(allTypes->String.includes("pushStatus: String!"))->toEqual(true)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
6
|
+
import * as Platform_ApiFragmentsApi$ReventlessCore from "../../src/admin/Platform_ApiFragmentsApi.res.mjs";
|
|
7
|
+
|
|
8
|
+
let state = {
|
|
9
|
+
pluginId: "Catalog",
|
|
10
|
+
encoded: `{"types":[],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
11
|
+
protocol: "graphql",
|
|
12
|
+
apiTarget: "Domain",
|
|
13
|
+
registeredAt: "2026-05-14T00:00:00.000Z",
|
|
14
|
+
updatedAt: "2026-05-14T00:00:00.000Z",
|
|
15
|
+
pushStatus: "ok",
|
|
16
|
+
pushMessage: "",
|
|
17
|
+
pushedAt: "2026-05-14T00:00:01.000Z"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let encoded = Platform_ApiFragmentsApi$ReventlessCore.encodeApiFragmentEntry(state);
|
|
21
|
+
|
|
22
|
+
let json = JSON.stringify(encoded);
|
|
23
|
+
|
|
24
|
+
globalThis.describe("encodeApiFragmentEntry", () => {
|
|
25
|
+
globalThis.test("produces a JSON object with the expected top-level keys (status only, no SDL)", () => {
|
|
26
|
+
let dict = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(encoded), {});
|
|
27
|
+
let keys = Object.keys(dict).toSorted(Primitive_string.compare);
|
|
28
|
+
globalThis.expect(keys).toEqual([
|
|
29
|
+
"apiTarget",
|
|
30
|
+
"pluginId",
|
|
31
|
+
"pushMessage",
|
|
32
|
+
"pushStatus",
|
|
33
|
+
"pushedAt",
|
|
34
|
+
"registeredAt",
|
|
35
|
+
"updatedAt"
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
globalThis.test("encodes pluginId", () => {
|
|
39
|
+
globalThis.expect(json.includes("\"pluginId\":\"Catalog\"")).toEqual(true);
|
|
40
|
+
});
|
|
41
|
+
globalThis.test("encodes the apiTarget variant as its bare string", () => {
|
|
42
|
+
globalThis.expect(json.includes("\"apiTarget\":\"Domain\"")).toEqual(true);
|
|
43
|
+
});
|
|
44
|
+
globalThis.test("encodes pushStatus", () => {
|
|
45
|
+
globalThis.expect(json.includes("\"pushStatus\":\"ok\"")).toEqual(true);
|
|
46
|
+
});
|
|
47
|
+
globalThis.test("does not leak the encoded SDL into the status shape", () => {
|
|
48
|
+
globalThis.expect(json.includes("subscriptionSources")).toEqual(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
globalThis.describe("Platform_ApiFragmentsApi.sdl", () => {
|
|
53
|
+
globalThis.test("query field declares the list type", () => {
|
|
54
|
+
globalThis.expect(Platform_ApiFragmentsApi$ReventlessCore.sdlQueryField.includes("[Platform_ApiFragmentEntry!]!")).toEqual(true);
|
|
55
|
+
});
|
|
56
|
+
globalThis.test("sdlTypes includes Platform_ApiFragmentEntry with pushStatus field", () => {
|
|
57
|
+
let allTypes = Platform_ApiFragmentsApi$ReventlessCore.sdlTypes.join("\n");
|
|
58
|
+
globalThis.expect(allTypes.includes("Platform_ApiFragmentEntry")).toEqual(true);
|
|
59
|
+
globalThis.expect(allTypes.includes("pushStatus: String!")).toEqual(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
state,
|
|
65
|
+
encoded,
|
|
66
|
+
json,
|
|
67
|
+
}
|
|
68
|
+
/* encoded Not a pure module */
|
|
@@ -25,7 +25,7 @@ let page: pageManifestEntry = {
|
|
|
25
25
|
requiredAccess: Some("admin"),
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
let state:
|
|
28
|
+
let state: UiFragments.state = {
|
|
29
29
|
pluginId: "Catalog",
|
|
30
30
|
remoteEntryUrl: "https://cdn.example.com/catalog@1.0/remoteEntry.js",
|
|
31
31
|
panels: [panel],
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
open GraphQL_PushPlanner
|
|
3
|
+
|
|
4
|
+
// Minimal neutral fragments. `stitch` always adds Relay base types + `node`, so we assert on the
|
|
5
|
+
// distinctive field names to know which base/fragments landed in each plan.
|
|
6
|
+
let mkFragment = (~query: string): Reventless.Plugin.apiSchemaFragment =>
|
|
7
|
+
GraphQL_Stitcher.encode({
|
|
8
|
+
types: [],
|
|
9
|
+
queries: [query],
|
|
10
|
+
mutations: [],
|
|
11
|
+
subscriptions: [],
|
|
12
|
+
subscriptionSources: [],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
let adminBase = mkFragment(~query=" Admin_Q: String")
|
|
16
|
+
let domainFrag = mkFragment(~query=" Domain_Q: String")
|
|
17
|
+
let platformFrag = mkFragment(~query=" Platform_Q: String")
|
|
18
|
+
|
|
19
|
+
let fragments = [
|
|
20
|
+
{fragment: domainFrag, target: Domain},
|
|
21
|
+
{fragment: platformFrag, target: Platform},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
let planFor = (plans: array<pushPlan>, api: planTarget): option<pushPlan> =>
|
|
25
|
+
plans->Array.find(p => p.api == api)
|
|
26
|
+
|
|
27
|
+
describe("GraphQL_PushPlanner.planPushes", () => {
|
|
28
|
+
testSync("split mode: admin base + Platform fragments on the Platform API only", () => {
|
|
29
|
+
let plans = planPushes(~adminBase, ~fragments, ~splitApi=true)
|
|
30
|
+
expect(plans->Array.length)->toBe(2)
|
|
31
|
+
let platform = planFor(plans, PlatformApi)->Belt.Option.getExn
|
|
32
|
+
expect(platform.sdl->String.includes("Admin_Q"))->toBe(true)
|
|
33
|
+
expect(platform.sdl->String.includes("Platform_Q"))->toBe(true)
|
|
34
|
+
expect(platform.sdl->String.includes("Domain_Q"))->toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
testSync("split mode: empty base + Domain fragments on the Domain API only (no admin)", () => {
|
|
38
|
+
let plans = planPushes(~adminBase, ~fragments, ~splitApi=true)
|
|
39
|
+
let domain = planFor(plans, DomainApi)->Belt.Option.getExn
|
|
40
|
+
expect(domain.sdl->String.includes("Domain_Q"))->toBe(true)
|
|
41
|
+
expect(domain.sdl->String.includes("Admin_Q"))->toBe(false)
|
|
42
|
+
expect(domain.sdl->String.includes("Platform_Q"))->toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
testSync("unified mode: one Domain API push with admin base + ALL fragments", () => {
|
|
46
|
+
let plans = planPushes(~adminBase, ~fragments, ~splitApi=false)
|
|
47
|
+
expect(plans->Array.length)->toBe(1)
|
|
48
|
+
let only = plans->Array.getUnsafe(0)
|
|
49
|
+
expect(only.api == DomainApi)->toBe(true)
|
|
50
|
+
expect(only.sdl->String.includes("Admin_Q"))->toBe(true)
|
|
51
|
+
expect(only.sdl->String.includes("Domain_Q"))->toBe(true)
|
|
52
|
+
expect(only.sdl->String.includes("Platform_Q"))->toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
testSync("empty registry still pushes the admin base (split → Platform API)", () => {
|
|
56
|
+
let plans = planPushes(~adminBase, ~fragments=[], ~splitApi=true)
|
|
57
|
+
let platform = planFor(plans, PlatformApi)->Belt.Option.getExn
|
|
58
|
+
expect(platform.sdl->String.includes("Admin_Q"))->toBe(true)
|
|
59
|
+
let domain = planFor(plans, DomainApi)->Belt.Option.getExn
|
|
60
|
+
expect(domain.sdl->String.includes("Admin_Q"))->toBe(false)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js";
|
|
4
|
+
import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
5
|
+
import * as GraphQL_PushPlanner$ReventlessCore from "../../src/components/Api/GraphQL_PushPlanner.res.mjs";
|
|
6
|
+
|
|
7
|
+
function mkFragment(query) {
|
|
8
|
+
return GraphQL_Stitcher$ReventlessCore.encode({
|
|
9
|
+
types: [],
|
|
10
|
+
mutations: [],
|
|
11
|
+
queries: [query],
|
|
12
|
+
subscriptions: [],
|
|
13
|
+
subscriptionSources: []
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let adminBase = mkFragment(" Admin_Q: String");
|
|
18
|
+
|
|
19
|
+
let domainFrag = mkFragment(" Domain_Q: String");
|
|
20
|
+
|
|
21
|
+
let platformFrag = mkFragment(" Platform_Q: String");
|
|
22
|
+
|
|
23
|
+
let fragments = [
|
|
24
|
+
{
|
|
25
|
+
fragment: domainFrag,
|
|
26
|
+
target: "Domain"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
fragment: platformFrag,
|
|
30
|
+
target: "Platform"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function planFor(plans, api) {
|
|
35
|
+
return plans.find(p => p.api === api);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
globalThis.describe("GraphQL_PushPlanner.planPushes", () => {
|
|
39
|
+
globalThis.test("split mode: admin base + Platform fragments on the Platform API only", () => {
|
|
40
|
+
let plans = GraphQL_PushPlanner$ReventlessCore.planPushes(adminBase, fragments, true);
|
|
41
|
+
globalThis.expect(plans.length).toBe(2);
|
|
42
|
+
let platform = Belt_Option.getExn(planFor(plans, "PlatformApi"));
|
|
43
|
+
globalThis.expect(platform.sdl.includes("Admin_Q")).toBe(true);
|
|
44
|
+
globalThis.expect(platform.sdl.includes("Platform_Q")).toBe(true);
|
|
45
|
+
globalThis.expect(platform.sdl.includes("Domain_Q")).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
globalThis.test("split mode: empty base + Domain fragments on the Domain API only (no admin)", () => {
|
|
48
|
+
let plans = GraphQL_PushPlanner$ReventlessCore.planPushes(adminBase, fragments, true);
|
|
49
|
+
let domain = Belt_Option.getExn(planFor(plans, "DomainApi"));
|
|
50
|
+
globalThis.expect(domain.sdl.includes("Domain_Q")).toBe(true);
|
|
51
|
+
globalThis.expect(domain.sdl.includes("Admin_Q")).toBe(false);
|
|
52
|
+
globalThis.expect(domain.sdl.includes("Platform_Q")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
globalThis.test("unified mode: one Domain API push with admin base + ALL fragments", () => {
|
|
55
|
+
let plans = GraphQL_PushPlanner$ReventlessCore.planPushes(adminBase, fragments, false);
|
|
56
|
+
globalThis.expect(plans.length).toBe(1);
|
|
57
|
+
let only = plans[0];
|
|
58
|
+
globalThis.expect(only.api === "DomainApi").toBe(true);
|
|
59
|
+
globalThis.expect(only.sdl.includes("Admin_Q")).toBe(true);
|
|
60
|
+
globalThis.expect(only.sdl.includes("Domain_Q")).toBe(true);
|
|
61
|
+
globalThis.expect(only.sdl.includes("Platform_Q")).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
globalThis.test("empty registry still pushes the admin base (split → Platform API)", () => {
|
|
64
|
+
let plans = GraphQL_PushPlanner$ReventlessCore.planPushes(adminBase, [], true);
|
|
65
|
+
let platform = Belt_Option.getExn(planFor(plans, "PlatformApi"));
|
|
66
|
+
globalThis.expect(platform.sdl.includes("Admin_Q")).toBe(true);
|
|
67
|
+
let domain = Belt_Option.getExn(planFor(plans, "DomainApi"));
|
|
68
|
+
globalThis.expect(domain.sdl.includes("Admin_Q")).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
mkFragment,
|
|
74
|
+
adminBase,
|
|
75
|
+
domainFrag,
|
|
76
|
+
platformFrag,
|
|
77
|
+
fragments,
|
|
78
|
+
planFor,
|
|
79
|
+
}
|
|
80
|
+
/* adminBase Not a pure module */
|
|
@@ -198,3 +198,66 @@ type Mutation {
|
|
|
198
198
|
expect(missing->Array.includes("Ordering_placeOrder"))->toBe(true)
|
|
199
199
|
})
|
|
200
200
|
})
|
|
201
|
+
|
|
202
|
+
describe("GraphQL_Stitcher.subscriptionSources", () => {
|
|
203
|
+
testSync("encode/decode round-trips the subscription→mutation source mapping", () => {
|
|
204
|
+
let fragment = encode({
|
|
205
|
+
types: [],
|
|
206
|
+
mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
|
|
207
|
+
queries: [],
|
|
208
|
+
subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
|
|
209
|
+
subscriptionSources: [{field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]}],
|
|
210
|
+
})
|
|
211
|
+
let parts = decode(fragment)
|
|
212
|
+
expect(parts.subscriptionSources)->toEqual([
|
|
213
|
+
{field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]},
|
|
214
|
+
])
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
testSync("decode tolerates fragments encoded before the field existed", () => {
|
|
218
|
+
let legacy: Reventless.Plugin.apiSchemaFragment = {
|
|
219
|
+
encoded: `{"types":[],"mutations":[],"queries":[],"subscriptions":[" onX(id: ID): CommandResult"]}`,
|
|
220
|
+
protocol: "graphql",
|
|
221
|
+
}
|
|
222
|
+
expect(decode(legacy).subscriptionSources)->toEqual([])
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
testSync("collectSubscriptionSources unions across fragments, first wins per field", () => {
|
|
226
|
+
let base = encode({
|
|
227
|
+
types: [],
|
|
228
|
+
mutations: [],
|
|
229
|
+
queries: [],
|
|
230
|
+
subscriptions: [` onUIFragmentChange: UIFragmentChangeEvent`],
|
|
231
|
+
subscriptionSources: [
|
|
232
|
+
{field: "onUIFragmentChange", mutations: ["Platform_UIFragmentRegistered"]},
|
|
233
|
+
],
|
|
234
|
+
})
|
|
235
|
+
let pluginA = encode({
|
|
236
|
+
types: [],
|
|
237
|
+
mutations: [],
|
|
238
|
+
queries: [],
|
|
239
|
+
subscriptions: [` onA(id: ID): CommandResult`],
|
|
240
|
+
subscriptionSources: [
|
|
241
|
+
{field: "onA", mutations: ["A"]},
|
|
242
|
+
// Colliding field — the base fragment's mapping must win.
|
|
243
|
+
{field: "onUIFragmentChange", mutations: ["Rogue_Mutation"]},
|
|
244
|
+
],
|
|
245
|
+
})
|
|
246
|
+
let collected = collectSubscriptionSources(~baseFragment=base, ~pluginFragments=[pluginA])
|
|
247
|
+
expect(collected)->toEqual([
|
|
248
|
+
{field: "onUIFragmentChange", mutations: ["Platform_UIFragmentRegistered"]},
|
|
249
|
+
{field: "onA", mutations: ["A"]},
|
|
250
|
+
])
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
testSync("core-emitted admin base fragment carries no provider directives", () => {
|
|
254
|
+
let base = AdminApi.baseFragment(~cloner=true)
|
|
255
|
+
expect(base.encoded->String.includes("@aws_subscribe"))->toBe(false)
|
|
256
|
+
expect(base.encoded->String.includes("@aws_auth"))->toBe(false)
|
|
257
|
+
// The subscription→mutation fan-in rides the structured metadata instead.
|
|
258
|
+
let parts = decode(base)
|
|
259
|
+
let fields = parts.subscriptionSources->Array.map(s => s.field)
|
|
260
|
+
expect(fields->Array.includes("onUIFragmentChange"))->toBe(true)
|
|
261
|
+
expect(fields->Array.includes("onPluginStatusChange"))->toBe(true)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as AdminApi$ReventlessCore from "../../src/admin/AdminApi.res.mjs";
|
|
3
4
|
import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
4
5
|
|
|
5
6
|
let fullSchemaSdl = `type Node {
|
|
@@ -165,6 +166,80 @@ type Mutation {
|
|
|
165
166
|
});
|
|
166
167
|
});
|
|
167
168
|
|
|
169
|
+
globalThis.describe("GraphQL_Stitcher.subscriptionSources", () => {
|
|
170
|
+
globalThis.test("encode/decode round-trips the subscription→mutation source mapping", () => {
|
|
171
|
+
let fragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
172
|
+
types: [],
|
|
173
|
+
mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
|
|
174
|
+
queries: [],
|
|
175
|
+
subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
|
|
176
|
+
subscriptionSources: [{
|
|
177
|
+
field: "onCatalog_AddProduct",
|
|
178
|
+
mutations: ["Catalog_AddProduct"]
|
|
179
|
+
}]
|
|
180
|
+
});
|
|
181
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
|
|
182
|
+
globalThis.expect(parts.subscriptionSources).toEqual([{
|
|
183
|
+
field: "onCatalog_AddProduct",
|
|
184
|
+
mutations: ["Catalog_AddProduct"]
|
|
185
|
+
}]);
|
|
186
|
+
});
|
|
187
|
+
globalThis.test("decode tolerates fragments encoded before the field existed", () => {
|
|
188
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode({
|
|
189
|
+
encoded: `{"types":[],"mutations":[],"queries":[],"subscriptions":[" onX(id: ID): CommandResult"]}`,
|
|
190
|
+
protocol: "graphql"
|
|
191
|
+
}).subscriptionSources).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
globalThis.test("collectSubscriptionSources unions across fragments, first wins per field", () => {
|
|
194
|
+
let base = GraphQL_Stitcher$ReventlessCore.encode({
|
|
195
|
+
types: [],
|
|
196
|
+
mutations: [],
|
|
197
|
+
queries: [],
|
|
198
|
+
subscriptions: [` onUIFragmentChange: UIFragmentChangeEvent`],
|
|
199
|
+
subscriptionSources: [{
|
|
200
|
+
field: "onUIFragmentChange",
|
|
201
|
+
mutations: ["Platform_UIFragmentRegistered"]
|
|
202
|
+
}]
|
|
203
|
+
});
|
|
204
|
+
let pluginA = GraphQL_Stitcher$ReventlessCore.encode({
|
|
205
|
+
types: [],
|
|
206
|
+
mutations: [],
|
|
207
|
+
queries: [],
|
|
208
|
+
subscriptions: [` onA(id: ID): CommandResult`],
|
|
209
|
+
subscriptionSources: [
|
|
210
|
+
{
|
|
211
|
+
field: "onA",
|
|
212
|
+
mutations: ["A"]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
field: "onUIFragmentChange",
|
|
216
|
+
mutations: ["Rogue_Mutation"]
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
let collected = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(base, [pluginA]);
|
|
221
|
+
globalThis.expect(collected).toEqual([
|
|
222
|
+
{
|
|
223
|
+
field: "onUIFragmentChange",
|
|
224
|
+
mutations: ["Platform_UIFragmentRegistered"]
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
field: "onA",
|
|
228
|
+
mutations: ["A"]
|
|
229
|
+
}
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
globalThis.test("core-emitted admin base fragment carries no provider directives", () => {
|
|
233
|
+
let base = AdminApi$ReventlessCore.baseFragment(true);
|
|
234
|
+
globalThis.expect(base.encoded.includes("@aws_subscribe")).toBe(false);
|
|
235
|
+
globalThis.expect(base.encoded.includes("@aws_auth")).toBe(false);
|
|
236
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(base);
|
|
237
|
+
let fields = parts.subscriptionSources.map(s => s.field);
|
|
238
|
+
globalThis.expect(fields.includes("onUIFragmentChange")).toBe(true);
|
|
239
|
+
globalThis.expect(fields.includes("onPluginStatusChange")).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
168
243
|
export {
|
|
169
244
|
fullSchemaSdl,
|
|
170
245
|
collapsedSchemaSdl,
|
|
@@ -73,7 +73,6 @@ describe("LogFormat", () => {
|
|
|
73
73
|
extensionProtocols: [],
|
|
74
74
|
apiSchemaFragment: None,
|
|
75
75
|
apiTarget: None,
|
|
76
|
-
uiFragments: None,
|
|
77
76
|
structure: None,
|
|
78
77
|
dcbEventLog: None,
|
|
79
78
|
kind: Domain,
|
|
@@ -98,7 +97,7 @@ describe("LogFormat", () => {
|
|
|
98
97
|
},
|
|
99
98
|
)
|
|
100
99
|
let expected1 = `1/2: \x1b[1mHeartbeat\x1b[0m(0): {"command":{"TAG":"Heartbeat","_0":"0.0.0"},"meta":${metaStr},"id":"0"}`
|
|
101
|
-
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"
|
|
100
|
+
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null,"dcbEventLog":null,"kind":"Domain"}},"meta":${metaStr},"id":"1"}`
|
|
102
101
|
expect(commandJsonsToLogMessages(arr))->toEqual([expected1, expected2])
|
|
103
102
|
},
|
|
104
103
|
)
|
|
@@ -74,7 +74,6 @@ globalThis.describe("LogFormat", () => {
|
|
|
74
74
|
extensionProtocols: [],
|
|
75
75
|
apiSchemaFragment: undefined,
|
|
76
76
|
apiTarget: undefined,
|
|
77
|
-
uiFragments: undefined,
|
|
78
77
|
structure: undefined,
|
|
79
78
|
dcbEventLog: undefined,
|
|
80
79
|
kind: "Domain"
|
|
@@ -96,7 +95,7 @@ globalThis.describe("LogFormat", () => {
|
|
|
96
95
|
commandJson: Message$ReventlessCore.encode(command, PluginSpec$ReventlessCore.commandSchema)
|
|
97
96
|
}));
|
|
98
97
|
let expected1 = `1/2: \x1b[1mHeartbeat\x1b[0m(0): {"command":{"TAG":"Heartbeat","_0":"0.0.0"},"meta":` + metaStr + `,"id":"0"}`;
|
|
99
|
-
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"
|
|
98
|
+
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null,"dcbEventLog":null,"kind":"Domain"}},"meta":` + metaStr + `,"id":"1"}`;
|
|
100
99
|
globalThis.expect(LogFormat$ReventlessCore.commandJsonsToLogMessages(arr)).toEqual([
|
|
101
100
|
expected1,
|
|
102
101
|
expected2
|
|
@@ -41,7 +41,6 @@ describe("Message should", () => {
|
|
|
41
41
|
extensionProtocols: [],
|
|
42
42
|
apiSchemaFragment: None,
|
|
43
43
|
apiTarget: None,
|
|
44
|
-
uiFragments: None,
|
|
45
44
|
structure: None,
|
|
46
45
|
dcbEventLog: None,
|
|
47
46
|
kind: Domain,
|
|
@@ -110,7 +109,6 @@ describe("Message should", () => {
|
|
|
110
109
|
extensionProtocols: [],
|
|
111
110
|
apiSchemaFragment: None,
|
|
112
111
|
apiTarget: None,
|
|
113
|
-
uiFragments: None,
|
|
114
112
|
structure: Some({
|
|
115
113
|
readModels: [],
|
|
116
114
|
stateViewSlices: [],
|
|
@@ -209,7 +207,6 @@ describe("Message should", () => {
|
|
|
209
207
|
extensionProtocols: [],
|
|
210
208
|
apiSchemaFragment: None,
|
|
211
209
|
apiTarget: None,
|
|
212
|
-
uiFragments: None,
|
|
213
210
|
structure: None,
|
|
214
211
|
dcbEventLog: None,
|
|
215
212
|
kind: Domain,
|
|
@@ -41,7 +41,6 @@ globalThis.describe("Message should", () => {
|
|
|
41
41
|
extensionProtocols: [],
|
|
42
42
|
apiSchemaFragment: undefined,
|
|
43
43
|
apiTarget: undefined,
|
|
44
|
-
uiFragments: undefined,
|
|
45
44
|
structure: undefined,
|
|
46
45
|
dcbEventLog: undefined,
|
|
47
46
|
kind: "Domain"
|
|
@@ -100,7 +99,6 @@ globalThis.describe("Message should", () => {
|
|
|
100
99
|
extensionProtocols: [],
|
|
101
100
|
apiSchemaFragment: undefined,
|
|
102
101
|
apiTarget: undefined,
|
|
103
|
-
uiFragments: undefined,
|
|
104
102
|
structure: {
|
|
105
103
|
readModels: [],
|
|
106
104
|
stateViewSlices: [],
|
|
@@ -188,7 +186,6 @@ globalThis.describe("Message should", () => {
|
|
|
188
186
|
extensionProtocols: [],
|
|
189
187
|
apiSchemaFragment: undefined,
|
|
190
188
|
apiTarget: undefined,
|
|
191
|
-
uiFragments: undefined,
|
|
192
189
|
structure: undefined,
|
|
193
190
|
dcbEventLog: undefined,
|
|
194
191
|
kind: "Domain"
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
open Reventless.Message
|
|
2
|
-
open Reventless.Projection
|
|
3
|
-
|
|
4
|
-
let moduleUrl: string = %raw(`import.meta.url`)
|
|
5
|
-
|
|
6
|
-
module UIFragmentRegistryMapping = Reventless.Projection.Mapping.Make(
|
|
7
|
-
PluginSpec,
|
|
8
|
-
UIFragmentRegistryReadModelSpec,
|
|
9
|
-
{
|
|
10
|
-
let project = ({event, id, meta: {time}}) =>
|
|
11
|
-
switch event {
|
|
12
|
-
| PluginSpec.UIFragmentRegistered({manifest}) =>
|
|
13
|
-
Set(
|
|
14
|
-
id,
|
|
15
|
-
{
|
|
16
|
-
UIFragmentRegistryReadModelSpec.pluginId: id,
|
|
17
|
-
remoteEntryUrl: manifest.remoteEntryUrl,
|
|
18
|
-
panels: manifest.panels,
|
|
19
|
-
pages: manifest.pages,
|
|
20
|
-
registeredAt: time,
|
|
21
|
-
updatedAt: time,
|
|
22
|
-
},
|
|
23
|
-
)
|
|
24
|
-
| UIFragmentUpdated({newManifest}) =>
|
|
25
|
-
Update(
|
|
26
|
-
id,
|
|
27
|
-
state => {
|
|
28
|
-
...state,
|
|
29
|
-
remoteEntryUrl: newManifest.remoteEntryUrl,
|
|
30
|
-
panels: newManifest.panels,
|
|
31
|
-
pages: newManifest.pages,
|
|
32
|
-
updatedAt: time,
|
|
33
|
-
},
|
|
34
|
-
)
|
|
35
|
-
| UIFragmentDeregistered(_) => Delete(id)
|
|
36
|
-
| _ => Ignore
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
module Mappings = Reventless.Projection.Mappings.Make(UIFragmentRegistryReadModelSpec)
|
|
42
|
-
|
|
43
|
-
let mappings: array<module(Mappings.Mapping)> = [module(UIFragmentRegistryMapping)]
|