@reventlessdev/reventless-local 3.0.0-alpha.142 → 3.0.0-alpha.143
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 +14 -0
- package/package.json +8 -8
- package/src/Platform.res +252 -110
- package/src/Platform.res.mjs +522 -166
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res +3 -3
- package/src/components/Aggregate_Builder.res +1 -0
- package/src/components/Aggregate_Builder.res.mjs +5 -0
- package/tests/PluginEventDecodeTest.res +47 -0
- package/tests/PluginEventDecodeTest.res.mjs +33 -0
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +6 -7
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res.mjs +2 -3
- package/tests/plugin/ApiFragmentRegistry_GWT.res +103 -0
- package/tests/plugin/ApiFragmentRegistry_GWT.res.mjs +196 -0
- package/tests/plugin/ApiFragments_GWT.res +103 -0
- package/tests/plugin/ApiFragments_GWT.res.mjs +170 -0
- package/tests/plugin/PluginBehavior_GWT.res +2 -18
- package/tests/plugin/PluginBehavior_GWT.res.mjs +0 -37
- package/tests/plugin/Plugin_Fixtures.res +0 -7
- package/tests/plugin/Plugin_Fixtures.res.mjs +0 -21
- package/tests/plugin/PluginsProjection_GWT.res +0 -12
- package/tests/plugin/PluginsProjection_GWT.res.mjs +0 -19
- package/tests/plugin/UiFragmentRegistry_GWT.res +53 -0
- package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +92 -0
- package/tests/plugin/UiFragments_GWT.res +55 -0
- package/tests/plugin/UiFragments_GWT.res.mjs +84 -0
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res +0 -72
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res.mjs +0 -117
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.143 (2026-07-12)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **admin:** event-source the API fragment registry as admin DCB slices (Phase 2b) ([3fc40d2](https://github.com/ReventlessDev/reventless-core/commit/3fc40d247c54bf66ed84d625959f08299aa04a98))
|
|
11
|
+
* **admin:** event-source the UI fragment registry as admin DCB slices ([711581e](https://github.com/ReventlessDev/reventless-core/commit/711581e77c626e7d0fc35db8ec351f62a70bd8f2))
|
|
12
|
+
* **admin:** expose the API fragment registry as system-callable mutations (Phase 2d) ([8b1ec49](https://github.com/ReventlessDev/reventless-core/commit/8b1ec49813e67f303bc86d60470c9b95d2a8408e))
|
|
13
|
+
* **admin:** make the API fragment registry per-target (Domain | Platform) ([30491a9](https://github.com/ReventlessDev/reventless-core/commit/30491a9e14b4236c98cc756efb6de68ede1e77d7))
|
|
14
|
+
* **admin:** populate the API fragment registry on connect + status query (Phase 2c) ([106d96c](https://github.com/ReventlessDev/reventless-core/commit/106d96c904e135f8360d53e582f395c107db704a))
|
|
15
|
+
* **admin:** retire the Plugin-aggregate UI-fragment path in favour of the registry slices ([1dbc708](https://github.com/ReventlessDev/reventless-core/commit/1dbc708e7439b34ff970cc3d963d7835a8c6fd48))
|
|
16
|
+
* **admin:** serve Platform_UIFragments from the new UiFragmentRegistry slices ([7a47533](https://github.com/ReventlessDev/reventless-core/commit/7a47533acc95977fbeeb6375e94449056b655abc))
|
|
17
|
+
* **api:** neutralize core-emitted SDL — [@aws](https://github.com/aws)_subscribe moves to the AWS adapter (Phase 2a) ([9b27ec8](https://github.com/ReventlessDev/reventless-core/commit/9b27ec8f6ab2b7453b1c50b9891e89781ec3b31a))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.142 (2026-07-11)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.143",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
"sury": "11.0.0-alpha.4",
|
|
33
33
|
"ws": "^8.18.0",
|
|
34
34
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.27",
|
|
35
|
-
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.18",
|
|
36
35
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.20",
|
|
37
36
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.7",
|
|
38
37
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
|
|
39
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
40
|
-
"@reventlessdev/
|
|
41
|
-
"@reventlessdev/reventless-
|
|
42
|
-
"@reventlessdev/reventless-
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
38
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.157",
|
|
39
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.18",
|
|
40
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.103",
|
|
41
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.4",
|
|
42
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.98",
|
|
43
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.21",
|
|
44
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.76"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"rescript": "^12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -44,6 +44,39 @@ let decodePluginEventEnvelope = (eventJson: JSON.t): option<ReventlessCore.Plugi
|
|
|
44
44
|
| exception _ => None
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Decode a UiFragmentRegistry slice event from the admin DcbEventLog's published
|
|
48
|
+
// bus envelope — same {id, meta, event} shape as the aggregate topic above
|
|
49
|
+
// (DcbEventLog_Operations publishes via Message.composeEventJson'). Top-level,
|
|
50
|
+
// not inside the functor, so the decode is unit-testable.
|
|
51
|
+
let decodeUiFragmentRegistryEventEnvelope = (
|
|
52
|
+
eventJson: JSON.t,
|
|
53
|
+
): option<ReventlessCore.UiFragmentRegistry.event> =>
|
|
54
|
+
switch eventJson
|
|
55
|
+
->JSON.Decode.object
|
|
56
|
+
->Option.flatMap(d => d->Dict.get("event"))
|
|
57
|
+
->Option.map(payload =>
|
|
58
|
+
payload->S.parseJsonOrThrow(ReventlessCore.UiFragmentRegistry.eventSchema)
|
|
59
|
+
) {
|
|
60
|
+
| result => result
|
|
61
|
+
| exception _ => None
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Same envelope shape for the ApiFragmentRegistry slice's events on the shared
|
|
65
|
+
// admin DcbEventLog topic. Returns None for envelopes of other admin slices
|
|
66
|
+
// (schema mismatch), so a single subscriber can try both decoders.
|
|
67
|
+
let decodeApiFragmentRegistryEventEnvelope = (
|
|
68
|
+
eventJson: JSON.t,
|
|
69
|
+
): option<ReventlessCore.ApiFragmentRegistry.event> =>
|
|
70
|
+
switch eventJson
|
|
71
|
+
->JSON.Decode.object
|
|
72
|
+
->Option.flatMap(d => d->Dict.get("event"))
|
|
73
|
+
->Option.map(payload =>
|
|
74
|
+
payload->S.parseJsonOrThrow(ReventlessCore.ApiFragmentRegistry.eventSchema)
|
|
75
|
+
) {
|
|
76
|
+
| result => result
|
|
77
|
+
| exception _ => None
|
|
78
|
+
}
|
|
79
|
+
|
|
47
80
|
// Configurable platform — set silent=true to suppress diagnostic warnings in tests,
|
|
48
81
|
// splitApi=true to serve core and plugin APIs on separate ports,
|
|
49
82
|
// backend=Backend.Sqlite({...}) to opt into file-backed SQLite persistence.
|
|
@@ -190,6 +223,8 @@ module MakeWithConfig = (
|
|
|
190
223
|
schedulerRoleUrn: ref(Pulumi.Output.make("")),
|
|
191
224
|
api: ref(None),
|
|
192
225
|
apiRole: ref(None),
|
|
226
|
+
// In-memory has a single schema (no split Domain/Platform API), so admin resolvers share `api`.
|
|
227
|
+
adminApi: ref(None),
|
|
193
228
|
deployTarget: ref("Domain"),
|
|
194
229
|
// Phase 1: register SDL + resolver stub synchronously.
|
|
195
230
|
// Pass the resolved server so the correct target (domain or platform) receives the schema.
|
|
@@ -634,6 +669,26 @@ module MakeWithConfig = (
|
|
|
634
669
|
StateViewSliceMaker.Make(Spec, Projection)
|
|
635
670
|
}
|
|
636
671
|
|
|
672
|
+
// Admin fragment registry slices (docs/plans/event-sourced-fragment-registries.md): the
|
|
673
|
+
// platform UI- and API-fragment registries hosted as admin DCB slices sharing the admin
|
|
674
|
+
// DcbEventLog. Built once and passed into Admin.construct below.
|
|
675
|
+
module UiFragmentRegistrySlice = StateChangeSlice.Make(
|
|
676
|
+
ReventlessCore.UiFragmentRegistry,
|
|
677
|
+
ReventlessCore.UiFragmentRegistry_Behavior,
|
|
678
|
+
)
|
|
679
|
+
module UiFragmentsViewSlice = StateViewSlice.Make(
|
|
680
|
+
ReventlessCore.UiFragments,
|
|
681
|
+
ReventlessCore.UiFragments_Projection,
|
|
682
|
+
)
|
|
683
|
+
module ApiFragmentRegistrySlice = StateChangeSlice.Make(
|
|
684
|
+
ReventlessCore.ApiFragmentRegistry,
|
|
685
|
+
ReventlessCore.ApiFragmentRegistry_Behavior,
|
|
686
|
+
)
|
|
687
|
+
module ApiFragmentsViewSlice = StateViewSlice.Make(
|
|
688
|
+
ReventlessCore.ApiFragments,
|
|
689
|
+
ReventlessCore.ApiFragments_Projection,
|
|
690
|
+
)
|
|
691
|
+
|
|
637
692
|
// In-memory has no DynamoDB streams — stream variant is identical to plain StateViewSlice.
|
|
638
693
|
module StateViewSliceStream = {
|
|
639
694
|
module Make = (
|
|
@@ -674,6 +729,7 @@ module MakeWithConfig = (
|
|
|
674
729
|
mutations: [],
|
|
675
730
|
queries: [],
|
|
676
731
|
subscriptions: [],
|
|
732
|
+
subscriptionSources: [],
|
|
677
733
|
})
|
|
678
734
|
|
|
679
735
|
module Api = {
|
|
@@ -809,35 +865,6 @@ module MakeWithConfig = (
|
|
|
809
865
|
// by dispatching a synthetic `Connect(pluginDefinition)` to LocalPluginAggregate
|
|
810
866
|
// (see connectPlugin).
|
|
811
867
|
|
|
812
|
-
// UIFragmentRegistry QueryDb store ops — initialized lazily on first seed call.
|
|
813
|
-
let uiFragmentQueryDbOpsRef: ref<option<ReventlessCore.QueryDb_Adapter.operations>> = ref(None)
|
|
814
|
-
|
|
815
|
-
// Backend-aware storage for the UIFragment registry — the same functor every
|
|
816
|
-
// other read model uses. Replaces a hand-rolled memory-only store that ignored
|
|
817
|
-
// BackendState, so the registry now persists under SQLite too.
|
|
818
|
-
module UIFragmentStorage = LocalQueryDbStorage.Make(Bus)
|
|
819
|
-
|
|
820
|
-
let ensureUIFragmentRegistryQueryDbStore = () => {
|
|
821
|
-
switch uiFragmentQueryDbOpsRef.contents {
|
|
822
|
-
| Some(ops) => ops
|
|
823
|
-
| None =>
|
|
824
|
-
let name = ReventlessCore.UIFragmentRegistryReadModelSpec.name
|
|
825
|
-
// `make` registers the ops (and scan/stream) with the Bus for the active
|
|
826
|
-
// backend; retrieve them synchronously via getQueryDb rather than resolving
|
|
827
|
-
// the Pulumi Output.
|
|
828
|
-
let _ = UIFragmentStorage.make(
|
|
829
|
-
~name,
|
|
830
|
-
~indexes=[],
|
|
831
|
-
~api=(),
|
|
832
|
-
~apiRole=(),
|
|
833
|
-
~opts=({}: Pulumi.CustomResourceOptions.t),
|
|
834
|
-
)
|
|
835
|
-
let ops = Bus.getQueryDb(name)->Option.getOrThrow
|
|
836
|
-
uiFragmentQueryDbOpsRef := Some(ops)
|
|
837
|
-
ops
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
868
|
// In-memory store for plugin structures, keyed by plugin ID.
|
|
842
869
|
let pluginStructuresStore: ref<dict<Reventless.Plugin.pluginStructure>> = ref(Dict.make())
|
|
843
870
|
|
|
@@ -876,12 +903,95 @@ module MakeWithConfig = (
|
|
|
876
903
|
Bus.dispatchCommand(pluginCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
|
|
877
904
|
}
|
|
878
905
|
|
|
906
|
+
// Bus key for the shared admin DCB command topic, derived exactly as
|
|
907
|
+
// Dcb_Builder + CommandTopic_Builder name the channel:
|
|
908
|
+
// "Admin" -> +"Dcb" -> +CmdTopic ("AdminDcbCmdTopic").
|
|
909
|
+
let adminDcbCmdTopicKey = {
|
|
910
|
+
module CT = ReventlessCore.ComponentType
|
|
911
|
+
"AdminDcb"->CT.name(CT.CommandTopic)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Dispatch a UiFragmentRegistry slice command in-process via the shared admin
|
|
915
|
+
// DCB command topic. The DCB filtering handler routes by the command's variant
|
|
916
|
+
// tag to the slice handler registered by Admin.construct's DcbBuilder. The real
|
|
917
|
+
// platform routes RegisterUiFragment through the admin PluginExtensionPoint's
|
|
918
|
+
// UI-fragment mapping; the local platform has no admin EP component (Connect is
|
|
919
|
+
// likewise dispatched straight to the Plugin aggregate), so this is the local
|
|
920
|
+
// analogue of that mapping's PublishCommand. Keyed by bare plugin NAME —
|
|
921
|
+
// dispatchCommand parks until the slice handler registers, so call-before-wire
|
|
922
|
+
// is safe.
|
|
923
|
+
let dispatchUiFragmentCommand = (
|
|
924
|
+
~pluginName: string,
|
|
925
|
+
~command: ReventlessCore.UiFragmentRegistry.command,
|
|
926
|
+
) => {
|
|
927
|
+
let cmdJson: Reventless.Message.commandJson = {
|
|
928
|
+
id: pluginName,
|
|
929
|
+
meta: ReventlessCore.Message.generateMeta(~service=ReventlessCore.UiFragmentRegistry.name),
|
|
930
|
+
commandJson: command->S.reverseConvertToJsonOrThrow(
|
|
931
|
+
ReventlessCore.UiFragmentRegistry.commandSchema,
|
|
932
|
+
),
|
|
933
|
+
}
|
|
934
|
+
Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Same in-process path for the ApiFragmentRegistry slice (the API-schema
|
|
938
|
+
// fragment registry). On the real platform the deploy is the caller (SigV4
|
|
939
|
+
// RegisterApiFragment mutation); locally we dispatch straight to the shared
|
|
940
|
+
// admin DCB command topic, the analogue used for the UI registry above.
|
|
941
|
+
let dispatchApiFragmentCommand = (
|
|
942
|
+
~pluginName: string,
|
|
943
|
+
~command: ReventlessCore.ApiFragmentRegistry.command,
|
|
944
|
+
) => {
|
|
945
|
+
let cmdJson: Reventless.Message.commandJson = {
|
|
946
|
+
id: pluginName,
|
|
947
|
+
meta: ReventlessCore.Message.generateMeta(~service=ReventlessCore.ApiFragmentRegistry.name),
|
|
948
|
+
commandJson: command->S.reverseConvertToJsonOrThrow(
|
|
949
|
+
ReventlessCore.ApiFragmentRegistry.commandSchema,
|
|
950
|
+
),
|
|
951
|
+
}
|
|
952
|
+
Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Platform_ApiFragments status-query resolver — scans the ApiFragments
|
|
956
|
+
// StateViewSlice QueryDb (populated by the ApiFragmentRegistry slice's
|
|
957
|
+
// projection) and encodes via the shared Platform_ApiFragmentsApi encoder, so
|
|
958
|
+
// the deploy-facing status shape matches AWS. Keyed by bare plugin name (one
|
|
959
|
+
// row per plugin), so no version collapse is needed. Registered at each admin
|
|
960
|
+
// server site below.
|
|
961
|
+
let apiFragmentsQueryResolver = async (_root, _args, _ctx): JSON.t => {
|
|
962
|
+
let items = switch Bus.getQueryDbScan(ReventlessCore.ApiFragments.name) {
|
|
963
|
+
| Some(scanAll) => scanAll()
|
|
964
|
+
| None => []
|
|
965
|
+
}
|
|
966
|
+
items
|
|
967
|
+
->Array.filterMap(item =>
|
|
968
|
+
switch item->S.parseOrThrow(ReventlessCore.ApiFragments.stateSchema) {
|
|
969
|
+
| state => Some(ReventlessCore.Platform_ApiFragmentsApi.encodeApiFragmentEntry(state))
|
|
970
|
+
| exception _ => None
|
|
971
|
+
}
|
|
972
|
+
)
|
|
973
|
+
->JSON.Encode.array
|
|
974
|
+
}
|
|
975
|
+
|
|
879
976
|
// Subscription topics for the admin live-update channels.
|
|
880
977
|
let pluginStatusSubTopic = "onPluginStatusChange"
|
|
881
978
|
let uiFragmentSubTopic = "onUIFragmentChange"
|
|
882
979
|
|
|
883
|
-
//
|
|
884
|
-
//
|
|
980
|
+
// Bus key + service identity of the admin DcbEventLog's event topic. The
|
|
981
|
+
// Platform_Admin construct name is "Admin"; DcbEventLog_Builder hands that name
|
|
982
|
+
// to EventTopic_Builder, whose publisher registers under `<name>EventTopic`,
|
|
983
|
+
// and DcbEventLog_Operations stamps every published event's meta.service with
|
|
984
|
+
// `<name>DcbEventLog`.
|
|
985
|
+
let adminDcbEventTopicKey = {
|
|
986
|
+
module CT = ReventlessCore.ComponentType
|
|
987
|
+
"Admin"->CT.name(CT.EventTopic)
|
|
988
|
+
}
|
|
989
|
+
let adminDcbServiceName = "AdminDcbEventLog"
|
|
990
|
+
|
|
991
|
+
// Relocated Source-C emission: subscribe to the Plugin aggregate's event topic
|
|
992
|
+
// (lifecycle status) and the admin DcbEventLog event topic (UiFragmentRegistry
|
|
993
|
+
// slice events — the Plugin aggregate no longer emits UIFragment* events) and
|
|
994
|
+
// fan each event out to the matching GraphQL subscription. Status
|
|
885
995
|
// flows from folding aggregate events (Connect / Activate / Deactivate / Retire),
|
|
886
996
|
// not from direct QueryDb writes, so the subscription emission lives here rather
|
|
887
997
|
// than inside the mutation resolvers. Idempotent: registered once per platform
|
|
@@ -923,37 +1033,68 @@ module MakeWithConfig = (
|
|
|
923
1033
|
| Some(VersionDisconnected(def)) => publishStatus(~name=def.name, ~status="Disconnected")
|
|
924
1034
|
| Some(VersionDeactivated(def)) => publishStatus(~name=def.name, ~status="Inactive")
|
|
925
1035
|
| Some(VersionRetired(def)) => publishStatus(~name=def.name, ~status="Retired")
|
|
926
|
-
| Some(UIFragmentRegistered(data)) =>
|
|
927
|
-
publishUIFragment(
|
|
928
|
-
~name=ReventlessCore.Plugin.name(data.pluginId),
|
|
929
|
-
~changeKind="Registered",
|
|
930
|
-
~manifest=manifestJson(data.manifest),
|
|
931
|
-
)
|
|
932
|
-
| Some(UIFragmentUpdated(data)) =>
|
|
933
|
-
publishUIFragment(
|
|
934
|
-
~name=ReventlessCore.Plugin.name(data.pluginId),
|
|
935
|
-
~changeKind="Updated",
|
|
936
|
-
~manifest=manifestJson(data.newManifest),
|
|
937
|
-
)
|
|
938
|
-
| Some(UIFragmentDeregistered(data)) =>
|
|
939
|
-
publishUIFragment(
|
|
940
|
-
~name=ReventlessCore.Plugin.name(data.pluginId),
|
|
941
|
-
~changeKind="Deregistered",
|
|
942
|
-
~manifest=JSON.Encode.null,
|
|
943
|
-
)
|
|
944
1036
|
| Some(VersionDetected(_))
|
|
945
1037
|
| Some(VersionSuperseded(_))
|
|
946
1038
|
| Some(IncompatiblePluginDetected(_))
|
|
947
1039
|
| None => ()
|
|
948
1040
|
}
|
|
949
1041
|
})
|
|
1042
|
+
// UI-fragment live updates — sourced from the UiFragmentRegistry slice's
|
|
1043
|
+
// events on the shared admin DcbEventLog topic. The slice's `pluginId` is
|
|
1044
|
+
// already the bare plugin name (the EP mapping keys the registry by name).
|
|
1045
|
+
// The service guard keeps this decoder scoped to DcbEventLog-published
|
|
1046
|
+
// envelopes as further admin slices join the shared log.
|
|
1047
|
+
Bus.subscribeToEvents(adminDcbEventTopicKey, async (service, _meta, eventJson) =>
|
|
1048
|
+
if service == adminDcbServiceName {
|
|
1049
|
+
switch decodeUiFragmentRegistryEventEnvelope(eventJson) {
|
|
1050
|
+
| Some(UiFragmentRegistered({pluginId, manifest})) =>
|
|
1051
|
+
publishUIFragment(
|
|
1052
|
+
~name=pluginId,
|
|
1053
|
+
~changeKind="Registered",
|
|
1054
|
+
~manifest=manifestJson(manifest),
|
|
1055
|
+
)
|
|
1056
|
+
| Some(UiFragmentUpdated({pluginId, newManifest})) =>
|
|
1057
|
+
publishUIFragment(
|
|
1058
|
+
~name=pluginId,
|
|
1059
|
+
~changeKind="Updated",
|
|
1060
|
+
~manifest=manifestJson(newManifest),
|
|
1061
|
+
)
|
|
1062
|
+
| Some(UiFragmentDeregistered({pluginId})) =>
|
|
1063
|
+
publishUIFragment(~name=pluginId, ~changeKind="Deregistered", ~manifest=JSON.Encode.null)
|
|
1064
|
+
| None => ()
|
|
1065
|
+
}
|
|
1066
|
+
// Local single writer for the API-schema fragment registry. On AWS the
|
|
1067
|
+
// subscriber stitches + pushes the schema then records the outcome; on
|
|
1068
|
+
// local the plugin's fields are already served by resolver registration,
|
|
1069
|
+
// so the "push" is a no-op and we simply record success — the honest
|
|
1070
|
+
// status for an in-process schema. Only Registered/Updated trigger a
|
|
1071
|
+
// record (never PushRecorded — no re-entrant loop) and the record's own
|
|
1072
|
+
// event decodes here as a no-op.
|
|
1073
|
+
switch decodeApiFragmentRegistryEventEnvelope(eventJson) {
|
|
1074
|
+
| Some(ApiFragmentRegistered({pluginId}))
|
|
1075
|
+
| Some(ApiFragmentUpdated({pluginId})) =>
|
|
1076
|
+
let _ = dispatchApiFragmentCommand(
|
|
1077
|
+
~pluginName=pluginId,
|
|
1078
|
+
~command=ReventlessCore.ApiFragmentRegistry.RecordApiFragmentPush({
|
|
1079
|
+
pluginId,
|
|
1080
|
+
ok: true,
|
|
1081
|
+
message: "",
|
|
1082
|
+
at: Date.make()->Date.toISOString,
|
|
1083
|
+
}),
|
|
1084
|
+
)
|
|
1085
|
+
| Some(ApiFragmentDeregistered(_))
|
|
1086
|
+
| Some(ApiFragmentPushRecorded(_))
|
|
1087
|
+
| None => ()
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
)
|
|
950
1091
|
}
|
|
951
1092
|
|
|
952
1093
|
// Drive the admin Plugin read models the same way AWS does: build a
|
|
953
1094
|
// `pluginDefinition` from the constructed plugin component outputs and dispatch a
|
|
954
1095
|
// synthetic `Connect(def)` to LocalPluginAggregate, keyed by the plugin NAME (the
|
|
955
1096
|
// Plugin aggregate is name-keyed). The Plugin aggregate emits `VersionConnected`
|
|
956
|
-
// (+ `
|
|
1097
|
+
// (+ `VersionSuperseded`) on its event topic; the real
|
|
957
1098
|
// PluginsProjection subscription (registered by
|
|
958
1099
|
// Admin.construct ~readModels) folds those events into the "Plugins" current view
|
|
959
1100
|
// QueryDb store. This replaces the previous
|
|
@@ -1015,7 +1156,6 @@ module MakeWithConfig = (
|
|
|
1015
1156
|
extensionProtocols: [],
|
|
1016
1157
|
apiSchemaFragment,
|
|
1017
1158
|
apiTarget: None,
|
|
1018
|
-
uiFragments,
|
|
1019
1159
|
structure: pluginStructure,
|
|
1020
1160
|
dcbEventLog: None,
|
|
1021
1161
|
kind: Domain,
|
|
@@ -1025,34 +1165,39 @@ module MakeWithConfig = (
|
|
|
1025
1165
|
~pluginName,
|
|
1026
1166
|
~command=ReventlessCore.PluginSpec.Connect(pluginDefinition),
|
|
1027
1167
|
)
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
// Seed the UIFragmentRegistry QueryDb from plugin outputs that carry a uiFragments manifest.
|
|
1033
|
-
let seedUIFragmentRegistryQueryDb = (~pluginComponents: array<ReventlessCore.Plugin.component>) => {
|
|
1034
|
-
let uiFragmentOps = ensureUIFragmentRegistryQueryDbStore()
|
|
1035
|
-
pluginComponents->Array.forEach(plugin => {
|
|
1036
|
-
let outputs: ReventlessInfra.Plugin.outputs = plugin->ReventlessCore.Component.outputs
|
|
1037
|
-
let _ =
|
|
1038
|
-
(outputs.id, outputs.uiFragments)
|
|
1039
|
-
->Pulumi.Output.all2
|
|
1040
|
-
->Pulumi.Output.apply(((id, uiFragments)) => {
|
|
1168
|
+
// Register the UI-fragment manifest with the UiFragmentRegistry slice
|
|
1169
|
+
// (via the shared admin DCB command topic), so the UiFragments
|
|
1170
|
+
// StateViewSlice populates in-process. `at` mirrors the EP mapping's
|
|
1171
|
+
// command-meta timestamp threading.
|
|
1041
1172
|
switch uiFragments {
|
|
1042
1173
|
| Some(manifest) =>
|
|
1043
|
-
let
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1174
|
+
let _ = dispatchUiFragmentCommand(
|
|
1175
|
+
~pluginName,
|
|
1176
|
+
~command=ReventlessCore.UiFragmentRegistry.RegisterUiFragment({
|
|
1177
|
+
pluginId: pluginName,
|
|
1178
|
+
manifest,
|
|
1179
|
+
at: Date.make()->Date.toISOString,
|
|
1180
|
+
}),
|
|
1181
|
+
)
|
|
1182
|
+
| None => ()
|
|
1183
|
+
}
|
|
1184
|
+
// Register the API-schema fragment with the ApiFragmentRegistry slice.
|
|
1185
|
+
// On the real platform the deploy sends this as a SigV4 caller; locally
|
|
1186
|
+
// connect stands in. apiTarget is Domain (mirrors pluginDefinition.kind
|
|
1187
|
+
// = Domain / apiTarget = None above — the local platform has no
|
|
1188
|
+
// Platform-target plugins). The subscriber below records the push as ok,
|
|
1189
|
+
// since the fields are already served in-process by resolver registration.
|
|
1190
|
+
switch apiSchemaFragment {
|
|
1191
|
+
| Some(fragment) =>
|
|
1192
|
+
let _ = dispatchApiFragmentCommand(
|
|
1193
|
+
~pluginName,
|
|
1194
|
+
~command=ReventlessCore.ApiFragmentRegistry.RegisterApiFragment({
|
|
1195
|
+
pluginId: pluginName,
|
|
1196
|
+
fragment,
|
|
1197
|
+
apiTarget: Domain,
|
|
1198
|
+
at: Date.make()->Date.toISOString,
|
|
1199
|
+
}),
|
|
1200
|
+
)
|
|
1056
1201
|
| None => ()
|
|
1057
1202
|
}
|
|
1058
1203
|
})
|
|
@@ -1326,8 +1471,8 @@ module MakeWithConfig = (
|
|
|
1326
1471
|
~resourceNaming=LocalPluginSpec.resourceNaming,
|
|
1327
1472
|
~api=(),
|
|
1328
1473
|
~apiRole=(),
|
|
1329
|
-
~stateChangeSlices=[],
|
|
1330
|
-
~stateViewSlices=[],
|
|
1474
|
+
~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
|
|
1475
|
+
~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
|
|
1331
1476
|
~automationSlices=[],
|
|
1332
1477
|
~outboundTranslationSlices=[],
|
|
1333
1478
|
~inboundTranslationSlices=[],
|
|
@@ -1409,7 +1554,6 @@ module MakeWithConfig = (
|
|
|
1409
1554
|
// chains, so deferring their invocation changes no synchronous contract.
|
|
1410
1555
|
let seedAdminStores = () => {
|
|
1411
1556
|
connectPlugin(~pluginComponents=plugins)
|
|
1412
|
-
seedUIFragmentRegistryQueryDb(~pluginComponents=plugins)
|
|
1413
1557
|
seedPluginStructuresStore(~pluginComponents=plugins)
|
|
1414
1558
|
}
|
|
1415
1559
|
switch (projectionCatchup, pgProjectionCatchup) {
|
|
@@ -1455,7 +1599,7 @@ module MakeWithConfig = (
|
|
|
1455
1599
|
)
|
|
1456
1600
|
|
|
1457
1601
|
let pluginQueryDbName = ReventlessCore.PluginsReadModelSpec.name
|
|
1458
|
-
let uiFragmentQueryDbName = ReventlessCore.
|
|
1602
|
+
let uiFragmentQueryDbName = ReventlessCore.UiFragments.name
|
|
1459
1603
|
|
|
1460
1604
|
// Wire the per-mutation plugin-status gate (Part 2.3 of the resolver plan).
|
|
1461
1605
|
// Resolves at call-time so updates to Plugin RM status are reflected immediately.
|
|
@@ -1605,10 +1749,10 @@ module MakeWithConfig = (
|
|
|
1605
1749
|
},
|
|
1606
1750
|
)
|
|
1607
1751
|
|
|
1608
|
-
// Platform_UIFragments resolver — reads the
|
|
1609
|
-
// (
|
|
1610
|
-
// the shared Platform_UIFragmentsApi encoder so AWS and
|
|
1611
|
-
// same JSON shape.
|
|
1752
|
+
// Platform_UIFragments resolver — reads the UiFragments StateViewSlice
|
|
1753
|
+
// QueryDb (populated by the UiFragmentRegistry slice's projection) and
|
|
1754
|
+
// encodes via the shared Platform_UIFragmentsApi encoder so AWS and
|
|
1755
|
+
// in-memory return the same JSON shape.
|
|
1612
1756
|
queryResolvers->Dict.set(
|
|
1613
1757
|
"Platform_UIFragments",
|
|
1614
1758
|
async (_root, _args, _ctx): JSON.t => {
|
|
@@ -1616,15 +1760,13 @@ module MakeWithConfig = (
|
|
|
1616
1760
|
| Some(scanAll) => scanAll()
|
|
1617
1761
|
| None => []
|
|
1618
1762
|
}
|
|
1619
|
-
// Collapse to one entry per plugin name (highest version).
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1763
|
+
// Collapse to one entry per plugin name (highest version). The registry is
|
|
1764
|
+
// keyed by bare plugin name now (a no-op collapse), but rows persisted by
|
|
1765
|
+
// the pre-slice registry were keyed name@version — keep the dedup so a
|
|
1766
|
+
// mixed store never surfaces duplicates (mirrors Platform_UIFragments_Lambda.res).
|
|
1623
1767
|
let latestByName = Dict.make()
|
|
1624
1768
|
items->Array.forEach(item =>
|
|
1625
|
-
switch item->S.parseOrThrow(
|
|
1626
|
-
ReventlessCore.UIFragmentRegistryReadModelSpec.stateSchema,
|
|
1627
|
-
) {
|
|
1769
|
+
switch item->S.parseOrThrow(ReventlessCore.UiFragments.stateSchema) {
|
|
1628
1770
|
| state =>
|
|
1629
1771
|
let name = ReventlessCore.Plugin.name(state.pluginId)
|
|
1630
1772
|
let version = ReventlessCore.Plugin.version(state.pluginId)
|
|
@@ -1646,6 +1788,7 @@ module MakeWithConfig = (
|
|
|
1646
1788
|
->JSON.Encode.array
|
|
1647
1789
|
},
|
|
1648
1790
|
)
|
|
1791
|
+
queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
|
|
1649
1792
|
|
|
1650
1793
|
// Composite-key / index admin query fields. Live loads from the seeded QueryDb stores.
|
|
1651
1794
|
registerAdminItemsAndIndexResolvers(~queryResolvers, ~live=true)
|
|
@@ -1730,8 +1873,9 @@ module MakeWithConfig = (
|
|
|
1730
1873
|
}
|
|
1731
1874
|
)
|
|
1732
1875
|
// UIFragment admin mutations — explicit admin-triggered fragment changes still
|
|
1733
|
-
// publish onUIFragmentChange directly (
|
|
1734
|
-
// emitted by subscribeToPluginEvents). uiFragmentSubTopic
|
|
1876
|
+
// publish onUIFragmentChange directly (slice-driven fragment changes are
|
|
1877
|
+
// emitted by subscribeToPluginEvents' admin-DCB subscription). uiFragmentSubTopic
|
|
1878
|
+
// is the shared topic.
|
|
1735
1879
|
let makeUIEvent = (~pluginId, ~changeKind, ~manifest) =>
|
|
1736
1880
|
JSON.Encode.object(
|
|
1737
1881
|
Dict.fromArray([
|
|
@@ -1769,10 +1913,11 @@ module MakeWithConfig = (
|
|
|
1769
1913
|
)
|
|
1770
1914
|
adminRegisteredServers.contents->Array.push(platformGraphQL)
|
|
1771
1915
|
// Register onUIFragmentChange + onPluginStatusChange subscriptions (Source C).
|
|
1916
|
+
// The SDL field constants are provider-neutral, shared with the AWS path.
|
|
1772
1917
|
platformGraphQL.registerSubscriptions(
|
|
1773
1918
|
~sdlFields=[
|
|
1774
|
-
|
|
1775
|
-
|
|
1919
|
+
ReventlessCore.AdminApi.uiFragmentSubscriptionField,
|
|
1920
|
+
ReventlessCore.AdminApi.pluginStatusSubscriptionField,
|
|
1776
1921
|
],
|
|
1777
1922
|
~resolvers=Dict.fromArray([
|
|
1778
1923
|
(
|
|
@@ -1892,8 +2037,8 @@ module MakeWithConfig = (
|
|
|
1892
2037
|
~resourceNaming=LocalPluginSpec.resourceNaming,
|
|
1893
2038
|
~api=(),
|
|
1894
2039
|
~apiRole=(),
|
|
1895
|
-
~stateChangeSlices=[],
|
|
1896
|
-
~stateViewSlices=[],
|
|
2040
|
+
~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
|
|
2041
|
+
~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
|
|
1897
2042
|
~automationSlices=[],
|
|
1898
2043
|
~outboundTranslationSlices=[],
|
|
1899
2044
|
~inboundTranslationSlices=[],
|
|
@@ -1925,6 +2070,7 @@ module MakeWithConfig = (
|
|
|
1925
2070
|
"Platform_UIFragments",
|
|
1926
2071
|
async (_root, _args, _ctx): JSON.t => JSON.Encode.array([]),
|
|
1927
2072
|
)
|
|
2073
|
+
queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
|
|
1928
2074
|
// Composite-key / index admin query fields — empty stubs in the platform-only
|
|
1929
2075
|
// path (no plugins connected → no QueryDb seeded).
|
|
1930
2076
|
registerAdminItemsAndIndexResolvers(~queryResolvers, ~live=false)
|
|
@@ -2058,8 +2204,8 @@ module MakeWithConfig = (
|
|
|
2058
2204
|
~resourceNaming=LocalPluginSpec.resourceNaming,
|
|
2059
2205
|
~api=(),
|
|
2060
2206
|
~apiRole=(),
|
|
2061
|
-
~stateChangeSlices=[],
|
|
2062
|
-
~stateViewSlices=[],
|
|
2207
|
+
~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
|
|
2208
|
+
~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
|
|
2063
2209
|
~automationSlices=[],
|
|
2064
2210
|
~outboundTranslationSlices=[],
|
|
2065
2211
|
~inboundTranslationSlices=[],
|
|
@@ -2099,23 +2245,19 @@ module MakeWithConfig = (
|
|
|
2099
2245
|
queryResolvers->Dict.set(adminQueryEntry.listFieldName, async (_root, _args, _ctx): JSON.t =>
|
|
2100
2246
|
connectionResponse([])
|
|
2101
2247
|
)
|
|
2102
|
-
// Platform_UIFragments — single-plugin path.
|
|
2103
|
-
//
|
|
2104
|
-
//
|
|
2248
|
+
// Platform_UIFragments — single-plugin path. Populated by the
|
|
2249
|
+
// UiFragments StateViewSlice when the plugin registers a manifest through
|
|
2250
|
+
// the admin EP (connectPlugin below), otherwise scans an empty store.
|
|
2105
2251
|
queryResolvers->Dict.set(
|
|
2106
2252
|
"Platform_UIFragments",
|
|
2107
2253
|
async (_root, _args, _ctx): JSON.t => {
|
|
2108
|
-
let items = switch Bus.getQueryDbScan(
|
|
2109
|
-
ReventlessCore.UIFragmentRegistryReadModelSpec.name,
|
|
2110
|
-
) {
|
|
2254
|
+
let items = switch Bus.getQueryDbScan(ReventlessCore.UiFragments.name) {
|
|
2111
2255
|
| Some(scanAll) => scanAll()
|
|
2112
2256
|
| None => []
|
|
2113
2257
|
}
|
|
2114
2258
|
items
|
|
2115
2259
|
->Array.filterMap(item =>
|
|
2116
|
-
switch item->S.parseOrThrow(
|
|
2117
|
-
ReventlessCore.UIFragmentRegistryReadModelSpec.stateSchema,
|
|
2118
|
-
) {
|
|
2260
|
+
switch item->S.parseOrThrow(ReventlessCore.UiFragments.stateSchema) {
|
|
2119
2261
|
| state =>
|
|
2120
2262
|
Some(ReventlessCore.Platform_UIFragmentsApi.encodeUIFragmentEntry(state))
|
|
2121
2263
|
| exception _ => None
|
|
@@ -2124,6 +2266,7 @@ module MakeWithConfig = (
|
|
|
2124
2266
|
->JSON.Encode.array
|
|
2125
2267
|
},
|
|
2126
2268
|
)
|
|
2269
|
+
queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
|
|
2127
2270
|
// Platform_ComponentDefinitions resolver — SDL is already stitched into baseParts via
|
|
2128
2271
|
// AdminApi.baseFragment so we register only the resolver here. Uses the shared
|
|
2129
2272
|
// encoder so the dynamic-plugin admin server emits the same canonical shape as
|
|
@@ -2204,7 +2347,6 @@ module MakeWithConfig = (
|
|
|
2204
2347
|
// Drive the admin Plugin read models via synthetic Connect dispatch through
|
|
2205
2348
|
// LocalPluginAggregate (replaces the old direct-write seed).
|
|
2206
2349
|
connectPlugin(~pluginComponents=[pluginComponent])
|
|
2207
|
-
seedUIFragmentRegistryQueryDb(~pluginComponents=[pluginComponent])
|
|
2208
2350
|
seedPluginStructuresStore(~pluginComponents=[pluginComponent])
|
|
2209
2351
|
|
|
2210
2352
|
// Fire onPluginDeployed hooks so subscribers learn about this plugin.
|