@reventlessdev/reventless-core 3.0.0-alpha.165 → 3.0.0-alpha.167

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 +15 -0
  2. package/package.json +7 -7
  3. package/src/admin/AdminApi.res +9 -62
  4. package/src/admin/AdminApi.res.mjs +6 -28
  5. package/src/components/Api/GraphQL_Stitcher.res +41 -123
  6. package/src/components/Api/GraphQL_Stitcher.res.mjs +15 -94
  7. package/tests/api/GraphQL_StitcherTest.res +32 -198
  8. package/tests/api/GraphQL_StitcherTest.res.mjs +32 -0
  9. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res +0 -108
  10. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res.mjs +0 -190
  11. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res +0 -71
  12. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res.mjs +0 -92
  13. package/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res +0 -63
  14. package/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res.mjs +0 -108
  15. package/src/admin/ApiFragmentRegistry/ApiFragmentsReadModelSpec.res +0 -23
  16. package/src/admin/ApiFragmentRegistry/ApiFragmentsReadModelSpec.res.mjs +0 -43
  17. package/src/admin/Platform_ApiFragmentsApi.res +0 -32
  18. package/src/admin/Platform_ApiFragmentsApi.res.mjs +0 -52
  19. package/src/components/Api/Api_Builder.res +0 -40
  20. package/src/components/Api/Api_Builder.res.mjs +0 -33
  21. package/src/components/Api/Api_Operations.res +0 -13
  22. package/src/components/Api/Api_Operations.res.mjs +0 -12
  23. package/src/components/Api/GraphQL_PushPlanner.res +0 -65
  24. package/src/components/Api/GraphQL_PushPlanner.res.mjs +0 -46
  25. package/tests/admin/Platform_ApiFragmentsApiTest.res +0 -66
  26. package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +0 -68
  27. package/tests/api/GraphQL_PushPlannerTest.res +0 -62
  28. package/tests/api/GraphQL_PushPlannerTest.res.mjs +0 -80
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.167 (2026-07-14)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-core
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.166 (2026-07-14)
15
+
16
+ ### Features
17
+
18
+ * **api:** merge-path codegen — stitchStandalone subgraph SDL + [@canonical](https://github.com/canonical) stamping (Phase 2) ([02ae93b](https://github.com/ReventlessDev/reventless-core/commit/02ae93b1c84a116ae47fb656cab17858a5df2c97))
19
+
20
+
6
21
  # 3.0.0-alpha.165 (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.165",
3
+ "version": "3.0.0-alpha.167",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,17 +28,17 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
- "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
32
31
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.12",
32
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
33
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.11",
34
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
35
34
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.11",
36
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
35
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
37
36
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.12",
37
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
38
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.15",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.76",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.26"
39
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.26",
40
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.99",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.76"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "^12.3.0",
@@ -9,46 +9,14 @@ let cloneMutationEntry: mutationSchemaEntry = {
9
9
  description: "Clone the system to a specific point in time",
10
10
  }
11
11
 
12
- // ApiFragmentRegistry admin mutations — the real GraphQL surface of the ApiFragmentRegistry
13
- // singleton aggregate: the plugin / standalone-service deploy calls
14
- // `Platform_ApiFragmentRegistry_RegisterApiFragment` / `..._DeregisterApiFragment` as a SigV4
15
- // system caller. Derived from the aggregate's `commandSchema` exactly like the Plugin aggregate's
16
- // entries (`PluginBaseFragment.pluginAggregateMutationEntries`) — one `Platform_<Spec>_<Command>`
17
- // field per non-`@noApi` variant (`RecordApiFragmentPush` is `@noApi`, filtered out). The matching
18
- // resolver wiring is fired by `Plugin_Helpers.registerAdminAggregateMutations` over the same
19
- // `~aggregates` list in `Platform_Admin.construct`, so the SDL field and the auto-bound resolver
20
- // carry byte-identical names.
21
- let apiFragmentRegistryMutationEntries: array<mutationSchemaEntry> = {
22
- let commandSchema = ApiFragmentRegistrySpec.commandSchema->S.castToUnknown
23
- let constructorNames = Reventless.DcbTag.extractAllVariantNames(ApiFragmentRegistrySpec.commandSchema)
24
- let filteredConstructorNames = ApiNoApiHelpers.filterNoApiVariants(constructorNames, commandSchema)
25
- let fieldNames =
26
- filteredConstructorNames->Array.map(cname =>
27
- Api_Naming.adminField(~name=ApiFragmentRegistrySpec.name ++ "_" ++ cname)
28
- )
29
- if fieldNames->Array.length === 0 {
30
- []
31
- } else {
32
- [
33
- {
34
- ReventlessInfra.Api.fieldNames,
35
- commandSchema,
36
- },
37
- ]
38
- }
39
- }
40
-
41
12
  // Aggregate-derived admin mutations come from `PluginBaseFragment.pluginAggregateMutationEntries`
42
- // (Plugin aggregate) + `apiFragmentRegistryMutationEntries` (ApiFragmentRegistry aggregate) so they
43
- // end up in the stitched SDL AND in the static `baseFragment` the AWS path pushes directly. The
44
- // corresponding resolver wiring is fired in parallel by
45
- // `Plugin_Helpers.registerAdminAggregateMutations` from `Platform_Admin.construct` over the same
46
- // `~aggregates` list.
13
+ // (Plugin aggregate) so they end up in the admin base document. The corresponding resolver wiring
14
+ // is fired in parallel by `Plugin_Helpers.registerAdminAggregateMutations` from
15
+ // `Platform_Admin.construct` over the same `~aggregates` list.
16
+ // (The ApiFragmentRegistry aggregate's entries were retired with the merged-API cutover —
17
+ // schema composition is push-free, so no deploy-time register/deregister surface exists.)
47
18
  let mutationEntries = (~cloner: bool) => {
48
- let base = Array.concat(
49
- PluginBaseFragment.pluginAggregateMutationEntries,
50
- apiFragmentRegistryMutationEntries,
51
- )
19
+ let base = PluginBaseFragment.pluginAggregateMutationEntries
52
20
  if cloner {
53
21
  Array.concat(base, [cloneMutationEntry])
54
22
  } else {
@@ -58,17 +26,6 @@ let mutationEntries = (~cloner: bool) => {
58
26
 
59
27
  let queryEntries = PluginBaseFragment.queryEntries
60
28
 
61
- // Admin fields a deploy-time system caller (machine credentials — SigV4/IAM on AWS) invokes: the
62
- // two ApiFragmentRegistry mutations (the deploy's register/deregister write path) and
63
- // `Platform_ApiFragments` (the deploy waiter's push-status poll). Provider-neutral by design — the
64
- // AWS adapter threads these into `injectAwsAuthAll`'s `~iamFieldNames` so only these fields carry
65
- // the dual-auth (`@aws_cognito_user_pools @aws_iam`) directive; every other admin field stays
66
- // Cognito-only. Derived from the same mutation entries so the names never drift from the SDL.
67
- let systemCallerFieldNames =
68
- apiFragmentRegistryMutationEntries
69
- ->Array.flatMap(e => e.fieldNames)
70
- ->Array.concat([Platform_ApiFragmentsApi.queryFieldName])
71
-
72
29
  // UIFragment lifecycle Source C subscriptions.
73
30
  // Mutations are called by the backend when UIFragment events occur; the
74
31
  // subscription→mutation fan-in is carried as neutral `subscriptionSource`
@@ -132,35 +89,25 @@ let baseFragment = (~cloner: bool) => {
132
89
  // matching SDL fields, AppSync's CreateResolver fails with "Type not found".
133
90
  let (pluginAggregateSubscriptionFields, pluginAggregateSubscriptionSources) =
134
91
  Plugin_SubscriptionSchema.sourceCFields(~mutationEntries=PluginBaseFragment.pluginAggregateMutationEntries)
135
- // The ApiFragmentRegistry aggregate's mutations go through the same CommandGenerator
136
- // auto-flow, which creates an `on<field>` subscription resolver per mutation. On the
137
- // static-push platform (AWS) those resolvers orphan unless the fields are declared in the
138
- // pushed baseFragment — so generate them here too, symmetric with the Plugin aggregate.
139
- let (apiFragmentAggregateSubscriptionFields, apiFragmentAggregateSubscriptionSources) =
140
- Plugin_SubscriptionSchema.sourceCFields(~mutationEntries=apiFragmentRegistryMutationEntries)
141
92
  GraphQL_Stitcher.encode({
142
93
  types: parts.types
143
94
  ->Array.concat(uiFragmentSubscriptionTypes)
144
95
  ->Array.concat(pluginStatusSubscriptionTypes)
145
96
  ->Array.concat(Platform_ComponentDefinitionsApi.sdlTypes)
146
- ->Array.concat(Platform_UIFragmentsApi.sdlTypes)
147
- ->Array.concat(Platform_ApiFragmentsApi.sdlTypes),
97
+ ->Array.concat(Platform_UIFragmentsApi.sdlTypes),
148
98
  queries: Array.concat(
149
99
  parts.queries,
150
100
  [
151
101
  Platform_ComponentDefinitionsApi.sdlQueryField,
152
102
  Platform_UIFragmentsApi.sdlQueryField,
153
- Platform_ApiFragmentsApi.sdlQueryField,
154
103
  ],
155
104
  ),
156
105
  mutations: parts.mutations
157
106
  ->Array.concat(uiFragmentMutationFields)
158
107
  ->Array.concat(pluginStatusMutationFields),
159
108
  subscriptions: [uiFragmentSubscriptionField, pluginStatusSubscriptionField]
160
- ->Array.concat(pluginAggregateSubscriptionFields)
161
- ->Array.concat(apiFragmentAggregateSubscriptionFields),
109
+ ->Array.concat(pluginAggregateSubscriptionFields),
162
110
  subscriptionSources: [uiFragmentSubscriptionSource, pluginStatusSubscriptionSource]
163
- ->Array.concat(pluginAggregateSubscriptionSources)
164
- ->Array.concat(apiFragmentAggregateSubscriptionSources),
111
+ ->Array.concat(pluginAggregateSubscriptionSources),
165
112
  })
166
113
  }
@@ -1,14 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
- import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
4
  import * as Api_Naming$ReventlessCore from "../components/Api/Api_Naming.res.mjs";
6
- import * as ApiNoApiHelpers$ReventlessCore from "../components/Api/ApiNoApiHelpers.res.mjs";
7
5
  import * as GraphQL_Stitcher$ReventlessCore from "../components/Api/GraphQL_Stitcher.res.mjs";
8
6
  import * as PluginBaseFragment$ReventlessCore from "../plugin/api/PluginBaseFragment.res.mjs";
9
- import * as ApiFragmentRegistrySpec$ReventlessCore from "./ApiFragmentRegistry/ApiFragmentRegistrySpec.res.mjs";
10
7
  import * as Platform_UIFragmentsApi$ReventlessCore from "./Platform_UIFragmentsApi.res.mjs";
11
- import * as Platform_ApiFragmentsApi$ReventlessCore from "./Platform_ApiFragmentsApi.res.mjs";
12
8
  import * as GraphQL_FragmentGenerator$ReventlessCore from "../components/Api/GraphQL_FragmentGenerator.res.mjs";
13
9
  import * as Plugin_SubscriptionSchema$ReventlessCore from "../plugin/component/Plugin_SubscriptionSchema.res.mjs";
14
10
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "./Platform_ComponentDefinitionsApi.res.mjs";
@@ -27,28 +23,14 @@ let cloneMutationEntry = {
27
23
  description: cloneMutationEntry_description
28
24
  };
29
25
 
30
- let constructorNames = DcbTag$Reventless.extractAllVariantNames(ApiFragmentRegistrySpec$ReventlessCore.commandSchema);
31
-
32
- let filteredConstructorNames = ApiNoApiHelpers$ReventlessCore.filterNoApiVariants(constructorNames, ApiFragmentRegistrySpec$ReventlessCore.commandSchema);
33
-
34
- let fieldNames = filteredConstructorNames.map(cname => Api_Naming$ReventlessCore.adminField(ApiFragmentRegistrySpec$ReventlessCore.name + "_" + cname));
35
-
36
- let apiFragmentRegistryMutationEntries = fieldNames.length === 0 ? [] : [{
37
- fieldNames: fieldNames,
38
- commandSchema: ApiFragmentRegistrySpec$ReventlessCore.commandSchema
39
- }];
40
-
41
26
  function mutationEntries(cloner) {
42
- let base = PluginBaseFragment$ReventlessCore.pluginAggregateMutationEntries.concat(apiFragmentRegistryMutationEntries);
43
27
  if (cloner) {
44
- return base.concat([cloneMutationEntry]);
28
+ return PluginBaseFragment$ReventlessCore.pluginAggregateMutationEntries.concat([cloneMutationEntry]);
45
29
  } else {
46
- return base;
30
+ return PluginBaseFragment$ReventlessCore.pluginAggregateMutationEntries;
47
31
  }
48
32
  }
49
33
 
50
- let systemCallerFieldNames = apiFragmentRegistryMutationEntries.flatMap(e => e.fieldNames).concat([Platform_ApiFragmentsApi$ReventlessCore.queryFieldName]);
51
-
52
34
  let uiFragmentSubscriptionTypes = [
53
35
  `enum UIFragmentChangeKind {\n Registered\n Updated\n Deregistered\n}`,
54
36
  `type UIFragmentChangeEvent {\n pluginId: ID!\n changeKind: UIFragmentChangeKind!\n manifest: String\n}`
@@ -93,23 +75,21 @@ function baseFragment(cloner) {
93
75
  let base = GraphQL_FragmentGenerator$ReventlessCore.generate(mutationEntries(cloner), PluginBaseFragment$ReventlessCore.queryEntries);
94
76
  let parts = GraphQL_Stitcher$ReventlessCore.decode(base);
95
77
  let match = Plugin_SubscriptionSchema$ReventlessCore.sourceCFields(PluginBaseFragment$ReventlessCore.pluginAggregateMutationEntries);
96
- let match$1 = Plugin_SubscriptionSchema$ReventlessCore.sourceCFields(apiFragmentRegistryMutationEntries);
97
78
  return GraphQL_Stitcher$ReventlessCore.encode({
98
- types: parts.types.concat(uiFragmentSubscriptionTypes).concat(pluginStatusSubscriptionTypes).concat(Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes).concat(Platform_UIFragmentsApi$ReventlessCore.sdlTypes).concat(Platform_ApiFragmentsApi$ReventlessCore.sdlTypes),
79
+ types: parts.types.concat(uiFragmentSubscriptionTypes).concat(pluginStatusSubscriptionTypes).concat(Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes).concat(Platform_UIFragmentsApi$ReventlessCore.sdlTypes),
99
80
  mutations: parts.mutations.concat(uiFragmentMutationFields).concat(pluginStatusMutationFields),
100
81
  queries: parts.queries.concat([
101
82
  Platform_ComponentDefinitionsApi$ReventlessCore.sdlQueryField,
102
- Platform_UIFragmentsApi$ReventlessCore.sdlQueryField,
103
- Platform_ApiFragmentsApi$ReventlessCore.sdlQueryField
83
+ Platform_UIFragmentsApi$ReventlessCore.sdlQueryField
104
84
  ]),
105
85
  subscriptions: [
106
86
  uiFragmentSubscriptionField,
107
87
  pluginStatusSubscriptionField
108
- ].concat(match[0]).concat(match$1[0]),
88
+ ].concat(match[0]),
109
89
  subscriptionSources: [
110
90
  uiFragmentSubscriptionSource,
111
91
  pluginStatusSubscriptionSource
112
- ].concat(match[1]).concat(match$1[1])
92
+ ].concat(match[1])
113
93
  });
114
94
  }
115
95
 
@@ -118,10 +98,8 @@ let queryEntries = PluginBaseFragment$ReventlessCore.queryEntries;
118
98
  export {
119
99
  cloneArgsSchema,
120
100
  cloneMutationEntry,
121
- apiFragmentRegistryMutationEntries,
122
101
  mutationEntries,
123
102
  queryEntries,
124
- systemCallerFieldNames,
125
103
  uiFragmentSubscriptionTypes,
126
104
  uiFragmentMutationFields,
127
105
  uiFragmentSubscriptionField,
@@ -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,115 +297,27 @@ let stitch = (
291
297
  sdlParts->Array.join("\n\n")
292
298
  }
293
299
 
294
- let isIdentStart = (line: string): bool =>
295
- switch line->String.codePointAt(0) {
296
- // A-Z (65-90), a-z (97-122), or `_` (95)
297
- | Some(c) => (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 95
298
- | None => false
299
- }
300
-
301
- /**
302
- List the field NAMES defined inside a root object type (`Mutation` / `Query` /
303
- `Subscription`) of a full SDL string.
304
-
305
- This is the identity-aware primitive underlying every schema-clobber guard:
306
- `countRootTypeFields` is just its length, and the deploy-time repair / shrink
307
- guards compare the returned NAME SETS rather than cardinalities so they catch
308
- field *swaps* and equal-cardinality drift that a count comparison misses.
309
-
310
- Heuristic but adequate: root operation fields are flat single-line declarations
311
- (`name(args): T` or `name: T`), so we slice the `type <typeName> { … }` block at
312
- its first `{`/`}` and read the leading identifier of each indented line that
313
- begins with an identifier character, ignoring directive-only lines (`@…`),
314
- comments (`#…`) and braces.
315
- */
316
- let rootTypeFieldNames = (~sdl: string, ~typeName: string): array<string> => {
317
- let marker = `type ${typeName}`
318
- switch sdl->String.indexOfOpt(marker) {
319
- | None => []
320
- | Some(typeStart) =>
321
- let rest = sdl->String.slice(~start=typeStart)
322
- switch (rest->String.indexOfOpt("{"), rest->String.indexOfOpt("}")) {
323
- | (Some(openIdx), Some(closeIdx)) if closeIdx > openIdx =>
324
- rest
325
- ->String.slice(~start=openIdx + 1, ~end=closeIdx)
326
- ->String.split("\n")
327
- ->Array.filterMap(line => {
328
- let trimmed = line->String.trim
329
- if trimmed->String.length > 0 && isIdentStart(trimmed) {
330
- // `name(args): T` → extractLeadingName strips the arg list; `name: T`
331
- // (no args) leaves a trailing colon on the token — drop it so a field
332
- // reads the same whether or not it takes arguments.
333
- let name = extractLeadingName(trimmed)
334
- let name = name->String.endsWith(":")
335
- ? name->String.slice(~start=0, ~end=name->String.length - 1)
336
- : name
337
- name->String.length > 0 ? Some(name) : None
338
- } else {
339
- None
340
- }
341
- })
342
- | _ => []
343
- }
344
- }
345
- }
346
-
347
- /**
348
- Count the field definitions inside a root object type (`Mutation` / `Query`) of a
349
- full SDL string. Length of `rootTypeFieldNames`.
350
- */
351
- let countRootTypeFields = (~sdl: string, ~typeName: string): int =>
352
- rootTypeFieldNames(~sdl, ~typeName)->Array.length
353
-
354
- // The three GraphQL root operation types every clobber guard reasons over.
355
- let rootTypeNames = ["Mutation", "Query", "Subscription"]
356
-
357
- /**
358
- Union of every root-operation field name across `Mutation` + `Query` +
359
- `Subscription`. The identity set the deploy-time repair and shrink guards diff.
360
- */
361
- let allRootFieldNames = (~sdl: string): array<string> =>
362
- rootTypeNames->Array.flatMap(typeName => rootTypeFieldNames(~sdl, ~typeName))
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
+ )
363
308
 
364
309
  /**
365
- Root-operation field names present in `expectedSdl` but MISSING from `liveSdl`
366
- (across Mutation + Query + Subscription). Non-empty ⇒ the live schema is not a
367
- superset of what we expect and a repair push is warranted — this heals field
368
- *swaps* and equal-cardinality drift that a bare count comparison would skip.
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`).
369
316
  */
370
- let missingRootFields = (~expectedSdl: string, ~liveSdl: string): array<string> => {
371
- let liveSet = allRootFieldNames(~sdl=liveSdl)->Set.fromArray
372
- allRootFieldNames(~sdl=expectedSdl)->Array.filter(name => !(liveSet->Set.has(name)))
373
- }
374
-
375
- /**
376
- Decide whether replacing the live schema (`currentSdl`) with `newSdl` would drop
377
- root-operation fields the pushing stack does not carry — i.e. an almost-certainly
378
- transient/incomplete stitch rather than an intentional removal.
379
-
380
- Identity-based (across `Mutation` + `Query` + `Subscription`):
381
- - When `newSdl`'s field-name set is a SUPERSET of the live set — nothing is
382
- dropped — it is purely additive (or unchanged) and never a clobber, so this
383
- returns `false` even when the pushing stack is legitimately adding its own
384
- fields.
385
- - Otherwise it falls back to the cardinality threshold on the union of the three
386
- root types: `true` only when the new total drops below `threshold` × the
387
- current total. A small intentional removal (above threshold) is allowed.
317
+ let stitchStandalone = (~fragment: Reventless.Plugin.apiSchemaFragment): string =>
318
+ assembleSdl(~fragments=[fragment], ~includeGlobalNodeQuery=false)
388
319
 
389
- When the current schema has no countable root fields (e.g. first deploy, or
390
- introspection was unavailable so `currentSdl` is empty) it returns `false` so the
391
- initial push always proceeds.
392
- */
393
- let isCatastrophicSchemaShrink = (~currentSdl: string, ~newSdl: string, ~threshold: float): bool => {
394
- let currentNames = allRootFieldNames(~sdl=currentSdl)
395
- let newSet = allRootFieldNames(~sdl=newSdl)->Set.fromArray
396
- let dropped = currentNames->Array.filter(name => !(newSet->Set.has(name)))
397
- if dropped->Array.length == 0 {
398
- // Additive or unchanged push — the new schema keeps every live field.
399
- false
400
- } else {
401
- let current = currentNames->Array.length
402
- let next = allRootFieldNames(~sdl=newSdl)->Array.length
403
- current > 0 && next->Int.toFloat < current->Int.toFloat *. threshold
404
- }
405
- }
320
+ // (The schema-clobber guard family — rootTypeFieldNames / countRootTypeFields /
321
+ // missingRootFields / isCatastrophicSchemaShrink — was retired with the push
322
+ // machinery: under merged-API composition every source API is a single writer,
323
+ // so no whole-replace push exists to guard.)
@@ -3,7 +3,6 @@
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
- import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
7
6
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
8
7
 
9
8
  let log = Logger$ReventlessCore.fromEnv();
@@ -132,9 +131,8 @@ let relayBaseTypes = [
132
131
 
133
132
  let relayBaseQueries = [` node(id: ID!): Node`];
134
133
 
135
- function stitch(baseFragment, pluginFragments) {
136
- let allFragments = [baseFragment].concat(pluginFragments);
137
- let parts = allFragments.map(decode);
134
+ function assembleSdl(fragments, includeGlobalNodeQuery) {
135
+ let parts = fragments.map(decode);
138
136
  let seenTypeNames = new Set();
139
137
  let allTypes = [];
140
138
  relayBaseTypes.forEach(typeDef => {
@@ -173,11 +171,13 @@ function stitch(baseFragment, pluginFragments) {
173
171
  });
174
172
  let seenQueryFields = new Set();
175
173
  let allQueries = [];
176
- relayBaseQueries.forEach(field => {
177
- let fieldName = extractLeadingName(field);
178
- seenQueryFields.add(fieldName);
179
- allQueries.push(field);
180
- });
174
+ if (includeGlobalNodeQuery) {
175
+ relayBaseQueries.forEach(field => {
176
+ let fieldName = extractLeadingName(field);
177
+ seenQueryFields.add(fieldName);
178
+ allQueries.push(field);
179
+ });
180
+ }
181
181
  parts.forEach(param => {
182
182
  param.queries.forEach(field => {
183
183
  let fieldName = extractLeadingName(field);
@@ -217,86 +217,12 @@ function stitch(baseFragment, pluginFragments) {
217
217
  return sdlParts.join("\n\n");
218
218
  }
219
219
 
220
- function isIdentStart(line) {
221
- let c = line.codePointAt(0);
222
- if (c !== undefined) {
223
- if (c >= 65 && c <= 90 || c >= 97 && c <= 122) {
224
- return true;
225
- } else {
226
- return c === 95;
227
- }
228
- } else {
229
- return false;
230
- }
231
- }
232
-
233
- function rootTypeFieldNames(sdl, typeName) {
234
- let marker = `type ` + typeName;
235
- let typeStart = Stdlib_String.indexOfOpt(sdl, marker);
236
- if (typeStart === undefined) {
237
- return [];
238
- }
239
- let rest = sdl.slice(typeStart);
240
- let match = Stdlib_String.indexOfOpt(rest, "{");
241
- let match$1 = Stdlib_String.indexOfOpt(rest, "}");
242
- if (match !== undefined) {
243
- if (match$1 !== undefined) {
244
- if (match$1 > match) {
245
- return Stdlib_Array.filterMap(rest.slice(match + 1 | 0, match$1).split("\n"), line => {
246
- let trimmed = line.trim();
247
- if (!(trimmed.length > 0 && isIdentStart(trimmed))) {
248
- return;
249
- }
250
- let name = extractLeadingName(trimmed);
251
- let name$1 = name.endsWith(":") ? name.slice(0, name.length - 1 | 0) : name;
252
- if (name$1.length > 0) {
253
- return name$1;
254
- }
255
- });
256
- } else {
257
- return [];
258
- }
259
- } else {
260
- return [];
261
- }
262
- } else {
263
- return [];
264
- }
265
- }
266
-
267
- function countRootTypeFields(sdl, typeName) {
268
- return rootTypeFieldNames(sdl, typeName).length;
269
- }
270
-
271
- let rootTypeNames = [
272
- "Mutation",
273
- "Query",
274
- "Subscription"
275
- ];
276
-
277
- function allRootFieldNames(sdl) {
278
- return rootTypeNames.flatMap(typeName => rootTypeFieldNames(sdl, typeName));
279
- }
280
-
281
- function missingRootFields(expectedSdl, liveSdl) {
282
- let liveSet = new Set(allRootFieldNames(liveSdl));
283
- return allRootFieldNames(expectedSdl).filter(name => !liveSet.has(name));
220
+ function stitch(baseFragment, pluginFragments) {
221
+ return assembleSdl([baseFragment].concat(pluginFragments), true);
284
222
  }
285
223
 
286
- function isCatastrophicSchemaShrink(currentSdl, newSdl, threshold) {
287
- let currentNames = allRootFieldNames(currentSdl);
288
- let newSet = new Set(allRootFieldNames(newSdl));
289
- let dropped = currentNames.filter(name => !newSet.has(name));
290
- if (dropped.length === 0) {
291
- return false;
292
- }
293
- let current = currentNames.length;
294
- let next = allRootFieldNames(newSdl).length;
295
- if (current > 0) {
296
- return next < current * threshold;
297
- } else {
298
- return false;
299
- }
224
+ function stitchStandalone(fragment) {
225
+ return assembleSdl([fragment], false);
300
226
  }
301
227
 
302
228
  export {
@@ -308,13 +234,8 @@ export {
308
234
  extractLeadingName,
309
235
  relayBaseTypes,
310
236
  relayBaseQueries,
237
+ assembleSdl,
311
238
  stitch,
312
- isIdentStart,
313
- rootTypeFieldNames,
314
- countRootTypeFields,
315
- rootTypeNames,
316
- allRootFieldNames,
317
- missingRootFields,
318
- isCatastrophicSchemaShrink,
239
+ stitchStandalone,
319
240
  }
320
241
  /* log Not a pure module */