@reventlessdev/reventless-core 3.0.0-alpha.143 → 3.0.0-alpha.145
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 +14 -0
- package/package.json +7 -7
- package/src/components/Api/GraphQL_Stitcher.res +81 -28
- package/src/components/Api/GraphQL_Stitcher.res.mjs +52 -12
- package/src/components/Dcb/Dcb_Builder.res +15 -5
- package/src/components/Dcb/Dcb_Builder.res.mjs +8 -3
- package/tests/api/GraphQL_StitcherTest.res +93 -0
- package/tests/api/GraphQL_StitcherTest.res.mjs +74 -0
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.145 (2026-07-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **reventless-aws:** stop admin-base schema clobber of split-mode DomainApi ([afce85f](https://github.com/ReventlessDev/reventless-core/commit/afce85fa57474735cea3dcb1c2a151c2d8804f0e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.144 (2026-07-07)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **reventless-aws:** thread inferred DCB scope into the deployed command handler ([4d8327f](https://github.com/ReventlessDev/reventless-core/commit/4d8327fad8659a1cde8c36098c72392737437af1))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.143 (2026-07-06)
|
|
7
21
|
|
|
8
22
|
### Features
|
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.145",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.25",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
|
|
33
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
|
|
34
|
-
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
|
|
35
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.6",
|
|
34
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
|
|
35
|
+
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
37
|
-
"@reventlessdev/
|
|
38
|
-
"@reventlessdev/reventless-
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.90",
|
|
39
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.68",
|
|
39
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.24",
|
|
40
|
-
"@reventlessdev/rescript-uuid": "1.1.0-alpha.14"
|
|
41
|
-
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.11"
|
|
41
|
+
"@reventlessdev/rescript-uuid": "1.1.0-alpha.14"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "^12.3.0",
|
|
@@ -218,21 +218,24 @@ let isIdentStart = (line: string): bool =>
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
|
-
|
|
222
|
-
full SDL string.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
List the field NAMES defined inside a root object type (`Mutation` / `Query` /
|
|
222
|
+
`Subscription`) of a full SDL string.
|
|
223
|
+
|
|
224
|
+
This is the identity-aware primitive underlying every schema-clobber guard:
|
|
225
|
+
`countRootTypeFields` is just its length, and the deploy-time repair / shrink
|
|
226
|
+
guards compare the returned NAME SETS rather than cardinalities so they catch
|
|
227
|
+
field *swaps* and equal-cardinality drift that a count comparison misses.
|
|
228
|
+
|
|
229
|
+
Heuristic but adequate: root operation fields are flat single-line declarations
|
|
230
|
+
(`name(args): T` or `name: T`), so we slice the `type <typeName> { … }` block at
|
|
231
|
+
its first `{`/`}` and read the leading identifier of each indented line that
|
|
232
|
+
begins with an identifier character, ignoring directive-only lines (`@…`),
|
|
233
|
+
comments (`#…`) and braces.
|
|
231
234
|
*/
|
|
232
|
-
let
|
|
235
|
+
let rootTypeFieldNames = (~sdl: string, ~typeName: string): array<string> => {
|
|
233
236
|
let marker = `type ${typeName}`
|
|
234
237
|
switch sdl->String.indexOfOpt(marker) {
|
|
235
|
-
| None =>
|
|
238
|
+
| None => []
|
|
236
239
|
| Some(typeStart) =>
|
|
237
240
|
let rest = sdl->String.slice(~start=typeStart)
|
|
238
241
|
switch (rest->String.indexOfOpt("{"), rest->String.indexOfOpt("}")) {
|
|
@@ -240,32 +243,82 @@ let countRootTypeFields = (~sdl: string, ~typeName: string): int => {
|
|
|
240
243
|
rest
|
|
241
244
|
->String.slice(~start=openIdx + 1, ~end=closeIdx)
|
|
242
245
|
->String.split("\n")
|
|
243
|
-
->Array.
|
|
246
|
+
->Array.filterMap(line => {
|
|
244
247
|
let trimmed = line->String.trim
|
|
245
|
-
trimmed->String.length > 0 && isIdentStart(trimmed)
|
|
248
|
+
if trimmed->String.length > 0 && isIdentStart(trimmed) {
|
|
249
|
+
// `name(args): T` → extractLeadingName strips the arg list; `name: T`
|
|
250
|
+
// (no args) leaves a trailing colon on the token — drop it so a field
|
|
251
|
+
// reads the same whether or not it takes arguments.
|
|
252
|
+
let name = extractLeadingName(trimmed)
|
|
253
|
+
let name = name->String.endsWith(":")
|
|
254
|
+
? name->String.slice(~start=0, ~end=name->String.length - 1)
|
|
255
|
+
: name
|
|
256
|
+
name->String.length > 0 ? Some(name) : None
|
|
257
|
+
} else {
|
|
258
|
+
None
|
|
259
|
+
}
|
|
246
260
|
})
|
|
247
|
-
|
|
248
|
-
| _ => 0
|
|
261
|
+
| _ => []
|
|
249
262
|
}
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
|
|
266
|
+
/**
|
|
267
|
+
Count the field definitions inside a root object type (`Mutation` / `Query`) of a
|
|
268
|
+
full SDL string. Length of `rootTypeFieldNames`.
|
|
269
|
+
*/
|
|
270
|
+
let countRootTypeFields = (~sdl: string, ~typeName: string): int =>
|
|
271
|
+
rootTypeFieldNames(~sdl, ~typeName)->Array.length
|
|
272
|
+
|
|
273
|
+
// The three GraphQL root operation types every clobber guard reasons over.
|
|
274
|
+
let rootTypeNames = ["Mutation", "Query", "Subscription"]
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
Union of every root-operation field name across `Mutation` + `Query` +
|
|
278
|
+
`Subscription`. The identity set the deploy-time repair and shrink guards diff.
|
|
279
|
+
*/
|
|
280
|
+
let allRootFieldNames = (~sdl: string): array<string> =>
|
|
281
|
+
rootTypeNames->Array.flatMap(typeName => rootTypeFieldNames(~sdl, ~typeName))
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
Root-operation field names present in `expectedSdl` but MISSING from `liveSdl`
|
|
285
|
+
(across Mutation + Query + Subscription). Non-empty ⇒ the live schema is not a
|
|
286
|
+
superset of what we expect and a repair push is warranted — this heals field
|
|
287
|
+
*swaps* and equal-cardinality drift that a bare count comparison would skip.
|
|
288
|
+
*/
|
|
289
|
+
let missingRootFields = (~expectedSdl: string, ~liveSdl: string): array<string> => {
|
|
290
|
+
let liveSet = allRootFieldNames(~sdl=liveSdl)->Set.fromArray
|
|
291
|
+
allRootFieldNames(~sdl=expectedSdl)->Array.filter(name => !(liveSet->Set.has(name)))
|
|
292
|
+
}
|
|
293
|
+
|
|
253
294
|
/**
|
|
254
295
|
Decide whether replacing the live schema (`currentSdl`) with `newSdl` would drop
|
|
255
|
-
|
|
296
|
+
root-operation fields the pushing stack does not carry — i.e. an almost-certainly
|
|
256
297
|
transient/incomplete stitch rather than an intentional removal.
|
|
257
298
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
the
|
|
299
|
+
Identity-based (across `Mutation` + `Query` + `Subscription`):
|
|
300
|
+
- When `newSdl`'s field-name set is a SUPERSET of the live set — nothing is
|
|
301
|
+
dropped — it is purely additive (or unchanged) and never a clobber, so this
|
|
302
|
+
returns `false` even when the pushing stack is legitimately adding its own
|
|
303
|
+
fields.
|
|
304
|
+
- Otherwise it falls back to the cardinality threshold on the union of the three
|
|
305
|
+
root types: `true` only when the new total drops below `threshold` × the
|
|
306
|
+
current total. A small intentional removal (above threshold) is allowed.
|
|
307
|
+
|
|
308
|
+
When the current schema has no countable root fields (e.g. first deploy, or
|
|
309
|
+
introspection was unavailable so `currentSdl` is empty) it returns `false` so the
|
|
310
|
+
initial push always proceeds.
|
|
262
311
|
*/
|
|
263
312
|
let isCatastrophicSchemaShrink = (~currentSdl: string, ~newSdl: string, ~threshold: float): bool => {
|
|
264
|
-
let
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
313
|
+
let currentNames = allRootFieldNames(~sdl=currentSdl)
|
|
314
|
+
let newSet = allRootFieldNames(~sdl=newSdl)->Set.fromArray
|
|
315
|
+
let dropped = currentNames->Array.filter(name => !(newSet->Set.has(name)))
|
|
316
|
+
if dropped->Array.length == 0 {
|
|
317
|
+
// Additive or unchanged push — the new schema keeps every live field.
|
|
318
|
+
false
|
|
319
|
+
} else {
|
|
320
|
+
let current = currentNames->Array.length
|
|
321
|
+
let next = allRootFieldNames(~sdl=newSdl)->Array.length
|
|
322
|
+
current > 0 && next->Int.toFloat < current->Int.toFloat *. threshold
|
|
323
|
+
}
|
|
271
324
|
}
|
|
@@ -181,32 +181,68 @@ function isIdentStart(line) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
function
|
|
184
|
+
function rootTypeFieldNames(sdl, typeName) {
|
|
185
185
|
let marker = `type ` + typeName;
|
|
186
186
|
let typeStart = Stdlib_String.indexOfOpt(sdl, marker);
|
|
187
187
|
if (typeStart === undefined) {
|
|
188
|
-
return
|
|
188
|
+
return [];
|
|
189
189
|
}
|
|
190
190
|
let rest = sdl.slice(typeStart);
|
|
191
191
|
let match = Stdlib_String.indexOfOpt(rest, "{");
|
|
192
192
|
let match$1 = Stdlib_String.indexOfOpt(rest, "}");
|
|
193
|
-
if (match !== undefined
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
if (match !== undefined) {
|
|
194
|
+
if (match$1 !== undefined) {
|
|
195
|
+
if (match$1 > match) {
|
|
196
|
+
return Stdlib_Array.filterMap(rest.slice(match + 1 | 0, match$1).split("\n"), line => {
|
|
197
|
+
let trimmed = line.trim();
|
|
198
|
+
if (!(trimmed.length > 0 && isIdentStart(trimmed))) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let name = extractLeadingName(trimmed);
|
|
202
|
+
let name$1 = name.endsWith(":") ? name.slice(0, name.length - 1 | 0) : name;
|
|
203
|
+
if (name$1.length > 0) {
|
|
204
|
+
return name$1;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
198
207
|
} else {
|
|
199
|
-
return
|
|
208
|
+
return [];
|
|
200
209
|
}
|
|
201
|
-
}
|
|
210
|
+
} else {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
202
213
|
} else {
|
|
203
|
-
return
|
|
214
|
+
return [];
|
|
204
215
|
}
|
|
205
216
|
}
|
|
206
217
|
|
|
218
|
+
function countRootTypeFields(sdl, typeName) {
|
|
219
|
+
return rootTypeFieldNames(sdl, typeName).length;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let rootTypeNames = [
|
|
223
|
+
"Mutation",
|
|
224
|
+
"Query",
|
|
225
|
+
"Subscription"
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
function allRootFieldNames(sdl) {
|
|
229
|
+
return rootTypeNames.flatMap(typeName => rootTypeFieldNames(sdl, typeName));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function missingRootFields(expectedSdl, liveSdl) {
|
|
233
|
+
let liveSet = new Set(allRootFieldNames(liveSdl));
|
|
234
|
+
return allRootFieldNames(expectedSdl).filter(name => !liveSet.has(name));
|
|
235
|
+
}
|
|
236
|
+
|
|
207
237
|
function isCatastrophicSchemaShrink(currentSdl, newSdl, threshold) {
|
|
208
|
-
let
|
|
209
|
-
let
|
|
238
|
+
let currentNames = allRootFieldNames(currentSdl);
|
|
239
|
+
let newSet = new Set(allRootFieldNames(newSdl));
|
|
240
|
+
let dropped = currentNames.filter(name => !newSet.has(name));
|
|
241
|
+
if (dropped.length === 0) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
let current = currentNames.length;
|
|
245
|
+
let next = allRootFieldNames(newSdl).length;
|
|
210
246
|
if (current > 0) {
|
|
211
247
|
return next < current * threshold;
|
|
212
248
|
} else {
|
|
@@ -223,7 +259,11 @@ export {
|
|
|
223
259
|
relayBaseQueries,
|
|
224
260
|
stitch,
|
|
225
261
|
isIdentStart,
|
|
262
|
+
rootTypeFieldNames,
|
|
226
263
|
countRootTypeFields,
|
|
264
|
+
rootTypeNames,
|
|
265
|
+
allRootFieldNames,
|
|
266
|
+
missingRootFields,
|
|
227
267
|
isCatastrophicSchemaShrink,
|
|
228
268
|
}
|
|
229
269
|
/* log Not a pure module */
|
|
@@ -289,11 +289,21 @@ module Make = (
|
|
|
289
289
|
|
|
290
290
|
// All-or-nothing: thread the derived scope only when every slice resolved.
|
|
291
291
|
// An ambiguous boundary keeps the annotated values (no partial inference).
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
// Both this builder and the deployed command-handler entry point
|
|
293
|
+
// (DcbCommandTopicEntryPoint.mjs) route through `DcbTag.deriveEffectiveScope`
|
|
294
|
+
// so the runtime decision query cannot drift from this scope — see
|
|
295
|
+
// docs/analysis/dcb-runtime-scope-annotation-drift.md.
|
|
296
|
+
let {
|
|
297
|
+
crossPartitionTagKeys: effectiveCrossPartitionTagKeys,
|
|
298
|
+
tagKeysByEventType: effectiveTagKeysByEventType,
|
|
299
|
+
} = Reventless.DcbTag.deriveEffectiveScope(
|
|
300
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => {
|
|
301
|
+
Reventless.DcbTag.name: Sc.Spec.name,
|
|
302
|
+
commandSchema: Sc.Spec.commandSchema->S.castToUnknown,
|
|
303
|
+
consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
304
|
+
eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
|
|
305
|
+
}),
|
|
306
|
+
)
|
|
297
307
|
|
|
298
308
|
module DcbEventLog = DcbEventLog_Builder.Make(
|
|
299
309
|
DcbEventLogStorage,
|
|
@@ -167,9 +167,14 @@ function Make(DcbEventLogStorage) {
|
|
|
167
167
|
let scopeIssues = DcbValidation$Reventless.validateScopeVsInference(scopeAnnotations, inferred);
|
|
168
168
|
scopeIssues.contradictions.forEach(e => log.warn("Dcb_Builder", undefined, `DCB scope contradiction (` + e.sliceName + `): ` + e.message));
|
|
169
169
|
scopeIssues.redundancies.forEach(e => log.info("Dcb_Builder", undefined, `DCB scope (` + e.sliceName + `): ` + e.message));
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
let match = DcbTag$Reventless.deriveEffectiveScope(stateChangeSlices.map(Sc => ({
|
|
171
|
+
name: Sc.Spec.name,
|
|
172
|
+
commandSchema: Sc.Spec.commandSchema,
|
|
173
|
+
consumedEventSchema: Sc.Spec.consumedEventSchema,
|
|
174
|
+
eventSchema: Sc.Spec.eventSchema
|
|
175
|
+
})));
|
|
176
|
+
let effectiveTagKeysByEventType = match.tagKeysByEventType;
|
|
177
|
+
let effectiveCrossPartitionTagKeys = match.crossPartitionTagKeys;
|
|
173
178
|
let DcbEventLog = DcbEventLog_Builder$ReventlessCore.Make(DcbEventLogStorage)(DcbEventTopicPublisher);
|
|
174
179
|
let dcbEventLog = DcbEventLog.make(name, indexes$1, partitionTag, effectiveCrossPartitionTagKeys, opts);
|
|
175
180
|
Stdlib_Option.forEach(HooksConfig.hooks.onDcbEventLogCreated, hook => hook(dcbEventLog));
|
|
@@ -104,4 +104,97 @@ type Mutation {
|
|
|
104
104
|
isCatastrophicSchemaShrink(~currentSdl=fullSchemaSdl, ~newSdl=fullSchemaSdl, ~threshold=0.5),
|
|
105
105
|
)->toBe(false)
|
|
106
106
|
})
|
|
107
|
+
testSync("allows a purely additive push even below threshold cardinality", () => {
|
|
108
|
+
// A tiny live schema growing into a big one drops nothing → never a clobber,
|
|
109
|
+
// regardless of the ratio.
|
|
110
|
+
expect(
|
|
111
|
+
isCatastrophicSchemaShrink(~currentSdl=collapsedSchemaSdl, ~newSdl=fullSchemaSdl, ~threshold=0.5),
|
|
112
|
+
)->toBe(false)
|
|
113
|
+
})
|
|
114
|
+
testSync("includes Subscription fields when judging a shrink", () => {
|
|
115
|
+
let withSubs = `type Query {
|
|
116
|
+
node(id: ID!): Node
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type Mutation {
|
|
120
|
+
Ordering_placeOrder(input: OrderInput): Order
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type Subscription {
|
|
124
|
+
onOrdering_Customer_Register: Customer
|
|
125
|
+
onOrdering_Customer_UpdateAddress: Customer
|
|
126
|
+
onCatalog_Product_Added: Product
|
|
127
|
+
}`
|
|
128
|
+
// Dropping every Subscription field (and keeping Mutation/Query) is a shrink
|
|
129
|
+
// the Mutation+Query-only guard would have missed.
|
|
130
|
+
let subsGone = `type Query {
|
|
131
|
+
node(id: ID!): Node
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type Mutation {
|
|
135
|
+
Ordering_placeOrder(input: OrderInput): Order
|
|
136
|
+
}`
|
|
137
|
+
expect(
|
|
138
|
+
isCatastrophicSchemaShrink(~currentSdl=withSubs, ~newSdl=subsGone, ~threshold=0.5),
|
|
139
|
+
)->toBe(true)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe("GraphQL_Stitcher.rootTypeFieldNames", () => {
|
|
144
|
+
testSync("lists Mutation field names", () => {
|
|
145
|
+
expect(rootTypeFieldNames(~sdl=fullSchemaSdl, ~typeName="Mutation"))->toEqual([
|
|
146
|
+
"Platform_connectPlugin",
|
|
147
|
+
"Catalog_addProduct",
|
|
148
|
+
"Catalog_updateProduct",
|
|
149
|
+
"Ordering_placeOrder",
|
|
150
|
+
])
|
|
151
|
+
})
|
|
152
|
+
testSync("strips args and @aws_auth directive lines", () => {
|
|
153
|
+
expect(rootTypeFieldNames(~sdl=authAnnotatedSdl, ~typeName="Mutation"))->toEqual([
|
|
154
|
+
"Platform_connectPlugin",
|
|
155
|
+
"Catalog_addProduct",
|
|
156
|
+
])
|
|
157
|
+
})
|
|
158
|
+
testSync("missing type returns []", () => {
|
|
159
|
+
expect(rootTypeFieldNames(~sdl=fullSchemaSdl, ~typeName="Subscription"))->toEqual([])
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe("GraphQL_Stitcher.missingRootFields", () => {
|
|
164
|
+
testSync("reports plugin fields absent from an admin-base clobber", () => {
|
|
165
|
+
// The alpha 2026-07-08 clobber: live schema is admin-base only. Every
|
|
166
|
+
// Catalog_*/Ordering_* field the deploy expects is missing → repair fires.
|
|
167
|
+
let missing = missingRootFields(~expectedSdl=fullSchemaSdl, ~liveSdl=collapsedSchemaSdl)
|
|
168
|
+
expect(missing->Array.includes("Catalog_addProduct"))->toBe(true)
|
|
169
|
+
expect(missing->Array.includes("Ordering_placeOrder"))->toBe(true)
|
|
170
|
+
expect(missing->Array.includes("Ordering_orders"))->toBe(true)
|
|
171
|
+
})
|
|
172
|
+
testSync("empty when live schema is a superset (intact)", () => {
|
|
173
|
+
expect(missingRootFields(~expectedSdl=fullSchemaSdl, ~liveSdl=fullSchemaSdl))->toEqual([])
|
|
174
|
+
})
|
|
175
|
+
testSync("detects an equal-cardinality field swap the count test misses", () => {
|
|
176
|
+
// Same number of root fields, but one Mutation field replaced by a foreign
|
|
177
|
+
// one — a swap. countRootTypeFields would report no drift; the name-set diff
|
|
178
|
+
// catches it.
|
|
179
|
+
let swapped = `type Query {
|
|
180
|
+
node(id: ID!): Node
|
|
181
|
+
Platform_plugins: [Plugin]
|
|
182
|
+
Catalog_products: [Product]
|
|
183
|
+
Ordering_orders: [Order]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type Mutation {
|
|
187
|
+
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
188
|
+
Catalog_addProduct(input: AddInput): Product
|
|
189
|
+
Catalog_updateProduct(input: UpdateInput): Product
|
|
190
|
+
Foreign_strayField(input: OrderInput): Order
|
|
191
|
+
}`
|
|
192
|
+
// Same count on both sides.
|
|
193
|
+
expect(countRootTypeFields(~sdl=swapped, ~typeName="Mutation"))->toBe(
|
|
194
|
+
countRootTypeFields(~sdl=fullSchemaSdl, ~typeName="Mutation"),
|
|
195
|
+
)
|
|
196
|
+
// But the expected Ordering_placeOrder is missing from the live (swapped) schema.
|
|
197
|
+
let missing = missingRootFields(~expectedSdl=fullSchemaSdl, ~liveSdl=swapped)
|
|
198
|
+
expect(missing->Array.includes("Ordering_placeOrder"))->toBe(true)
|
|
199
|
+
})
|
|
107
200
|
})
|
|
@@ -89,6 +89,80 @@ type Mutation {
|
|
|
89
89
|
globalThis.test("allows an unchanged re-push (same SDL)", () => {
|
|
90
90
|
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(fullSchemaSdl, fullSchemaSdl, 0.5)).toBe(false);
|
|
91
91
|
});
|
|
92
|
+
globalThis.test("allows a purely additive push even below threshold cardinality", () => {
|
|
93
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(collapsedSchemaSdl, fullSchemaSdl, 0.5)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
globalThis.test("includes Subscription fields when judging a shrink", () => {
|
|
96
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(`type Query {
|
|
97
|
+
node(id: ID!): Node
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type Mutation {
|
|
101
|
+
Ordering_placeOrder(input: OrderInput): Order
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Subscription {
|
|
105
|
+
onOrdering_Customer_Register: Customer
|
|
106
|
+
onOrdering_Customer_UpdateAddress: Customer
|
|
107
|
+
onCatalog_Product_Added: Product
|
|
108
|
+
}`, `type Query {
|
|
109
|
+
node(id: ID!): Node
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type Mutation {
|
|
113
|
+
Ordering_placeOrder(input: OrderInput): Order
|
|
114
|
+
}`, 0.5)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
globalThis.describe("GraphQL_Stitcher.rootTypeFieldNames", () => {
|
|
119
|
+
globalThis.test("lists Mutation field names", () => {
|
|
120
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.rootTypeFieldNames(fullSchemaSdl, "Mutation")).toEqual([
|
|
121
|
+
"Platform_connectPlugin",
|
|
122
|
+
"Catalog_addProduct",
|
|
123
|
+
"Catalog_updateProduct",
|
|
124
|
+
"Ordering_placeOrder"
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
globalThis.test("strips args and @aws_auth directive lines", () => {
|
|
128
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.rootTypeFieldNames(authAnnotatedSdl, "Mutation")).toEqual([
|
|
129
|
+
"Platform_connectPlugin",
|
|
130
|
+
"Catalog_addProduct"
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
globalThis.test("missing type returns []", () => {
|
|
134
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.rootTypeFieldNames(fullSchemaSdl, "Subscription")).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
globalThis.describe("GraphQL_Stitcher.missingRootFields", () => {
|
|
139
|
+
globalThis.test("reports plugin fields absent from an admin-base clobber", () => {
|
|
140
|
+
let missing = GraphQL_Stitcher$ReventlessCore.missingRootFields(fullSchemaSdl, collapsedSchemaSdl);
|
|
141
|
+
globalThis.expect(missing.includes("Catalog_addProduct")).toBe(true);
|
|
142
|
+
globalThis.expect(missing.includes("Ordering_placeOrder")).toBe(true);
|
|
143
|
+
globalThis.expect(missing.includes("Ordering_orders")).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
globalThis.test("empty when live schema is a superset (intact)", () => {
|
|
146
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.missingRootFields(fullSchemaSdl, fullSchemaSdl)).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
globalThis.test("detects an equal-cardinality field swap the count test misses", () => {
|
|
149
|
+
let swapped = `type Query {
|
|
150
|
+
node(id: ID!): Node
|
|
151
|
+
Platform_plugins: [Plugin]
|
|
152
|
+
Catalog_products: [Product]
|
|
153
|
+
Ordering_orders: [Order]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type Mutation {
|
|
157
|
+
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
158
|
+
Catalog_addProduct(input: AddInput): Product
|
|
159
|
+
Catalog_updateProduct(input: UpdateInput): Product
|
|
160
|
+
Foreign_strayField(input: OrderInput): Order
|
|
161
|
+
}`;
|
|
162
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(swapped, "Mutation")).toBe(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(fullSchemaSdl, "Mutation"));
|
|
163
|
+
let missing = GraphQL_Stitcher$ReventlessCore.missingRootFields(fullSchemaSdl, swapped);
|
|
164
|
+
globalThis.expect(missing.includes("Ordering_placeOrder")).toBe(true);
|
|
165
|
+
});
|
|
92
166
|
});
|
|
93
167
|
|
|
94
168
|
export {
|