@reventlessdev/reventless-aws 3.0.0-alpha.200 → 3.0.0-alpha.202

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 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.202 (2026-07-13)
7
+
8
+ ### Features
9
+
10
+ * **admin:** ApiFragmentRegistry slice→singleton-aggregate + reactive schema-push SideEffect ([cf70903](https://github.com/ReventlessDev/reventless-core/commit/cf709033f0d822248ebbcfe3fe6b4019b3c1e8a0)), closes [#4](https://github.com/ReventlessDev/reventless-core/issues/4)
11
+
12
+
13
+ # 3.0.0-alpha.201 (2026-07-13)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **api:** dual-auth arg-less IAM fields — extractLeadingName dropped the trailing colon ([ea34da2](https://github.com/ReventlessDev/reventless-core/commit/ea34da229164db63988ad035138c17ba9134ac55))
18
+
19
+
6
20
  # 3.0.0-alpha.200 (2026-07-12)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.200",
3
+ "version": "3.0.0-alpha.202",
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",
12
11
  "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
12
+ "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.21",
13
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
13
14
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.50",
14
15
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
15
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
16
16
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.15",
17
- "@reventlessdev/reventless-core": "3.0.0-alpha.159",
17
+ "@reventlessdev/reventless-core": "3.0.0-alpha.161",
18
18
  "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
19
- "@reventlessdev/reventless-interop": "3.0.0-alpha.26",
19
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.25",
20
20
  "@reventlessdev/reventless-spec": "3.0.0-alpha.76",
21
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.23"
21
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.26"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rescript": "^12.3.0",
package/src/Platform.res CHANGED
@@ -113,9 +113,9 @@ module MakeWithConfig = (
113
113
  // Lambda (DynamoDB scan) — keep it off the Postgres selection like the other
114
114
  // admin stores.
115
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)
116
+ // The ApiFragments read model backs the Platform_ApiFragments status query
117
+ // (DynamoDB scan) and the schema-push SideEffect — admin store, off Postgres too.
118
+ QueryDbBackend.exempt(ReventlessCore.ApiFragmentsReadModelSpec.name)
119
119
  })
120
120
  type api = Types.AppSync.api
121
121
  type role = Types.AppSync.role
@@ -585,14 +585,8 @@ module MakeWithConfig = (
585
585
  ReventlessCore.UiFragments,
586
586
  ReventlessCore.UiFragments_Projection,
587
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
- )
588
+ // The API-fragment registry is a SINGLETON AGGREGATE now (not a DCB slice) — see
589
+ // ApiFragmentRegistryAggregate / ApiFragmentsReadModel below.
596
590
 
597
591
  // Empty base fragment — no types, no mutations, no queries.
598
592
  // Used by the plugin Api in split mode so plugin schema has no core fields.
@@ -779,8 +773,10 @@ module MakeWithConfig = (
779
773
  ->Pulumi.Output.flatMap(endpoint => {
780
774
  let run = async () => {
781
775
  let at = Date.make()->Date.toISOString
776
+ // The registry is a SINGLETON aggregate — the mutation `id` arg is the fixed
777
+ // constant, `pluginId` (a payload field) carries the plugin name.
782
778
  let variables = {
783
- "id": name,
779
+ "id": "registry",
784
780
  "pluginId": name,
785
781
  "fragment": {"encoded": fragment.encoded, "protocol": fragment.protocol},
786
782
  "apiTarget": Util_AppSync_Caller.graphqlEnum(apiTargetName),
@@ -794,14 +790,14 @@ module MakeWithConfig = (
794
790
  let result = await Util_AppSync_Caller.sendMutation(
795
791
  ~endpoint,
796
792
  ~region,
797
- ~mutation="Platform_RegisterApiFragment",
793
+ ~mutation="Platform_ApiFragmentRegistry_RegisterApiFragment",
798
794
  ~selection,
799
795
  ~variables,
800
796
  )
801
797
  let outcome =
802
798
  result
803
799
  ->Option.flatMap(JSON.Decode.object)
804
- ->Option.flatMap(d => d->Dict.get("Platform_RegisterApiFragment"))
800
+ ->Option.flatMap(d => d->Dict.get("Platform_ApiFragmentRegistry_RegisterApiFragment"))
805
801
  ->Option.flatMap(JSON.Decode.object)
806
802
  switch outcome {
807
803
  | Some(cmd) =>
@@ -1480,6 +1476,43 @@ module MakeWithConfig = (
1480
1476
  PluginReadModelMappings,
1481
1477
  )
1482
1478
 
1479
+ // Admin-internal ApiFragmentRegistry singleton aggregate — the platform API-schema
1480
+ // fragment registry (docs/plans/event-sourced-fragment-registries.md). Threaded into
1481
+ // Admin.construct's ~aggregates so registerAdminAggregateMutations wires
1482
+ // Platform_ApiFragmentRegistry_RegisterApiFragment / _DeregisterApiFragment via the
1483
+ // standard CommandGenerator auto-flow (RecordApiFragmentPush carries `@noApi`). IAM
1484
+ // dual-auth for the SigV4 deploy caller comes from AdminApi.systemCallerFieldNames.
1485
+ module ApiFragmentRegistryAggregate: (
1486
+ ReventlessInfra.Aggregate.T with type api = Types.AppSync.api
1487
+ ) = Aggregate_Builder_Single.Make(
1488
+ ReventlessCore.ApiFragmentRegistrySpec,
1489
+ ReventlessCore.ApiFragmentRegistryBehavior,
1490
+ ReventlessInfra.NoEventMappings.Make(ReventlessCore.ApiFragmentRegistrySpec),
1491
+ )
1492
+
1493
+ // Admin ApiFragments read model — per-plugin status rows off the aggregate's event
1494
+ // topic (DynamoDB stream), backing the Platform_ApiFragments status query.
1495
+ module ApiFragmentsReadModelMappings: Reventless.Projection.Mappings
1496
+ with module Target := ReventlessCore.ApiFragmentsReadModelSpec = {
1497
+ module M = ReventlessCore.ApiFragmentsProjection.Mappings
1498
+ module type Mapping = M.Mapping
1499
+ let moduleUrl: string = ReventlessCore.ApiFragmentsProjection.moduleUrl
1500
+ let mappings: array<module(Mapping)> = ReventlessCore.ApiFragmentsProjection.mappings
1501
+ }
1502
+ module ApiFragmentsReadModel = ReadModel_Builder_Single_Stream.Make(
1503
+ ReventlessCore.ApiFragmentsReadModelSpec,
1504
+ ApiFragmentsReadModelMappings,
1505
+ )
1506
+
1507
+ // Admin SideEffectHandler hosting the reactive schema-push side effect (ApiSchemaPush).
1508
+ // Subscribes to the ApiFragmentRegistry aggregate's DynamoDB stream (single-shard for a
1509
+ // singleton → naturally serialized, no concurrent StartSchemaCreation) and, on each
1510
+ // ApiSchemaComputed, stitches + pushes the schema per target API, then records the
1511
+ // outcome via RecordApiFragmentPush. Wired in deployPlatform (the staged path where the
1512
+ // deploy caller fires RegisterApiFragment); makePlatform builds the schema directly and
1513
+ // never triggers it.
1514
+ module AdminApiSchemaPushHandler = SideEffectHandler_Single.Make()
1515
+
1483
1516
  module type PluginMaker = {
1484
1517
  let make: unit => Plugin.component
1485
1518
  }
@@ -1649,14 +1682,14 @@ module MakeWithConfig = (
1649
1682
  let admin = Admin.construct(
1650
1683
  ~version,
1651
1684
  ~extensionPoints=[],
1652
- ~aggregates=[module(PluginAggregate)],
1653
- ~readModels=[module(PluginReadModel)],
1685
+ ~aggregates=[module(PluginAggregate), module(ApiFragmentRegistryAggregate)],
1686
+ ~readModels=[module(PluginReadModel), module(ApiFragmentsReadModel)],
1654
1687
  ~scheduler,
1655
1688
  ~resourceNaming=Util_ResourceNaming.operations,
1656
1689
  ~api=platformApi,
1657
1690
  ~apiRole=platformApiRole,
1658
- ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
1659
- ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1691
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice)],
1692
+ ~stateViewSlices=[module(UiFragmentsViewSlice)],
1660
1693
  ~automationSlices=[],
1661
1694
  ~outboundTranslationSlices=[],
1662
1695
  ~inboundTranslationSlices=[],
@@ -1704,7 +1737,7 @@ module MakeWithConfig = (
1704
1737
  // StateViewSlice table and returns the push-status row per plugin (the deploy
1705
1738
  // waiter polls this). The query field is in the pushed admin base but was
1706
1739
  // unresolved on AWS until now.
1707
- switch admin.stateViewSlicesOutputs->Dict.get("ApiFragments") {
1740
+ switch admin.readModelsOutputs->Dict.get("ApiFragments") {
1708
1741
  | Some(rm) =>
1709
1742
  switch rm.queryDb.resources->Array.get(0) {
1710
1743
  | Some(r) =>
@@ -1899,19 +1932,84 @@ module MakeWithConfig = (
1899
1932
  let admin = Admin.construct(
1900
1933
  ~version,
1901
1934
  ~extensionPoints=[module(PluginExtensionPoint)],
1902
- ~aggregates=[module(PluginAggregate)],
1903
- ~readModels=[module(PluginReadModel)],
1935
+ ~aggregates=[module(PluginAggregate), module(ApiFragmentRegistryAggregate)],
1936
+ ~readModels=[module(PluginReadModel), module(ApiFragmentsReadModel)],
1904
1937
  ~scheduler,
1905
1938
  ~resourceNaming=Util_ResourceNaming.operations,
1906
1939
  ~api=platformApi,
1907
1940
  ~apiRole=platformApiRole,
1908
- ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
1909
- ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1941
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice)],
1942
+ ~stateViewSlices=[module(UiFragmentsViewSlice)],
1910
1943
  ~automationSlices=[],
1911
1944
  ~outboundTranslationSlices=[],
1912
1945
  ~inboundTranslationSlices=[],
1913
1946
  )
1914
1947
 
1948
+ // Reactive schema-push SideEffect (docs/plans/event-sourced-fragment-registries.md §
1949
+ // Reactive writer design). Host ApiSchemaPush on an admin SideEffectHandler subscribed to
1950
+ // the ApiFragmentRegistry aggregate's event topic (its DynamoDB stream). Deploy-derived
1951
+ // config the runtime-pure push reads at invocation time is injected as Lambda env
1952
+ // (~extraEnvVars); ~targets grants SQS send to the aggregate command topic (for the
1953
+ // RecordApiFragmentPush write-back). Only in deployPlatform (the staged path where the
1954
+ // deploy caller fires RegisterApiFragment); makePlatform pushes the schema directly.
1955
+ let apiSchemaPushEventTopics = ReventlessCore.Aggregate.allEventTopics(admin.aggregatesOutputs)
1956
+ let apiSchemaPushCmdTopics = ReventlessCore.Aggregate.allCommandTopics(admin.aggregatesOutputs)
1957
+ let apiSchemaPushCmdTopicUrl =
1958
+ admin.aggregatesOutputs
1959
+ ->Dict.get(ReventlessCore.ApiFragmentRegistrySpec.name)
1960
+ ->Option.map(agg =>
1961
+ agg.commandTopic->Pulumi.Output.flatMap(ct =>
1962
+ switch ct.resources->Array.get(0) {
1963
+ | Some(r) => r.id
1964
+ | None => Pulumi.Output.make("")
1965
+ }
1966
+ )
1967
+ )
1968
+ ->Option.getOr(Pulumi.Output.make(""))
1969
+ let apiSchemaPushEnv = Dict.fromArray([
1970
+ ("API_SCHEMA_PUSH_DOMAIN_API_ID", domainApiId->Pulumi.Output.asInput),
1971
+ (
1972
+ "API_SCHEMA_PUSH_PLATFORM_API_ID",
1973
+ platformApi->Pulumi.Output.flatMap(api => api.id)->Pulumi.Output.asInput,
1974
+ ),
1975
+ (
1976
+ "API_SCHEMA_PUSH_SPLIT_API",
1977
+ Pulumi.Output.make(Config.splitApi ? "true" : "false")->Pulumi.Output.asInput,
1978
+ ),
1979
+ (
1980
+ "API_SCHEMA_PUSH_CLONER",
1981
+ Pulumi.Output.make(Config.cloner ? "true" : "false")->Pulumi.Output.asInput,
1982
+ ),
1983
+ ("API_SCHEMA_PUSH_CMD_TOPIC_URL", apiSchemaPushCmdTopicUrl->Pulumi.Output.asInput),
1984
+ ])
1985
+ // ApiSchemaPush ignores the injected queryEngine (it self-wires push + write-back from
1986
+ // env); an empty one satisfies the SideEffectHandler contract. `scheduler`/`queryEngine`
1987
+ // are Outputs, so make() runs inside their apply; finish() (which builds the shared
1988
+ // Lambda from the registered handlers) is registered on apiSchemaPushCmdTopics
1989
+ // immediately after make() so it runs AFTER make's own deferred forEventCollector apply
1990
+ // on that same Output (Pulumi runs same-Output apply callbacks in registration order).
1991
+ let apiSchemaPushQueryEngine = QueryEngine.DynamoDb.make(Dict.make())
1992
+ let _ =
1993
+ (scheduler, apiSchemaPushQueryEngine)
1994
+ ->Pulumi.Output.all2
1995
+ ->Pulumi.Output.apply(((sched, qe)) => {
1996
+ let _ = AdminApiSchemaPushHandler.make(
1997
+ ~name="AdminApiSchemaPush",
1998
+ ~sideEffects=[module(ApiSchemaPush)],
1999
+ ~allEventTopics=apiSchemaPushEventTopics,
2000
+ ~allCommandTopics=apiSchemaPushCmdTopics,
2001
+ ~targets=[ReventlessCore.ApiFragmentRegistrySpec.name],
2002
+ ~queryEngine=qe,
2003
+ ~scheduler=sched,
2004
+ ~resourceNaming=Util_ResourceNaming.operations,
2005
+ ~extraEnvVars=apiSchemaPushEnv,
2006
+ ~opts={},
2007
+ )
2008
+ let _ = apiSchemaPushCmdTopics->Pulumi.Output.apply(_ =>
2009
+ SideEffectHandlerRuntime_Builder_Single.finish()
2010
+ )
2011
+ })
2012
+
1915
2013
  // Extract Plugin RM table name as Output.t<string>.
1916
2014
  // IMPORTANT: Do NOT use option<Pulumi.Output.t<…>> — Pulumi Outputs use property
1917
2015
  // lifting, which breaks ReScript's internal option encoding (BS_PRIVATE_NESTED_SOME_NONE).
@@ -1991,7 +2089,7 @@ module MakeWithConfig = (
1991
2089
  // so the consumer's `tableOutput->Pulumi.Output.apply` crashes with
1992
2090
  // "apply is not a function". A `Some(r.name)` LITERAL compiles unboxed (bare
1993
2091
  // r.name), preserving the Output — the same dodge `pluginReadModelTableName` uses.
1994
- ~apiFragmentRegistryTableName=?switch admin.stateViewSlicesOutputs
2092
+ ~apiFragmentRegistryTableName=?switch admin.readModelsOutputs
1995
2093
  ->Dict.get("ApiFragments")
1996
2094
  ->Option.flatMap(rm => rm.queryDb.resources->Array.get(0)) {
1997
2095
  | Some(r) => Some(r.name)
@@ -2040,7 +2138,7 @@ module MakeWithConfig = (
2040
2138
  // StateViewSlice table and returns the push-status row per plugin (the deploy
2041
2139
  // waiter polls this). The query field is in the pushed admin base but was
2042
2140
  // unresolved on AWS until now.
2043
- switch admin.stateViewSlicesOutputs->Dict.get("ApiFragments") {
2141
+ switch admin.readModelsOutputs->Dict.get("ApiFragments") {
2044
2142
  | Some(rm) =>
2045
2143
  switch rm.queryDb.resources->Array.get(0) {
2046
2144
  | Some(r) =>