@reventlessdev/reventless-local 3.0.0-alpha.152 → 3.0.0-alpha.154

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.154 (2026-07-14)
7
+
8
+ ### Features
9
+
10
+ * surface masked GraphQL resolver errors server-side (local + AWS) ([b3bca51](https://github.com/ReventlessDev/reventless-core/commit/b3bca51b539e40c604b3db338d570de6215ea837))
11
+
12
+
13
+ # 3.0.0-alpha.153 (2026-07-14)
14
+
15
+ ### Features
16
+
17
+ * **local:** plugin=subgraph composition — per-plugin subschemas merged at start (Phase 6, plan complete) ([40f44c5](https://github.com/ReventlessDev/reventless-core/commit/40f44c5c6aa32fbbd64f362841cf41ba4e2fc2e0))
18
+
19
+
6
20
  # 3.0.0-alpha.152 (2026-07-14)
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.152",
3
+ "version": "3.0.0-alpha.154",
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": {
@@ -31,17 +31,17 @@
31
31
  "graphql-yoga": "^5.21.0",
32
32
  "sury": "11.0.0-alpha.4",
33
33
  "ws": "^8.18.0",
34
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
35
34
  "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
36
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
35
+ "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.22",
36
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
37
37
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.18",
38
- "@reventlessdev/reventless-core": "3.0.0-alpha.166",
39
- "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.20",
40
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.13",
41
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.112",
42
- "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
43
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.30",
44
- "@reventlessdev/reventless-spec": "3.0.0-alpha.76"
38
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
39
+ "@reventlessdev/reventless-core": "3.0.0-alpha.168",
40
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.15",
41
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.114",
42
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.99",
43
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.76",
44
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.32"
45
45
  },
46
46
  "devDependencies": {
47
47
  "rescript": "^12.3.0",
package/src/Platform.res CHANGED
@@ -61,23 +61,6 @@ let decodeUiFragmentRegistryEventEnvelope = (
61
61
  | exception _ => None
62
62
  }
63
63
 
64
- // Same {id, meta, event} envelope for the ApiFragmentRegistry singleton aggregate's
65
- // events on its own aggregate event topic (the registry is an aggregate now, not a
66
- // DCB slice — its events no longer ride the admin DcbEventLog). Top-level so the
67
- // decode is unit-testable.
68
- let decodeApiFragmentRegistryEventEnvelope = (
69
- eventJson: JSON.t,
70
- ): option<ReventlessCore.ApiFragmentRegistrySpec.event> =>
71
- switch eventJson
72
- ->JSON.Decode.object
73
- ->Option.flatMap(d => d->Dict.get("event"))
74
- ->Option.map(payload =>
75
- payload->S.parseJsonOrThrow(ReventlessCore.ApiFragmentRegistrySpec.eventSchema)
76
- ) {
77
- | result => result
78
- | exception _ => None
79
- }
80
-
81
64
  // Configurable platform — set silent=true to suppress diagnostic warnings in tests,
82
65
  // splitApi=true to serve core and plugin APIs on separate ports,
83
66
  // backend=Backend.Sqlite({...}) to opt into file-backed SQLite persistence.
@@ -176,6 +159,30 @@ module MakeWithConfig = (
176
159
  // Domain = plugin-facing (default); Platform = admin/core (split mode).
177
160
  let currentDeployTarget: ref<apiTarget> = ref(Domain)
178
161
 
162
+ // Plugin=subgraph scoping (mirrors the AWS plugin=source-API model): each
163
+ // plugin's construction runs inside its own DomainGraphQL_Server scope so
164
+ // its GraphQL registrations form a standalone subgraph, validated in
165
+ // isolation and composed at start(). The plugin name is only known after
166
+ // construction (via the synchronous plugin-built hook), so the scope opens
167
+ // under a unique token and is relabeled to the plugin name afterwards.
168
+ let pluginScopeCounter = ref(0)
169
+ let buildPluginInScope = (
170
+ ~makePlugin: unit => ReventlessCore.Plugin.component,
171
+ ~builtInfos: ref<array<ReventlessCore.Plugin_Helpers.pluginBuiltInfo>>,
172
+ ): ReventlessCore.Plugin.component => {
173
+ pluginScopeCounter.contents = pluginScopeCounter.contents + 1
174
+ let scopeToken = `plugin-${pluginScopeCounter.contents->Int.toString}`
175
+ DomainGraphQL_Server.setScope(scopeToken)
176
+ let countBefore = builtInfos.contents->Array.length
177
+ let component = makePlugin()
178
+ switch builtInfos.contents->Array.get(countBefore) {
179
+ | Some(info) => DomainGraphQL_Server.relabelScope(~from=scopeToken, ~to_=info.name)
180
+ | None => ()
181
+ }
182
+ DomainGraphQL_Server.resetScope()
183
+ component
184
+ }
185
+
179
186
  // Track which GraphQL servers have had admin schema (Plugin queries/mutations) registered.
180
187
  // Prevents double-registration when makePlatform and deployPlugin both target the same server.
181
188
  let adminRegisteredServers: ref<array<ReventlessGraphqlServer.GraphQL_ServerInstance.t>> = ref([])
@@ -672,8 +679,7 @@ module MakeWithConfig = (
672
679
 
673
680
  // Admin UI-fragment registry (docs/plans/event-sourced-fragment-registries.md): the
674
681
  // platform UI-fragment registry hosted as an admin DCB slice on the admin DcbEventLog.
675
- // Built once and passed into Admin.construct below. (The API-fragment registry is a
676
- // singleton aggregate — see LocalApiFragmentRegistryAggregate — not a slice.)
682
+ // Built once and passed into Admin.construct below.
677
683
  module UiFragmentRegistrySlice = StateChangeSlice.Make(
678
684
  ReventlessCore.UiFragmentRegistry,
679
685
  ReventlessCore.UiFragmentRegistry_Behavior,
@@ -716,35 +722,6 @@ module MakeWithConfig = (
716
722
  InboundTranslationSliceMaker.Make(Spec, Translation)
717
723
  }
718
724
 
719
- // Empty base fragment — no types, no mutations, no queries.
720
- // Used by the plugin Api in split mode so plugin schema has no core fields.
721
- let emptyBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
722
- types: [],
723
- mutations: [],
724
- queries: [],
725
- subscriptions: [],
726
- subscriptionSources: [],
727
- })
728
-
729
- module Api = {
730
- module Make = (
731
- FragmentConfig: {
732
- let baseFragment: ReventlessInfra.Api.schemaFragment
733
- },
734
- ): ReventlessInfra.Api.T => {
735
- module Builder = ReventlessCore.Api_Builder.Make(LocalGraphQL_Adapter)
736
- // In split mode, the plugin API uses an empty base fragment so plugin schema
737
- // has no core fields. In unified mode, use the provided base fragment as-is.
738
- let effectiveBaseFragment = if Config.splitApi {
739
- emptyBaseFragment
740
- } else {
741
- FragmentConfig.baseFragment
742
- }
743
- let make = (~name, ~opts=?) =>
744
- Builder.make(~name, ~baseFragment=effectiveBaseFragment, ~opts?)
745
- }
746
- }
747
-
748
725
  module PluginMaker = Plugin_Builder.Make(
749
726
  Bus,
750
727
  {
@@ -797,33 +774,6 @@ module MakeWithConfig = (
797
774
  PluginsReadModelMappings,
798
775
  )
799
776
 
800
- // Admin-internal ApiFragmentRegistry singleton aggregate — the platform API-schema
801
- // fragment registry (docs/plans/event-sourced-fragment-registries.md). Threaded into
802
- // Admin.construct's ~aggregates so registerAdminAggregateMutations wires
803
- // Platform_ApiFragmentRegistry_RegisterApiFragment / _DeregisterApiFragment through the
804
- // standard CommandGenerator auto-flow. RecordApiFragmentPush carries `@noApi`.
805
- module LocalApiFragmentRegistryAggregate: ReventlessInfra.Aggregate.T
806
- with type api = unit = AggregateMaker.Make(
807
- ReventlessCore.ApiFragmentRegistrySpec,
808
- ReventlessCore.ApiFragmentRegistryBehavior,
809
- ReventlessInfra.NoEventMappings.Make(ReventlessCore.ApiFragmentRegistrySpec),
810
- )
811
-
812
- // Admin ApiFragments read model — per-plugin status rows off the aggregate's event
813
- // topic, backing the Platform_ApiFragments status query.
814
- module ApiFragmentsReadModelMappings: Reventless.Projection.Mappings
815
- with module Target := ReventlessCore.ApiFragmentsReadModelSpec = {
816
- module M = ReventlessCore.ApiFragmentsProjection.Mappings
817
- module type Mapping = M.Mapping
818
- let moduleUrl: string = ReventlessCore.ApiFragmentsProjection.moduleUrl
819
- let mappings: array<module(Mapping)> = ReventlessCore.ApiFragmentsProjection.mappings
820
- }
821
- module ApiFragmentsReadModel = ReadModelMaker.MakeNoResolver(
822
- ReventlessCore.ApiFragmentsReadModelSpec,
823
- ApiFragmentsReadModelMappings,
824
- )
825
-
826
-
827
777
  module Admin = ReventlessCore.Platform_Admin.Make(
828
778
  LocalRuntimeEnvironment,
829
779
  EventCollectorChannel,
@@ -954,57 +904,6 @@ module MakeWithConfig = (
954
904
  Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
955
905
  }
956
906
 
957
- // The ApiFragmentRegistry is a SINGLETON aggregate now (not a DCB slice): the
958
- // whole registry lives on one aggregate instance keyed by a fixed id, so a
959
- // fragment change reads the whole registry from one consistency boundary. Its
960
- // commands ride the aggregate command topic (name-derived, like the Plugin
961
- // aggregate above) and its events its own aggregate event topic.
962
- let apiFragmentRegistryId = "registry"
963
- let apiFragmentRegistryCmdTopicKey =
964
- ReventlessCore.ApiFragmentRegistrySpec.name ++ "Aggr" ++ "CmdTopic"
965
- let apiFragmentRegistryEventTopicKey =
966
- ReventlessCore.ApiFragmentRegistrySpec.name ++ "Aggr" ++ "EventTopic"
967
-
968
- // Dispatch an ApiFragmentRegistry aggregate command in-process. On the real
969
- // platform the deploy is the caller (SigV4 Platform_ApiFragmentRegistry_*
970
- // mutation); locally we dispatch straight to the aggregate command topic. The
971
- // aggregate id is the fixed singleton constant — `pluginId` is a payload field.
972
- let dispatchApiFragmentCommand = (~command: ReventlessCore.ApiFragmentRegistrySpec.command) => {
973
- let cmdJson: Reventless.Message.commandJson = {
974
- id: apiFragmentRegistryId,
975
- meta: ReventlessCore.Message.generateMeta(
976
- ~service=ReventlessCore.ApiFragmentRegistrySpec.name,
977
- ),
978
- commandJson: command->S.reverseConvertToJsonOrThrow(
979
- ReventlessCore.ApiFragmentRegistrySpec.commandSchema,
980
- ),
981
- }
982
- Bus.dispatchCommand(
983
- apiFragmentRegistryCmdTopicKey,
984
- ReventlessCore.CommandTopic.encodeCommandJson(cmdJson),
985
- )
986
- }
987
-
988
- // Platform_ApiFragments status-query resolver — scans the ApiFragments read
989
- // model (populated by the ApiFragmentRegistry aggregate's projection) and
990
- // encodes via the shared Platform_ApiFragmentsApi encoder, so the deploy-facing
991
- // status shape matches AWS. Keyed by bare plugin name (one row per plugin).
992
- // Registered at each admin server site below.
993
- let apiFragmentsQueryResolver = async (_root, _args, _ctx): JSON.t => {
994
- let items = switch Bus.getQueryDbScan(ReventlessCore.ApiFragmentsReadModelSpec.name) {
995
- | Some(scanAll) => scanAll()
996
- | None => []
997
- }
998
- items
999
- ->Array.filterMap(item =>
1000
- switch item->S.parseOrThrow(ReventlessCore.ApiFragmentsReadModelSpec.stateSchema) {
1001
- | state => Some(ReventlessCore.Platform_ApiFragmentsApi.encodeApiFragmentEntry(state))
1002
- | exception _ => None
1003
- }
1004
- )
1005
- ->JSON.Encode.array
1006
- }
1007
-
1008
907
  // Subscription topics for the admin live-update channels.
1009
908
  let pluginStatusSubTopic = "onPluginStatusChange"
1010
909
  let uiFragmentSubTopic = "onUIFragmentChange"
@@ -1097,33 +996,6 @@ module MakeWithConfig = (
1097
996
  }
1098
997
  }
1099
998
  )
1100
- // Local single writer for the API-schema fragment registry — the analogue of
1101
- // the AWS ApiSchemaPush SideEffect. Sourced from the ApiFragmentRegistry
1102
- // aggregate's own event topic (the registry is an aggregate now). On AWS the
1103
- // SideEffect stitches + pushes the schema then records the outcome; on local
1104
- // the plugin's fields are already served by resolver registration, so the
1105
- // "push" is a no-op and we simply record success — the honest status for an
1106
- // in-process schema. Only Registered/Updated trigger a record (never
1107
- // PushRecorded — no re-entrant loop; ApiSchemaComputed is the AWS push trigger
1108
- // and is a no-op here).
1109
- Bus.subscribeToEvents(apiFragmentRegistryEventTopicKey, async (_service, _meta, eventJson) =>
1110
- switch decodeApiFragmentRegistryEventEnvelope(eventJson) {
1111
- | Some(ApiFragmentRegistered({pluginId}))
1112
- | Some(ApiFragmentUpdated({pluginId})) =>
1113
- let _ = dispatchApiFragmentCommand(
1114
- ~command=ReventlessCore.ApiFragmentRegistrySpec.RecordApiFragmentPush({
1115
- pluginId,
1116
- ok: true,
1117
- message: "",
1118
- at: Date.make()->Date.toISOString,
1119
- }),
1120
- )
1121
- | Some(ApiFragmentDeregistered(_))
1122
- | Some(ApiFragmentPushRecorded(_))
1123
- | Some(ApiSchemaComputed(_))
1124
- | None => ()
1125
- }
1126
- )
1127
999
  }
1128
1000
 
1129
1001
  // Drive the admin Plugin read models the same way AWS does: build a
@@ -1217,24 +1089,6 @@ module MakeWithConfig = (
1217
1089
  )
1218
1090
  | None => ()
1219
1091
  }
1220
- // Register the API-schema fragment with the ApiFragmentRegistry aggregate.
1221
- // On the real platform the deploy sends this as a SigV4 caller; locally
1222
- // connect stands in. apiTarget is Domain (mirrors pluginDefinition.kind
1223
- // = Domain / apiTarget = None above — the local platform has no
1224
- // Platform-target plugins). The subscriber below records the push as ok,
1225
- // since the fields are already served in-process by resolver registration.
1226
- switch apiSchemaFragment {
1227
- | Some(fragment) =>
1228
- let _ = dispatchApiFragmentCommand(
1229
- ~command=ReventlessCore.ApiFragmentRegistrySpec.RegisterApiFragment({
1230
- pluginId: pluginName,
1231
- fragment,
1232
- apiTarget: Domain,
1233
- at: Date.make()->Date.toISOString,
1234
- }),
1235
- )
1236
- | None => ()
1237
- }
1238
1092
  })
1239
1093
  })
1240
1094
  }
@@ -1500,8 +1354,8 @@ module MakeWithConfig = (
1500
1354
  let admin = Admin.construct(
1501
1355
  ~version,
1502
1356
  ~extensionPoints=[],
1503
- ~aggregates=[module(LocalPluginAggregate), module(LocalApiFragmentRegistryAggregate)],
1504
- ~readModels=[module(PluginsReadModel), module(ApiFragmentsReadModel)],
1357
+ ~aggregates=[module(LocalPluginAggregate)],
1358
+ ~readModels=[module(PluginsReadModel)],
1505
1359
  ~scheduler,
1506
1360
  ~resourceNaming=LocalPluginSpec.resourceNaming,
1507
1361
  ~api=(),
@@ -1531,10 +1385,12 @@ module MakeWithConfig = (
1531
1385
  builtInfos.contents->Array.push(info)
1532
1386
  })
1533
1387
 
1534
- // Build each plugin.
1388
+ // Build each plugin inside its own DomainGraphQL_Server scope so its
1389
+ // GraphQL registrations form a standalone subgraph (plugin=subgraph,
1390
+ // platform=merge — mirrors the AWS source-API model).
1535
1391
  let plugins = plugins->Array.map(plugin => {
1536
1392
  module P = unpack(plugin)
1537
- P.make()
1393
+ buildPluginInScope(~makePlugin=P.make, ~builtInfos)
1538
1394
  })
1539
1395
 
1540
1396
  // Restore original hook and fire deployed hooks synchronously.
@@ -1823,8 +1679,6 @@ module MakeWithConfig = (
1823
1679
  ->JSON.Encode.array
1824
1680
  },
1825
1681
  )
1826
- queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
1827
-
1828
1682
  // Composite-key / index admin query fields. Live loads from the seeded QueryDb stores.
1829
1683
  registerAdminItemsAndIndexResolvers(~queryResolvers, ~live=true)
1830
1684
 
@@ -2066,8 +1920,8 @@ module MakeWithConfig = (
2066
1920
  let _admin = Admin.construct(
2067
1921
  ~version,
2068
1922
  ~extensionPoints=[],
2069
- ~aggregates=[module(LocalApiFragmentRegistryAggregate)],
2070
- ~readModels=[module(ApiFragmentsReadModel)],
1923
+ ~aggregates=[],
1924
+ ~readModels=[],
2071
1925
  ~scheduler,
2072
1926
  ~resourceNaming=LocalPluginSpec.resourceNaming,
2073
1927
  ~api=(),
@@ -2105,7 +1959,6 @@ module MakeWithConfig = (
2105
1959
  "Platform_UIFragments",
2106
1960
  async (_root, _args, _ctx): JSON.t => JSON.Encode.array([]),
2107
1961
  )
2108
- queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
2109
1962
  // Composite-key / index admin query fields — empty stubs in the platform-only
2110
1963
  // path (no plugins connected → no QueryDb seeded).
2111
1964
  registerAdminItemsAndIndexResolvers(~queryResolvers, ~live=false)
@@ -2233,8 +2086,8 @@ module MakeWithConfig = (
2233
2086
  let admin = Admin.construct(
2234
2087
  ~version="",
2235
2088
  ~extensionPoints=[],
2236
- ~aggregates=[module(LocalPluginAggregate), module(LocalApiFragmentRegistryAggregate)],
2237
- ~readModels=[module(PluginsReadModel), module(ApiFragmentsReadModel)],
2089
+ ~aggregates=[module(LocalPluginAggregate)],
2090
+ ~readModels=[module(PluginsReadModel)],
2238
2091
  ~scheduler,
2239
2092
  ~resourceNaming=LocalPluginSpec.resourceNaming,
2240
2093
  ~api=(),
@@ -2258,8 +2111,12 @@ module MakeWithConfig = (
2258
2111
  builtInfos.contents->Array.push(info)
2259
2112
  })
2260
2113
 
2114
+ // Construct the plugin inside its own DomainGraphQL_Server scope
2115
+ // (plugin=subgraph — see buildPluginInScope). Registrations targeting the
2116
+ // platform server (apiTarget=Platform in split mode) are unaffected:
2117
+ // scopes only exist on DomainGraphQL_Server.
2261
2118
  module P = unpack(plugin)
2262
- let pluginComponent = P.make()
2119
+ let pluginComponent = buildPluginInScope(~makePlugin=P.make, ~builtInfos)
2263
2120
 
2264
2121
  ReventlessCore.Plugin_Helpers.onPluginBuiltHook.contents = existingBuiltHook
2265
2122
 
@@ -2301,7 +2158,6 @@ module MakeWithConfig = (
2301
2158
  ->JSON.Encode.array
2302
2159
  },
2303
2160
  )
2304
- queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
2305
2161
  // Platform_ComponentDefinitions resolver — SDL is already stitched into baseParts via
2306
2162
  // AdminApi.baseFragment so we register only the resolver here. Uses the shared
2307
2163
  // encoder so the dynamic-plugin admin server emits the same canonical shape as