@reventlessdev/reventless-aws 3.0.0-alpha.197 → 3.0.0-alpha.199

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +7 -7
  3. package/src/Platform.res +294 -44
  4. package/src/Platform.res.mjs +420 -94
  5. package/src/adapter/Api/ApiFragmentDeregistration.res +137 -0
  6. package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +108 -0
  7. package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +4 -3
  8. package/src/adapter/Api/Platform_ApiFragments_Lambda.res +222 -0
  9. package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
  10. package/src/adapter/Api/Platform_UIFragments_Lambda.res +27 -17
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +13 -13
  12. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +2 -1
  13. package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
  14. package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +235 -9
  15. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +4 -0
  16. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +5 -0
  17. package/src/components/Api/AppSync_Adapter.res +36 -59
  18. package/src/components/Api/AppSync_Adapter.res.mjs +18 -74
  19. package/src/components/Api/AppSync_SdlDecorate.res +189 -0
  20. package/src/components/Api/AppSync_SdlDecorate.res.mjs +125 -0
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +107 -2
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +70 -7
  23. package/src/util/Util_AppSync_Caller.res +12 -4
  24. package/src/util/Util_AppSync_Caller.res.mjs +9 -3
  25. package/tests/AppSync_AdapterTest.res +6 -0
  26. package/tests/AppSync_AdapterTest.res.mjs +12 -6
  27. package/tests/AppSync_SdlDecorateTest.res +157 -0
  28. package/tests/AppSync_SdlDecorateTest.res.mjs +147 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
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.199 (2026-07-12)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **deploy:** gate admin status-query resolvers on the schema push ([2634694](https://github.com/ReventlessDev/reventless-core/commit/2634694b3a122bc1db44d129769f5442e124d9b7))
11
+
12
+
13
+ # 3.0.0-alpha.198 (2026-07-12)
14
+
15
+ ### Features
16
+
17
+ * **admin:** event-source the API fragment registry as admin DCB slices (Phase 2b) ([3fc40d2](https://github.com/ReventlessDev/reventless-core/commit/3fc40d247c54bf66ed84d625959f08299aa04a98))
18
+ * **admin:** event-source the UI fragment registry as admin DCB slices ([711581e](https://github.com/ReventlessDev/reventless-core/commit/711581e77c626e7d0fc35db8ec351f62a70bd8f2))
19
+ * **admin:** expose the API fragment registry as system-callable mutations (Phase 2d) ([8b1ec49](https://github.com/ReventlessDev/reventless-core/commit/8b1ec49813e67f303bc86d60470c9b95d2a8408e))
20
+ * **admin:** react to ApiFragment* events with a schema single writer (Phase 2e) ([59f9e46](https://github.com/ReventlessDev/reventless-core/commit/59f9e46c154d8ca2a34537974722014fc279de3e))
21
+ * **admin:** retire the Plugin-aggregate UI-fragment path in favour of the registry slices ([1dbc708](https://github.com/ReventlessDev/reventless-core/commit/1dbc708e7439b34ff970cc3d963d7835a8c6fd48))
22
+ * **admin:** serve Platform_UIFragments from the new UiFragmentRegistry slices ([7a47533](https://github.com/ReventlessDev/reventless-core/commit/7a47533acc95977fbeeb6375e94449056b655abc))
23
+ * **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))
24
+ * **deploy:** deregister API fragments on plugin-stack destroy (Phase 3 tail) ([4a10614](https://github.com/ReventlessDev/reventless-core/commit/4a10614e785359ca4c8021da9813c3d485abe9ca))
25
+ * **deploy:** register API fragments via the Platform API on staged deploys (Phase 3) ([7ddb045](https://github.com/ReventlessDev/reventless-core/commit/7ddb045392380497494a0109712e7a10008f9207))
26
+
27
+
6
28
  # 3.0.0-alpha.197 (2026-07-11)
7
29
 
8
30
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.197",
3
+ "version": "3.0.0-alpha.199",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -8,17 +8,17 @@
8
8
  "@aws-sdk/client-cloudfront": "3.970.0",
9
9
  "sury": "11.0.0-alpha.4",
10
10
  "uuid": "^13.0.0",
11
- "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.21",
11
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
12
12
  "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
13
+ "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.21",
13
14
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.50",
14
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
15
15
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
16
+ "@reventlessdev/reventless-core": "3.0.0-alpha.158",
16
17
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.15",
17
- "@reventlessdev/reventless-core": "3.0.0-alpha.156",
18
- "@reventlessdev/reventless-infra": "3.0.0-alpha.97",
18
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
19
19
  "@reventlessdev/reventless-interop": "3.0.0-alpha.26",
20
- "@reventlessdev/reventless-spec": "3.0.0-alpha.75",
21
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.20"
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.76",
21
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.22"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rescript": "^12.3.0",
package/src/Platform.res CHANGED
@@ -109,7 +109,13 @@ module MakeWithConfig = (
109
109
  subnetIds: pg.subnetIds,
110
110
  })
111
111
  QueryDbBackend.exempt(ReventlessCore.PluginsReadModelSpec.name)
112
- QueryDbBackend.exempt(ReventlessCore.UIFragmentRegistryReadModelSpec.name)
112
+ // The UiFragments StateViewSlice table is scanned by the Platform_UIFragments
113
+ // Lambda (DynamoDB scan) — keep it off the Postgres selection like the other
114
+ // admin stores.
115
+ QueryDbBackend.exempt(ReventlessCore.UiFragments.name)
116
+ // The ApiFragments StateViewSlice is the durable source of the schema-push
117
+ // automation and the runtime re-stitcher — admin store, off Postgres too.
118
+ QueryDbBackend.exempt(ReventlessCore.ApiFragments.name)
113
119
  })
114
120
  type api = Types.AppSync.api
115
121
  type role = Types.AppSync.role
@@ -567,6 +573,27 @@ module MakeWithConfig = (
567
573
  }
568
574
  module InboundTranslationSlice = InboundTranslationSlice_Builder.Make(ApiConfig)
569
575
 
576
+ // Admin UI-fragment registry slices (docs/plans/event-sourced-fragment-registries.md): the
577
+ // platform UI-fragment registry hosted as admin DCB slices sharing the admin DcbEventLog.
578
+ // Built once at platform-module level and passed into Admin.construct below (registers the
579
+ // slice's command handler exactly once).
580
+ module UiFragmentRegistrySlice = StateChangeSlice.Make(
581
+ ReventlessCore.UiFragmentRegistry,
582
+ ReventlessCore.UiFragmentRegistry_Behavior,
583
+ )
584
+ module UiFragmentsViewSlice = StateViewSlice.Make(
585
+ ReventlessCore.UiFragments,
586
+ ReventlessCore.UiFragments_Projection,
587
+ )
588
+ module ApiFragmentRegistrySlice = StateChangeSlice.Make(
589
+ ReventlessCore.ApiFragmentRegistry,
590
+ ReventlessCore.ApiFragmentRegistry_Behavior,
591
+ )
592
+ module ApiFragmentsViewSlice = StateViewSlice.Make(
593
+ ReventlessCore.ApiFragments,
594
+ ReventlessCore.ApiFragments_Projection,
595
+ )
596
+
570
597
  // Empty base fragment — no types, no mutations, no queries.
571
598
  // Used by the plugin Api in split mode so plugin schema has no core fields.
572
599
  let emptyBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
@@ -574,6 +601,7 @@ module MakeWithConfig = (
574
601
  mutations: [],
575
602
  queries: [],
576
603
  subscriptions: [],
604
+ subscriptionSources: [],
577
605
  })
578
606
 
579
607
  module Api = {
@@ -639,6 +667,11 @@ module MakeWithConfig = (
639
667
  // targetApi that was set before plugin.make() and is never reset.
640
668
  let hooksApiRef: ref<option<ReventlessCore.Plugin_Helpers.hookedValue<unknown>>> = ref(None)
641
669
  let hooksApiRoleRef: ref<option<ReventlessCore.Plugin_Helpers.hookedValue<unknown>>> = ref(None)
670
+ // Admin DCB mutation resolvers bind here (the Platform API in split mode). Set by
671
+ // makePlatform / deployPlatform once the Platform API resource exists. Distinct from hooksApiRef
672
+ // because both are read in *deferred* callbacks — admin resolvers need the Platform API while the
673
+ // plugins built afterwards read hooksApiRef (Domain/deploy-target). See platformHooks.adminApi.
674
+ let hooksAdminApiRef: ref<option<ReventlessCore.Plugin_Helpers.hookedValue<unknown>>> = ref(None)
642
675
 
643
676
  let resolveHookedApi = (): Types.AppSync.api =>
644
677
  switch hooksApiRef.contents {
@@ -646,6 +679,160 @@ module MakeWithConfig = (
646
679
  | None => resolveTargetApi()
647
680
  }
648
681
 
682
+ // Admin DCB resolvers resolve to the Platform API when it has been recorded; otherwise fall back
683
+ // to the same api plugins use (unified mode, or before the split Platform API exists).
684
+ let resolveAdminHookedApi = (): Types.AppSync.api =>
685
+ switch hooksAdminApiRef.contents {
686
+ | Some({val}) => Obj.magic(val)
687
+ | None => resolveHookedApi()
688
+ }
689
+
690
+ // ── Phase 3: staged-deploy API-fragment registration ────────────────────────
691
+ // When a plugin deploys against an ALREADY-RUNNING platform (plugin-stack mode),
692
+ // it registers its API-schema fragment with the platform via the Platform API
693
+ // (SigV4 system caller) instead of writing deploy-schema:* rows and pushing the
694
+ // schema itself. The platform-side reactive single writer (Plan 2e) stitches and
695
+ // pushes; this waits on the push status so resolver creation (gated on the
696
+ // returned Output) proceeds only once the plugin's fields are ACTIVE — or fails
697
+ // with the stitch error. Used only in plugin-stack mode; makePlatform (all-at-once)
698
+ // keeps the direct deploy-time push (the reactive writer is dormant there).
699
+ @val external registerWaiterSetTimeout: (unit => unit, int) => unit = "setTimeout"
700
+ let deployWaiterDelay = (ms: int): promise<unit> =>
701
+ Promise.make((resolve, _) => registerWaiterSetTimeout(() => resolve(), ms))
702
+
703
+ // Poll the platform's Platform_ApiFragments status query until the plugin's row
704
+ // shows a push that landed at/after our registration time (`sinceAt`) — a fresh
705
+ // "ok" means our fields are live; a fresh "error" surfaces the stitch failure. An
706
+ // older "ok" is a stale prior deploy's and is ignored (ISO strings sort lexically).
707
+ let waitForApiFragmentPush = async (~endpoint, ~region, ~pluginId, ~sinceAt) => {
708
+ let query = "query { Platform_ApiFragments { pluginId pushStatus pushMessage pushedAt } }"
709
+ let maxAttempts = 90 // ~3 min at 2s intervals
710
+ let rec loop = async attempt =>
711
+ if attempt >= maxAttempts {
712
+ JsError.throwWithMessage(
713
+ `Timed out (>3min) waiting for the platform to push ${pluginId}'s API fragment — check the AdminEventCollector Lambda logs.`,
714
+ )
715
+ } else {
716
+ let data = await Util_AppSync_Caller.sendQuery(~endpoint, ~region, ~queryString=query)
717
+ let row =
718
+ data
719
+ ->Option.flatMap(JSON.Decode.object)
720
+ ->Option.flatMap(d => d->Dict.get("Platform_ApiFragments"))
721
+ ->Option.flatMap(JSON.Decode.array)
722
+ ->Option.getOr([])
723
+ ->Array.find(entry =>
724
+ entry
725
+ ->JSON.Decode.object
726
+ ->Option.flatMap(e => e->Dict.get("pluginId"))
727
+ ->Option.flatMap(JSON.Decode.string) == Some(pluginId)
728
+ )
729
+ ->Option.flatMap(JSON.Decode.object)
730
+ switch row {
731
+ | Some(e) =>
732
+ let field = k => e->Dict.get(k)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
733
+ let status = field("pushStatus")
734
+ let fresh = field("pushedAt") >= sinceAt
735
+ if fresh && status == "ok" {
736
+ log.info(~comp="registerFragmentViaApi", `${pluginId} schema push confirmed ACTIVE`)
737
+ } else if fresh && status == "error" {
738
+ JsError.throwWithMessage(`Schema push failed for ${pluginId}: ${field("pushMessage")}`)
739
+ } else {
740
+ await deployWaiterDelay(2000)
741
+ await loop(attempt + 1)
742
+ }
743
+ | None =>
744
+ await deployWaiterDelay(2000)
745
+ await loop(attempt + 1)
746
+ }
747
+ }
748
+ await loop(0)
749
+ }
750
+
751
+ let registerFragmentViaApi = (
752
+ ~name: string,
753
+ ~fragment: Reventless.Plugin.apiSchemaFragment,
754
+ ~apiTargetName: string,
755
+ ): Pulumi.Output.t<unit> => {
756
+ // Platform_RegisterApiFragment is an admin systemCallable mutation on the Platform
757
+ // API (regardless of the plugin's apiTarget, which is passed as the enum argument).
758
+ let platformApi = switch apiConfigRef.contents {
759
+ | Some({platformApi}) => platformApi
760
+ | None => domainApi
761
+ }
762
+ let region =
763
+ Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown")
764
+ let endpointOutput =
765
+ platformApi
766
+ ->Pulumi.Output.flatMap(api => api.uris)
767
+ ->Pulumi.Output.apply(uris => uris.graphQL)
768
+ // Deregister-on-destroy: a dynamic resource whose `delete` handler sends
769
+ // Platform_DeregisterApiFragment when the plugin stack is destroyed (final
770
+ // retirement). It never replaces on a version bump, so supersession does NOT
771
+ // deregister — only a genuine `pulumi destroy` removes the fields.
772
+ let _ = ApiFragmentDeregistration.make(
773
+ ~name=`${name}ApiFragmentRegistration`,
774
+ ~pluginId=name,
775
+ ~endpoint=endpointOutput->Pulumi.Output.asInput,
776
+ ~region,
777
+ )
778
+ endpointOutput
779
+ ->Pulumi.Output.flatMap(endpoint => {
780
+ let run = async () => {
781
+ let at = Date.make()->Date.toISOString
782
+ let variables = {
783
+ "id": name,
784
+ "pluginId": name,
785
+ "fragment": {"encoded": fragment.encoded, "protocol": fragment.protocol},
786
+ "apiTarget": Util_AppSync_Caller.graphqlEnum(apiTargetName),
787
+ "at": at,
788
+ }
789
+ let selection = "{ __typename ... on CommandAccepted { eventCount } ... on CommandRejected { errorCode errorDetail } }"
790
+ log.info(
791
+ ~comp="registerFragmentViaApi",
792
+ `Registering API fragment for ${name} (${apiTargetName}) via ${endpoint}`,
793
+ )
794
+ let result = await Util_AppSync_Caller.sendMutation(
795
+ ~endpoint,
796
+ ~region,
797
+ ~mutation="Platform_RegisterApiFragment",
798
+ ~selection,
799
+ ~variables,
800
+ )
801
+ let outcome =
802
+ result
803
+ ->Option.flatMap(JSON.Decode.object)
804
+ ->Option.flatMap(d => d->Dict.get("Platform_RegisterApiFragment"))
805
+ ->Option.flatMap(JSON.Decode.object)
806
+ switch outcome {
807
+ | Some(cmd) =>
808
+ let typename =
809
+ cmd->Dict.get("__typename")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
810
+ switch typename {
811
+ | "CommandRejected" =>
812
+ let code =
813
+ cmd->Dict.get("errorCode")->Option.flatMap(JSON.Decode.string)->Option.getOr("unknown")
814
+ JsError.throwWithMessage(`RegisterApiFragment rejected for ${name}: ${code}`)
815
+ | "CommandAccepted" =>
816
+ // eventCount 0 ⇒ idempotent no-op (unchanged fragment) — fields already live,
817
+ // proceed without waiting; >0 ⇒ a real change, wait for the reactive push.
818
+ let eventCount =
819
+ cmd->Dict.get("eventCount")->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0)
820
+ if eventCount > 0.0 {
821
+ await waitForApiFragmentPush(~endpoint, ~region, ~pluginId=name, ~sinceAt=at)
822
+ } else {
823
+ log.info(~comp="registerFragmentViaApi", `${name} fragment unchanged — no push needed`)
824
+ }
825
+ | _ => await waitForApiFragmentPush(~endpoint, ~region, ~pluginId=name, ~sinceAt=at)
826
+ }
827
+ | None =>
828
+ // Unparseable result (mutation may have failed) — fall back to polling.
829
+ await waitForApiFragmentPush(~endpoint, ~region, ~pluginId=name, ~sinceAt=at)
830
+ }
831
+ }
832
+ run()->Pulumi.Output.fromPromise
833
+ })
834
+ }
835
+
649
836
  let hooks: ReventlessCore.Plugin_Helpers.platformHooks = {
650
837
  // AWS uses Interstack for admin extension points — leave ref at empty dict.
651
838
  adminExtensionPoints: ref(Pulumi.Output.make(Dict.make())),
@@ -654,6 +841,7 @@ module MakeWithConfig = (
654
841
  schedulerRoleUrn: ref(Pulumi.Output.make("")),
655
842
  api: hooksApiRef,
656
843
  apiRole: hooksApiRoleRef,
844
+ adminApi: hooksAdminApiRef,
657
845
  deployTarget: ref("Domain"),
658
846
  inboundAppSyncResolverHook: ({runtime, fieldNames, externalInputSchemas: _, opts}) => {
659
847
  let runtimeTyped: ReventlessCore.Runtime.environment<Util.Lambda.runtimeParts> =
@@ -665,11 +853,12 @@ module MakeWithConfig = (
665
853
  ~opts,
666
854
  )
667
855
  },
668
- dcbAppSyncResolverHook: ({runtime, fieldNames, tags, opts}) => {
856
+ dcbAppSyncResolverHook: ({runtime, fieldNames, tags, onAdminApi, opts}) => {
669
857
  let runtimeTyped: ReventlessCore.Runtime.environment<Util.Lambda.runtimeParts> =
670
858
  runtime->asLambdaRuntime
671
859
  CommandGeneratorResolvers_AppSync.makeDcb(
672
- ~api=resolveHookedApi(),
860
+ // Admin slices bind on the Platform API (split mode); plugins on the deploy-target api.
861
+ ~api=onAdminApi ? resolveAdminHookedApi() : resolveHookedApi(),
673
862
  ~runtime=runtimeTyped,
674
863
  ~fieldNames,
675
864
  ~tags,
@@ -714,11 +903,15 @@ module MakeWithConfig = (
714
903
  let adminBaseFragment = AppSync_Adapter.injectAwsAuthAll(
715
904
  ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
716
905
  ~group="Admin",
906
+ // The ApiFragmentRegistry register/deregister mutations + the Platform_ApiFragments
907
+ // status query are invoked by the plugin/standalone deploy as a SigV4 system caller, so
908
+ // they carry the dual-auth (@aws_cognito_user_pools @aws_iam) directive.
909
+ ~iamFieldNames=ReventlessCore.AdminApi.systemCallerFieldNames,
717
910
  )
718
- let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
911
+ let sdl = AppSync_Adapter.stitchWithAwsDirectives(
719
912
  ~baseFragment=adminBaseFragment,
720
913
  ~pluginFragments=[],
721
- )->AppSync_Adapter.stampSharedIamTypes
914
+ )
722
915
  (targetApi, adminBarrier)
723
916
  ->Pulumi.Output.all2
724
917
  ->Pulumi.Output.flatMap(((api, _)) =>
@@ -756,6 +949,19 @@ module MakeWithConfig = (
756
949
  // Domain after P.make() returns, before any Pulumi.Output async callbacks run.
757
950
  let capturedDeployTarget = currentDeployTarget.contents
758
951
 
952
+ switch platformStackRef {
953
+ | Some(_) =>
954
+ // Phase 3 — staged deploy against a running platform (deployPlugin): register the
955
+ // fragment via the Platform API; the reactive single writer (2e) stitches + pushes.
956
+ let apiTargetName = switch capturedDeployTarget {
957
+ | Domain => "Domain"
958
+ | Platform => "Platform"
959
+ }
960
+ registerFragmentViaApi(~name, ~fragment=pluginFragment, ~apiTargetName)
961
+ | None =>
962
+ // All-at-once (makePlatform): platform + plugins deploy in one stack, so the
963
+ // reactive writer is dormant — keep the direct deploy-time stitch + push below.
964
+
759
965
  // Select DynamoDB key prefix and target AppSync API based on the current deploy target.
760
966
  // Domain plugins use "deploy-schema:" and the Domain API (default behaviour).
761
967
  // Platform plugins use a separate "deploy-schema-platform:" namespace and the Core API,
@@ -933,6 +1139,7 @@ module MakeWithConfig = (
933
1139
  AppSync_Adapter.injectAwsAuthAll(
934
1140
  ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
935
1141
  ~group="Admin",
1142
+ ~iamFieldNames=ReventlessCore.AdminApi.systemCallerFieldNames,
936
1143
  )
937
1144
  | Domain =>
938
1145
  if Config.splitApi {
@@ -944,10 +1151,10 @@ module MakeWithConfig = (
944
1151
  )
945
1152
  }
946
1153
  }
947
- let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
1154
+ let sdl = AppSync_Adapter.stitchWithAwsDirectives(
948
1155
  ~baseFragment,
949
1156
  ~pluginFragments=allPluginFragments,
950
- )->AppSync_Adapter.stampSharedIamTypes
1157
+ )
951
1158
  let currentHash = AppSync_Adapter.sha256Hex(sdl)
952
1159
  let storedHash = switch tableNameOpt {
953
1160
  | Some(tn) => await readSchemaHash(~tableName=tn, ~apiId)
@@ -1088,6 +1295,7 @@ module MakeWithConfig = (
1088
1295
  })
1089
1296
  )
1090
1297
  })
1298
+ }
1091
1299
  },
1092
1300
  // DCB EventLog created hook — extracts DynamoDB table name for DCB CommandTopic Lambda handler.
1093
1301
  // Postgres-backed DCB logs (B2.3c) create no table, so there is no resource to read:
@@ -1271,23 +1479,6 @@ module MakeWithConfig = (
1271
1479
  PluginReadModelMappings,
1272
1480
  )
1273
1481
 
1274
- // Admin-internal UIFragmentRegistry read model — projects UIFragmentRegistered/
1275
- // Updated/Deregistered events from the Plugin aggregate into a DynamoDB table the
1276
- // Platform_UIFragments admin query Lambda scans.
1277
- module UIFragmentRegistryReadModelMappings: Reventless.Projection.Mappings
1278
- with module Target := ReventlessCore.UIFragmentRegistryReadModelSpec = {
1279
- module M = ReventlessCore.UIFragmentRegistryProjection.Mappings
1280
- module type Mapping = M.Mapping
1281
- // See PluginReadModelMappings.moduleUrl note above.
1282
- let moduleUrl: string = ReventlessCore.UIFragmentRegistryProjection.moduleUrl
1283
- let mappings: array<module(Mapping)> = ReventlessCore.UIFragmentRegistryProjection.mappings
1284
- }
1285
-
1286
- module UIFragmentRegistryReadModel = ReadModel_Builder_NoResolver_Stream.Make(
1287
- ReventlessCore.UIFragmentRegistryReadModelSpec,
1288
- UIFragmentRegistryReadModelMappings,
1289
- )
1290
-
1291
1482
  module type PluginMaker = {
1292
1483
  let make: unit => Plugin.component
1293
1484
  }
@@ -1432,6 +1623,11 @@ module MakeWithConfig = (
1432
1623
  (domainApi, domainApiRole)
1433
1624
  }
1434
1625
 
1626
+ // Admin DCB mutation resolvers bind to the Platform API (split mode) or the shared api
1627
+ // (unified). Recorded now that the Platform API resource exists, so the admin's deferred
1628
+ // dcbConnectFn (via ~onAdminApi) targets it rather than the Domain/deploy-target hooksApiRef.
1629
+ hooks.adminApi := Some(platformApi->wrapHookedValue)
1630
+
1435
1631
  // Update splitApiOutputsRef and apiConfig with the now-known Platform API.
1436
1632
  if Config.splitApi {
1437
1633
  splitApiOutputsRef := Some({platformApi, platformApiRole})
@@ -1453,16 +1649,13 @@ module MakeWithConfig = (
1453
1649
  ~version,
1454
1650
  ~extensionPoints=[],
1455
1651
  ~aggregates=[module(PluginAggregate)],
1456
- ~readModels=[
1457
- module(PluginReadModel),
1458
- module(UIFragmentRegistryReadModel),
1459
- ],
1652
+ ~readModels=[module(PluginReadModel)],
1460
1653
  ~scheduler,
1461
1654
  ~resourceNaming=Util_ResourceNaming.operations,
1462
1655
  ~api=platformApi,
1463
1656
  ~apiRole=platformApiRole,
1464
- ~stateChangeSlices=[],
1465
- ~stateViewSlices=[],
1657
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
1658
+ ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1466
1659
  ~automationSlices=[],
1467
1660
  ~outboundTranslationSlices=[],
1468
1661
  ~inboundTranslationSlices=[],
@@ -1488,15 +1681,36 @@ module MakeWithConfig = (
1488
1681
  | None => ()
1489
1682
  }
1490
1683
 
1491
- // Mount the Platform_UIFragments Lambda resolver — scans the UIFragmentRegistry
1492
- // table provisioned above and returns one entry per registered plugin UI.
1493
- switch admin.readModelsOutputs->Dict.get("UIFragmentRegistry") {
1684
+ // Mount the Platform_UIFragments Lambda resolver — scans the UiFragments
1685
+ // StateViewSlice table provisioned above and returns one entry per registered
1686
+ // plugin UI.
1687
+ switch admin.stateViewSlicesOutputs->Dict.get("UiFragments") {
1494
1688
  | Some(rm) =>
1495
1689
  switch rm.queryDb.resources->Array.get(0) {
1496
1690
  | Some(r) =>
1497
1691
  Platform_UIFragments_Lambda.make(
1498
1692
  ~api=platformApi,
1499
1693
  ~uiFragmentRegistryTableName=r.name,
1694
+ ~schemaReady=admin.adminSchemaPushed,
1695
+ ~opts={},
1696
+ )
1697
+ | None => ()
1698
+ }
1699
+ | None => ()
1700
+ }
1701
+
1702
+ // Mount the Platform_ApiFragments Lambda resolver — scans the ApiFragments
1703
+ // StateViewSlice table and returns the push-status row per plugin (the deploy
1704
+ // waiter polls this). The query field is in the pushed admin base but was
1705
+ // unresolved on AWS until now.
1706
+ switch admin.stateViewSlicesOutputs->Dict.get("ApiFragments") {
1707
+ | Some(rm) =>
1708
+ switch rm.queryDb.resources->Array.get(0) {
1709
+ | Some(r) =>
1710
+ Platform_ApiFragments_Lambda.make(
1711
+ ~api=platformApi,
1712
+ ~apiFragmentRegistryTableName=r.name,
1713
+ ~schemaReady=admin.adminSchemaPushed,
1500
1714
  ~opts={},
1501
1715
  )
1502
1716
  | None => ()
@@ -1599,6 +1813,11 @@ module MakeWithConfig = (
1599
1813
  (domainApi, domainApiRole)
1600
1814
  }
1601
1815
 
1816
+ // Admin DCB mutation resolvers bind to the Platform API (split mode) or the shared api
1817
+ // (unified). Recorded now that the Platform API resource exists, so the admin's deferred
1818
+ // dcbConnectFn (via ~onAdminApi) targets it rather than the Domain/deploy-target hooksApiRef.
1819
+ hooks.adminApi := Some(platformApi->wrapHookedValue)
1820
+
1602
1821
  // Update splitApiOutputsRef and apiConfig with the now-known Platform API.
1603
1822
  if Config.splitApi {
1604
1823
  splitApiOutputsRef := Some({platformApi, platformApiRole})
@@ -1660,12 +1879,13 @@ module MakeWithConfig = (
1660
1879
  AppSync_Adapter.injectAwsAuthAll(
1661
1880
  ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
1662
1881
  ~group="Admin",
1882
+ ~iamFieldNames=ReventlessCore.AdminApi.systemCallerFieldNames,
1663
1883
  )
1664
1884
  }
1665
- let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
1885
+ let sdl = AppSync_Adapter.stitchWithAwsDirectives(
1666
1886
  ~baseFragment,
1667
1887
  ~pluginFragments=fragments,
1668
- )->AppSync_Adapter.stampSharedIamTypes
1888
+ )
1669
1889
  await AppSync_Adapter.getClient()->AppSync_Adapter.startSchemaCreationRetrying({
1670
1890
  apiId,
1671
1891
  definition: sdl,
@@ -1679,16 +1899,13 @@ module MakeWithConfig = (
1679
1899
  ~version,
1680
1900
  ~extensionPoints=[module(PluginExtensionPoint)],
1681
1901
  ~aggregates=[module(PluginAggregate)],
1682
- ~readModels=[
1683
- module(PluginReadModel),
1684
- module(UIFragmentRegistryReadModel),
1685
- ],
1902
+ ~readModels=[module(PluginReadModel)],
1686
1903
  ~scheduler,
1687
1904
  ~resourceNaming=Util_ResourceNaming.operations,
1688
1905
  ~api=platformApi,
1689
1906
  ~apiRole=platformApiRole,
1690
- ~stateChangeSlices=[],
1691
- ~stateViewSlices=[],
1907
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
1908
+ ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1692
1909
  ~automationSlices=[],
1693
1910
  ~outboundTranslationSlices=[],
1694
1911
  ~inboundTranslationSlices=[],
@@ -1758,6 +1975,18 @@ module MakeWithConfig = (
1758
1975
  ~pluginSchemaPersistenceTableName=pluginSchemaPersistenceTable.name,
1759
1976
  ~schedulerRoleArn=hooks.schedulerRoleUrn.contents,
1760
1977
  ~clonerEnabled=Config.cloner,
1978
+ // 2e: the reactive ApiFragmentRegistry single writer (admin EventCollector).
1979
+ // The Platform AppSync id it pushes Platform-target fragments to (unified →
1980
+ // domainApi, since platformApi == domainApi); the ApiFragments StateViewSlice
1981
+ // table it re-folds from; the admin DCB command-topic FIFO URL it dispatches
1982
+ // RecordApiFragmentPush to (captured during Admin.construct); and the mode flag.
1983
+ ~platformApiId=platformApi->Pulumi.Output.flatMap(api => api.id),
1984
+ ~apiFragmentRegistryTableName=?admin.stateViewSlicesOutputs
1985
+ ->Dict.get("ApiFragments")
1986
+ ->Option.flatMap(rm => rm.queryDb.resources->Array.get(0))
1987
+ ->Option.map(r => r.name),
1988
+ ~adminDcbCmdTopicUrl=?AutomationSliceRuntime_Builder_Single.getDcbQueueUrl(),
1989
+ ~splitApi=Config.splitApi,
1761
1990
  (),
1762
1991
  )
1763
1992
 
@@ -1777,15 +2006,36 @@ module MakeWithConfig = (
1777
2006
  | None => ()
1778
2007
  }
1779
2008
 
1780
- // Mount the Platform_UIFragments Lambda resolver — scans the UIFragmentRegistry
1781
- // table provisioned above and returns one entry per registered plugin UI.
1782
- switch admin.readModelsOutputs->Dict.get("UIFragmentRegistry") {
2009
+ // Mount the Platform_UIFragments Lambda resolver — scans the UiFragments
2010
+ // StateViewSlice table provisioned above and returns one entry per registered
2011
+ // plugin UI.
2012
+ switch admin.stateViewSlicesOutputs->Dict.get("UiFragments") {
1783
2013
  | Some(rm) =>
1784
2014
  switch rm.queryDb.resources->Array.get(0) {
1785
2015
  | Some(r) =>
1786
2016
  Platform_UIFragments_Lambda.make(
1787
2017
  ~api=platformApi,
1788
2018
  ~uiFragmentRegistryTableName=r.name,
2019
+ ~schemaReady=admin.adminSchemaPushed,
2020
+ ~opts={},
2021
+ )
2022
+ | None => ()
2023
+ }
2024
+ | None => ()
2025
+ }
2026
+
2027
+ // Mount the Platform_ApiFragments Lambda resolver — scans the ApiFragments
2028
+ // StateViewSlice table and returns the push-status row per plugin (the deploy
2029
+ // waiter polls this). The query field is in the pushed admin base but was
2030
+ // unresolved on AWS until now.
2031
+ switch admin.stateViewSlicesOutputs->Dict.get("ApiFragments") {
2032
+ | Some(rm) =>
2033
+ switch rm.queryDb.resources->Array.get(0) {
2034
+ | Some(r) =>
2035
+ Platform_ApiFragments_Lambda.make(
2036
+ ~api=platformApi,
2037
+ ~apiFragmentRegistryTableName=r.name,
2038
+ ~schemaReady=admin.adminSchemaPushed,
1789
2039
  ~opts={},
1790
2040
  )
1791
2041
  | None => ()