@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 +15 -0
- package/package.json +3 -3
- package/src/admin/Platform_Admin.res +6 -13
- package/src/admin/Platform_Admin.res.mjs +2 -11
- package/src/components/Api/GraphQL_Stitcher.res +41 -15
- package/src/components/Api/GraphQL_Stitcher.res.mjs +19 -8
- package/tests/api/GraphQL_StitcherTest.res +32 -0
- package/tests/api/GraphQL_StitcherTest.res.mjs +32 -0
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
175
|
+
Assemble fragments into a complete GraphQL SDL string.
|
|
176
176
|
|
|
177
|
-
-
|
|
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)
|
|
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
|
|
185
|
-
~
|
|
186
|
-
~
|
|
187
|
+
let assembleSdl = (
|
|
188
|
+
~fragments: array<Reventless.Plugin.apiSchemaFragment>,
|
|
189
|
+
~includeGlobalNodeQuery: bool,
|
|
187
190
|
): string => {
|
|
188
|
-
let
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
136
|
-
let
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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,
|