@reventlessdev/reventless-core 3.0.0-alpha.164 → 3.0.0-alpha.166

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,21 @@
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.166 (2026-07-14)
7
+
8
+ ### Features
9
+
10
+ * **api:** merge-path codegen — stitchStandalone subgraph SDL + [@canonical](https://github.com/canonical) stamping (Phase 2) ([02ae93b](https://github.com/ReventlessDev/reventless-core/commit/02ae93b1c84a116ae47fb656cab17858a5df2c97))
11
+
12
+
13
+ # 3.0.0-alpha.165 (2026-07-13)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/reventless-core
16
+
17
+
18
+
19
+
20
+
6
21
  # 3.0.0-alpha.164 (2026-07-13)
7
22
 
8
23
  **Note:** Version bump only for package @reventlessdev/reventless-core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.164",
3
+ "version": "3.0.0-alpha.166",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,14 +29,14 @@
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
32
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
33
32
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.12",
34
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.11",
35
34
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.11",
35
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
37
+ "@reventlessdev/rescript-ssh2": "1.1.0-alpha.12",
37
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.15",
38
39
  "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
39
- "@reventlessdev/rescript-ssh2": "1.1.0-alpha.12",
40
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.26",
41
41
  "@reventlessdev/reventless-spec": "3.0.0-alpha.76"
42
42
  },
@@ -387,20 +387,13 @@ module Make = (
387
387
  ->Array.reduce(Belt.Set.String.empty, (acc, names) => acc->Belt.Set.String.union(names))
388
388
 
389
389
  let aggregatesOutputs = addEventMappers(allEventTopics, queryEngine)
390
+ // The admin EventCollector subscribes only to the aggregate event topics
391
+ // filtered by its extension-point aggregate names. It is NOT a reader of the
392
+ // admin DcbEventLog stream: ApiFragmentRegistry is now a singleton aggregate
393
+ // (its reactive push is the ApiSchemaPush SideEffect on the aggregate event
394
+ // topic), and the remaining DcbEventLog slice (UiFragmentRegistry) is consumed
395
+ // by the AllStateViewSlices projection, not here.
390
396
  let eventTopics = aggregatesOutputs->Aggregate.filterEventTopics(aggregateNames)
391
- // 2e: also subscribe the admin EventCollector to the admin DcbEventLog's
392
- // event topic (a DynamoDB stream on AWS) so the reactive ApiFragmentRegistry
393
- // single writer receives ApiFragment* events. The EventCollector channel
394
- // already provisions the stream subscription + read IAM for stream-backed
395
- // event-topic resources. Copy the filtered dict so the shared aggregate
396
- // outputs stay untouched.
397
- let eventTopics = switch dcbResult.dcbEventLogOutputs {
398
- | Some(dcbOutputs) =>
399
- let augmented = eventTopics->Dict.copy
400
- augmented->Dict.set(name ++ "DcbEventLog", dcbOutputs.eventTopic)
401
- augmented
402
- | None => eventTopics
403
- }
404
397
 
405
398
  module EventCollectorHelper = MakeEventCollectorHelper(
406
399
  RuntimeEnvironment,
@@ -172,15 +172,6 @@ function Make(RuntimeEnvironment) {
172
172
  let aggregateNames = Stdlib_Array.reduce(extensionPointsOutputs.map(extensionPointOutputs => Belt_SetString.fromArray(extensionPointOutputs.aggregateNames)), undefined, Belt_SetString.union);
173
173
  let aggregatesOutputs = Builder_Helpers$ReventlessCore.addEventMappers(aggregateEventTopics, queryEngine);
174
174
  let eventTopics = Aggregate$ReventlessCore.filterEventTopics(aggregatesOutputs, aggregateNames);
175
- let dcbOutputs = dcbResult.dcbEventLogOutputs;
176
- let eventTopics$1;
177
- if (dcbOutputs !== undefined) {
178
- let augmented = Object.assign({}, eventTopics);
179
- augmented[name + "DcbEventLog"] = dcbOutputs.eventTopic;
180
- eventTopics$1 = augmented;
181
- } else {
182
- eventTopics$1 = eventTopics;
183
- }
184
175
  let AdminEventCollector = EventCollector_Builder$ReventlessCore.Make(RuntimeEnvironment)(EventCollectorChannel);
185
176
  let make = (name, eventTopics, opts) => {
186
177
  let eventCollector = AdminEventCollector.make(name, eventTopics, opts);
@@ -215,9 +206,9 @@ function Make(RuntimeEnvironment) {
215
206
  AdminRuntimeBuilder.forPluginEventCollector(handler, eventTopics, resources, undefined, undefined, eventCollector);
216
207
  });
217
208
  };
218
- let match$1 = make(name, eventTopics$1, opts);
209
+ let match$1 = make(name, eventTopics, opts);
219
210
  let eventCollector = match$1[0];
220
- Output$Pulumi.flatMap(Pulumi.all(match[1]), extensionPointsOutgoingJsonEventsHandlers => connect(eventCollector, eventTopics$1, extensionPointsOutputs, extensionPointsOutgoingJsonEventsHandlers));
211
+ Output$Pulumi.flatMap(Pulumi.all(match[1]), extensionPointsOutgoingJsonEventsHandlers => connect(eventCollector, eventTopics, extensionPointsOutputs, extensionPointsOutgoingJsonEventsHandlers));
221
212
  Stdlib_Option.forEach(dcbResult.dcbRuntimeSetup, dcbRuntimeSetup => dcbRuntimeSetup());
222
213
  return extensionPointsOutputs;
223
214
  });
@@ -172,21 +172,23 @@ let relayBaseQueries = [
172
172
  ]
173
173
 
174
174
  /**
175
- Stitch a base fragment with plugin fragments into a complete GraphQL SDL string.
175
+ Assemble fragments into a complete GraphQL SDL string.
176
176
 
177
- - Base fragment is always first (defines core types + Plugin mutations/queries).
178
- - Plugin fragments add their own types, mutations, and queries.
177
+ - Fragments are processed in order (first wins on collisions).
179
178
  - Collision detection: duplicate type definitions and field names are logged and skipped.
180
- - Relay base types (Node interface, PageInfo) and node query are always injected.
179
+ - Relay base types (Node interface, PageInfo) are always injected; the global
180
+ `node` query only when `includeGlobalNodeQuery` (stitched schemas yes,
181
+ standalone subgraph documents no).
181
182
 
182
183
  Returns the full SDL string ready for use with AppSync or graphql-yoga.
184
+ Public entry points: `stitch` (base + plugins, the whole-schema path) and
185
+ `stitchStandalone` (one fragment as a self-contained subgraph document).
183
186
  */
184
- let stitch = (
185
- ~baseFragment: Reventless.Plugin.apiSchemaFragment,
186
- ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
187
+ let assembleSdl = (
188
+ ~fragments: array<Reventless.Plugin.apiSchemaFragment>,
189
+ ~includeGlobalNodeQuery: bool,
187
190
  ): string => {
188
- let allFragments = Array.concat([baseFragment], pluginFragments)
189
- let parts = allFragments->Array.map(decode)
191
+ let parts = fragments->Array.map(decode)
190
192
 
191
193
  // Collect all type definitions — detect collisions by type name
192
194
  // Start with Relay base types (Node interface, PageInfo)
@@ -229,14 +231,18 @@ let stitch = (
229
231
  )
230
232
 
231
233
  // Collect all query fields — detect collisions by field name
232
- // Start with Relay node query
234
+ // Start with Relay node query (single-resolver-owned: omitted from
235
+ // standalone subgraph documents, where the platform's canonical source
236
+ // owns the global `node` field)
233
237
  let seenQueryFields: Set.t<string> = Set.make()
234
238
  let allQueries: array<string> = []
235
- relayBaseQueries->Array.forEach(field => {
236
- let fieldName = extractLeadingName(field)
237
- seenQueryFields->Set.add(fieldName)
238
- allQueries->Array.push(field)
239
- })
239
+ if includeGlobalNodeQuery {
240
+ relayBaseQueries->Array.forEach(field => {
241
+ let fieldName = extractLeadingName(field)
242
+ seenQueryFields->Set.add(fieldName)
243
+ allQueries->Array.push(field)
244
+ })
245
+ }
240
246
 
241
247
  parts->Array.forEach(({queries}) =>
242
248
  queries->Array.forEach(field => {
@@ -291,6 +297,26 @@ let stitch = (
291
297
  sdlParts->Array.join("\n\n")
292
298
  }
293
299
 
300
+ let stitch = (
301
+ ~baseFragment: Reventless.Plugin.apiSchemaFragment,
302
+ ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
303
+ ): string =>
304
+ assembleSdl(
305
+ ~fragments=Array.concat([baseFragment], pluginFragments),
306
+ ~includeGlobalNodeQuery=true,
307
+ )
308
+
309
+ /**
310
+ Render ONE fragment as a standalone schema document for merge-based
311
+ composition (plugin = subgraph, platform = merge). The relay base types are
312
+ included so the document is self-contained and valid on its own; the global
313
+ `node(id: ID!)` root query is omitted — exactly one source (the platform's
314
+ canonical one) may own that field's resolver, so only the canonical base
315
+ document carries it (via `stitch`).
316
+ */
317
+ let stitchStandalone = (~fragment: Reventless.Plugin.apiSchemaFragment): string =>
318
+ assembleSdl(~fragments=[fragment], ~includeGlobalNodeQuery=false)
319
+
294
320
  let isIdentStart = (line: string): bool =>
295
321
  switch line->String.codePointAt(0) {
296
322
  // A-Z (65-90), a-z (97-122), or `_` (95)
@@ -132,9 +132,8 @@ let relayBaseTypes = [
132
132
 
133
133
  let relayBaseQueries = [` node(id: ID!): Node`];
134
134
 
135
- function stitch(baseFragment, pluginFragments) {
136
- let allFragments = [baseFragment].concat(pluginFragments);
137
- let parts = allFragments.map(decode);
135
+ function assembleSdl(fragments, includeGlobalNodeQuery) {
136
+ let parts = fragments.map(decode);
138
137
  let seenTypeNames = new Set();
139
138
  let allTypes = [];
140
139
  relayBaseTypes.forEach(typeDef => {
@@ -173,11 +172,13 @@ function stitch(baseFragment, pluginFragments) {
173
172
  });
174
173
  let seenQueryFields = new Set();
175
174
  let allQueries = [];
176
- relayBaseQueries.forEach(field => {
177
- let fieldName = extractLeadingName(field);
178
- seenQueryFields.add(fieldName);
179
- allQueries.push(field);
180
- });
175
+ if (includeGlobalNodeQuery) {
176
+ relayBaseQueries.forEach(field => {
177
+ let fieldName = extractLeadingName(field);
178
+ seenQueryFields.add(fieldName);
179
+ allQueries.push(field);
180
+ });
181
+ }
181
182
  parts.forEach(param => {
182
183
  param.queries.forEach(field => {
183
184
  let fieldName = extractLeadingName(field);
@@ -217,6 +218,14 @@ function stitch(baseFragment, pluginFragments) {
217
218
  return sdlParts.join("\n\n");
218
219
  }
219
220
 
221
+ function stitch(baseFragment, pluginFragments) {
222
+ return assembleSdl([baseFragment].concat(pluginFragments), true);
223
+ }
224
+
225
+ function stitchStandalone(fragment) {
226
+ return assembleSdl([fragment], false);
227
+ }
228
+
220
229
  function isIdentStart(line) {
221
230
  let c = line.codePointAt(0);
222
231
  if (c !== undefined) {
@@ -308,7 +317,9 @@ export {
308
317
  extractLeadingName,
309
318
  relayBaseTypes,
310
319
  relayBaseQueries,
320
+ assembleSdl,
311
321
  stitch,
322
+ stitchStandalone,
312
323
  isIdentStart,
313
324
  rootTypeFieldNames,
314
325
  countRootTypeFields,
@@ -261,3 +261,35 @@ describe("GraphQL_Stitcher.subscriptionSources", () => {
261
261
  expect(fields->Array.includes("onPluginStatusChange"))->toBe(true)
262
262
  })
263
263
  })
264
+
265
+ describe("GraphQL_Stitcher.stitchStandalone", () => {
266
+ let pluginFragment = encode({
267
+ types: [
268
+ `type Product implements Node {\n id: ID!\n name: String\n}`,
269
+ `union CommandResult = CommandAccepted | CommandRejected | CommandPending`,
270
+ ],
271
+ mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
272
+ queries: [` Catalog_products: [Product]`],
273
+ subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
274
+ subscriptionSources: [{field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]}],
275
+ })
276
+
277
+ testSync("renders a self-contained subgraph document with relay base types", () => {
278
+ let sdl = stitchStandalone(~fragment=pluginFragment)
279
+ expect(sdl)->toContain("interface Node {")
280
+ expect(sdl)->toContain("type PageInfo {")
281
+ expect(sdl)->toContain("type Product implements Node {")
282
+ expect(sdl)->toContain("Catalog_AddProduct(input: AddInput): CommandResult")
283
+ expect(sdl)->toContain("onCatalog_AddProduct(id: ID): CommandResult")
284
+ })
285
+
286
+ testSync("omits the global node query (the canonical base document owns it)", () => {
287
+ let sdl = stitchStandalone(~fragment=pluginFragment)
288
+ expect(sdl->String.includes("node(id: ID!): Node"))->toBe(false)
289
+ })
290
+
291
+ testSync("stitch keeps injecting the global node query (whole-schema path unchanged)", () => {
292
+ let sdl = stitch(~baseFragment=pluginFragment, ~pluginFragments=[])
293
+ expect(sdl)->toContain("node(id: ID!): Node")
294
+ })
295
+ })
@@ -240,6 +240,38 @@ globalThis.describe("GraphQL_Stitcher.subscriptionSources", () => {
240
240
  });
241
241
  });
242
242
 
243
+ globalThis.describe("GraphQL_Stitcher.stitchStandalone", () => {
244
+ let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
245
+ types: [
246
+ `type Product implements Node {\n id: ID!\n name: String\n}`,
247
+ `union CommandResult = CommandAccepted | CommandRejected | CommandPending`
248
+ ],
249
+ mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
250
+ queries: [` Catalog_products: [Product]`],
251
+ subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
252
+ subscriptionSources: [{
253
+ field: "onCatalog_AddProduct",
254
+ mutations: ["Catalog_AddProduct"]
255
+ }]
256
+ });
257
+ globalThis.test("renders a self-contained subgraph document with relay base types", () => {
258
+ let sdl = GraphQL_Stitcher$ReventlessCore.stitchStandalone(pluginFragment);
259
+ globalThis.expect(sdl).toContain("interface Node {");
260
+ globalThis.expect(sdl).toContain("type PageInfo {");
261
+ globalThis.expect(sdl).toContain("type Product implements Node {");
262
+ globalThis.expect(sdl).toContain("Catalog_AddProduct(input: AddInput): CommandResult");
263
+ globalThis.expect(sdl).toContain("onCatalog_AddProduct(id: ID): CommandResult");
264
+ });
265
+ globalThis.test("omits the global node query (the canonical base document owns it)", () => {
266
+ let sdl = GraphQL_Stitcher$ReventlessCore.stitchStandalone(pluginFragment);
267
+ globalThis.expect(sdl.includes("node(id: ID!): Node")).toBe(false);
268
+ });
269
+ globalThis.test("stitch keeps injecting the global node query (whole-schema path unchanged)", () => {
270
+ let sdl = GraphQL_Stitcher$ReventlessCore.stitch(pluginFragment, []);
271
+ globalThis.expect(sdl).toContain("node(id: ID!): Node");
272
+ });
273
+ });
274
+
243
275
  export {
244
276
  fullSchemaSdl,
245
277
  collapsedSchemaSdl,