@reventlessdev/reventless-core 3.0.0-alpha.144 → 3.0.0-alpha.146
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 +6 -6
- package/src/admin/Platform_Admin.res +1 -0
- package/src/admin/Platform_Admin.res.mjs +4 -2
- package/src/components/Api/GraphQL_Stitcher.res +81 -28
- package/src/components/Api/GraphQL_Stitcher.res.mjs +52 -12
- package/src/plugin/component/Plugin_Builder.res +6 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +2 -1
- package/src/plugin/component/Plugin_BuiltHook.res +5 -2
- package/src/plugin/lifecycle/PluginsProjection.res +1 -0
- package/src/plugin/lifecycle/PluginsProjection.res.mjs +2 -0
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +7 -0
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +20 -1
- package/tests/api/GraphQL_StitcherTest.res +93 -0
- package/tests/api/GraphQL_StitcherTest.res.mjs +74 -0
- package/tests/logger/LogFormatTest.res +2 -1
- package/tests/logger/LogFormatTest.res.mjs +3 -2
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
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.146 (2026-07-08)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **reventless-core:** classify plugins by kind in the lifecycle read model ([64e3f22](https://github.com/ReventlessDev/reventless-core/commit/64e3f22b8114a771886b7c8ec023e95971413c0b))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.145 (2026-07-08)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **reventless-aws:** stop admin-base schema clobber of split-mode DomainApi ([afce85f](https://github.com/ReventlessDev/reventless-core/commit/afce85fa57474735cea3dcb1c2a151c2d8804f0e))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.144 (2026-07-07)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
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.146",
|
|
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.25",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
|
|
33
31
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
|
|
32
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
|
|
33
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.25",
|
|
34
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.6",
|
|
35
35
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
37
37
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.91",
|
|
39
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.24",
|
|
40
|
-
"@reventlessdev/
|
|
41
|
-
"@reventlessdev/
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.69",
|
|
41
|
+
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.11"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "^12.3.0",
|
|
@@ -46,7 +46,8 @@ function Make(RuntimeEnvironment) {
|
|
|
46
46
|
apiTarget: undefined,
|
|
47
47
|
uiFragments: undefined,
|
|
48
48
|
structure: undefined,
|
|
49
|
-
dcbEventLog: undefined
|
|
49
|
+
dcbEventLog: undefined,
|
|
50
|
+
kind: "Domain"
|
|
50
51
|
};
|
|
51
52
|
let Callback = Admin_Callback$ReventlessCore.Make({
|
|
52
53
|
pluginDefinition: fakePluginDefinition,
|
|
@@ -187,7 +188,8 @@ function Make(RuntimeEnvironment) {
|
|
|
187
188
|
apiTarget: undefined,
|
|
188
189
|
uiFragments: undefined,
|
|
189
190
|
structure: undefined,
|
|
190
|
-
dcbEventLog: undefined
|
|
191
|
+
dcbEventLog: undefined,
|
|
192
|
+
kind: "Domain"
|
|
191
193
|
};
|
|
192
194
|
let Callback = Admin_Callback$ReventlessCore.Make({
|
|
193
195
|
pluginDefinition: fakePluginDefinition,
|
|
@@ -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 */
|
|
@@ -687,6 +687,12 @@ module Make = (
|
|
|
687
687
|
uiFragments,
|
|
688
688
|
structure: pluginStructure,
|
|
689
689
|
dcbEventLog: dcbEventLogDef,
|
|
690
|
+
// Business role from the deploy-time metadata registry (set via
|
|
691
|
+
// registerPluginMetadata). Unset → Domain. Rides the handshake to the
|
|
692
|
+
// plugin-lifecycle projection so the admin can segregate infrastructure.
|
|
693
|
+
kind: Plugin_Helpers.pluginMetadataRegistry.contents
|
|
694
|
+
->Option.flatMap(m => m.kind)
|
|
695
|
+
->Option.getOr(Plugin_Helpers.Domain),
|
|
690
696
|
})
|
|
691
697
|
|
|
692
698
|
// Bundled Plugin EventCollector reconstructs Extension_Operations at
|
|
@@ -462,7 +462,8 @@ function Make(Spec) {
|
|
|
462
462
|
apiTarget: capturedDeployTarget,
|
|
463
463
|
uiFragments: uiFragments,
|
|
464
464
|
structure: pluginStructure,
|
|
465
|
-
dcbEventLog: param[2]
|
|
465
|
+
dcbEventLog: param[2],
|
|
466
|
+
kind: Stdlib_Option.getOr(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.pluginMetadataRegistry.contents, m => m.kind), "Domain")
|
|
466
467
|
}));
|
|
467
468
|
let aggregateQueueUrls = {};
|
|
468
469
|
Object.entries(aggregateResources).forEach(param => {
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
// describe a plugin's role and origin.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
/** Classifies the business role of a plugin. Absent defaults to Domain.
|
|
16
|
-
|
|
15
|
+
/** Classifies the business role of a plugin. Absent defaults to Domain.
|
|
16
|
+
Aliases the canonical `@schema` enum in `reventless-spec` (`Reventless.Plugin.pluginKind`)
|
|
17
|
+
so the deploy-time metadata registry and the serialized `pluginDefinition` share one
|
|
18
|
+
type — no mapping needed where metadata flows onto the definition. */
|
|
19
|
+
type pluginKind = Reventless.Plugin.pluginKind =
|
|
17
20
|
| Domain
|
|
18
21
|
| PlatformInfrastructure
|
|
19
22
|
| Commercial
|
|
@@ -58,6 +58,7 @@ let displayState = (
|
|
|
58
58
|
uiFragments: def.uiFragments,
|
|
59
59
|
structure: def.structure,
|
|
60
60
|
dcbEventLog: def.dcbEventLog,
|
|
61
|
+
kind: def.kind,
|
|
61
62
|
// The current version is never listed among the "other" connected versions.
|
|
62
63
|
otherConnectedVersions: otherConnectedVersions->Util.without(def.version),
|
|
63
64
|
}
|
|
@@ -74,6 +74,7 @@ function displayState(def, status, statusChange, otherConnectedVersions) {
|
|
|
74
74
|
let base_uiFragments = def.uiFragments;
|
|
75
75
|
let base_structure = def.structure;
|
|
76
76
|
let base_dcbEventLog = def.dcbEventLog;
|
|
77
|
+
let base_kind = def.kind;
|
|
77
78
|
let base_otherConnectedVersions = without(otherConnectedVersions, def.version);
|
|
78
79
|
let base = {
|
|
79
80
|
name: base_name,
|
|
@@ -89,6 +90,7 @@ function displayState(def, status, statusChange, otherConnectedVersions) {
|
|
|
89
90
|
uiFragments: base_uiFragments,
|
|
90
91
|
structure: base_structure,
|
|
91
92
|
dcbEventLog: base_dcbEventLog,
|
|
93
|
+
kind: base_kind,
|
|
92
94
|
otherConnectedVersions: base_otherConnectedVersions
|
|
93
95
|
};
|
|
94
96
|
let target = def.apiTarget;
|
|
@@ -37,6 +37,12 @@ type state = {
|
|
|
37
37
|
// from this plugin's DCB topic → peer EventCollectors (and vice-versa). None
|
|
38
38
|
// for pure-aggregate plugins or for plugins persisted before Phase 4.
|
|
39
39
|
dcbEventLog: @s.matches(Reventless.Plugin.dcbEventLogOptionSchema) option<Reventless.Plugin.dcbEventLogDefinition>,
|
|
40
|
+
// Business role of the plugin (from pluginDefinition.kind). Lets the admin Plugins
|
|
41
|
+
// view segregate PlatformInfrastructure / Commercial / Marketplace from Domain.
|
|
42
|
+
// @scan opts the field into server-side equality filtering so the connection gains
|
|
43
|
+
// a `kindEq` filter — the panel split (main list vs System/Infrastructure) pages
|
|
44
|
+
// correctly server-side rather than filtering a page client-side.
|
|
45
|
+
@scan kind: Reventless.Plugin.pluginKind,
|
|
40
46
|
// Other versions of this name that are currently **Connected** — excludes this
|
|
41
47
|
// row's own `version` (whose status is the `status` field above). Lets the
|
|
42
48
|
// projection recompute which version is current (highest Connected) during a
|
|
@@ -61,6 +67,7 @@ type queryResult = {
|
|
|
61
67
|
uiFragments: option<Reventless.Plugin.uiFragmentManifest>,
|
|
62
68
|
structure: option<Reventless.Plugin.pluginStructure>,
|
|
63
69
|
dcbEventLog: option<Reventless.Plugin.dcbEventLogDefinition>,
|
|
70
|
+
kind: Reventless.Plugin.pluginKind,
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
|
|
@@ -4,6 +4,7 @@ import * as S from "sury/src/S.res.mjs";
|
|
|
4
4
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
5
5
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
6
6
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
7
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
7
8
|
|
|
8
9
|
let statusSchema = S.union([
|
|
9
10
|
S.literal("Connected"),
|
|
@@ -27,11 +28,29 @@ let stateSchema = S.schema(s => ({
|
|
|
27
28
|
uiFragments: s.m(Plugin$Reventless.uiFragmentManifestOptionSchema),
|
|
28
29
|
structure: s.m(Plugin$Reventless.pluginStructureOptionSchema),
|
|
29
30
|
dcbEventLog: s.m(Plugin$Reventless.dcbEventLogOptionSchema),
|
|
31
|
+
kind: s.m(Plugin$Reventless.pluginKindSchema),
|
|
30
32
|
otherConnectedVersions: s.m(S.array(Plugin$Reventless.versionSchema))
|
|
31
33
|
}));
|
|
32
34
|
|
|
33
35
|
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
34
36
|
|
|
37
|
+
let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stateAnnotationsId, {
|
|
38
|
+
ids: [],
|
|
39
|
+
compositeIds: [],
|
|
40
|
+
subIds: [],
|
|
41
|
+
compositeSubIds: [],
|
|
42
|
+
indexes: [],
|
|
43
|
+
hidden: [],
|
|
44
|
+
summary: [],
|
|
45
|
+
drillTargets: [],
|
|
46
|
+
drillTargetKeys: [],
|
|
47
|
+
collapsed: [],
|
|
48
|
+
scan: ["kind"],
|
|
49
|
+
scanSort: [],
|
|
50
|
+
status: undefined,
|
|
51
|
+
visibility: undefined
|
|
52
|
+
});
|
|
53
|
+
|
|
35
54
|
let name = "Plugins";
|
|
36
55
|
|
|
37
56
|
let Id;
|
|
@@ -48,9 +67,9 @@ export {
|
|
|
48
67
|
name,
|
|
49
68
|
Id,
|
|
50
69
|
statusSchema,
|
|
51
|
-
stateSchema,
|
|
52
70
|
config,
|
|
53
71
|
subIdConfig,
|
|
72
|
+
stateSchema$1 as stateSchema,
|
|
54
73
|
moduleUrl,
|
|
55
74
|
authorization,
|
|
56
75
|
visibility,
|
|
@@ -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 {
|
|
@@ -76,6 +76,7 @@ describe("LogFormat", () => {
|
|
|
76
76
|
uiFragments: None,
|
|
77
77
|
structure: None,
|
|
78
78
|
dcbEventLog: None,
|
|
79
|
+
kind: Domain,
|
|
79
80
|
}),
|
|
80
81
|
]
|
|
81
82
|
let meta: Message.meta = {
|
|
@@ -97,7 +98,7 @@ describe("LogFormat", () => {
|
|
|
97
98
|
},
|
|
98
99
|
)
|
|
99
100
|
let expected1 = `1/2: \x1b[1mHeartbeat\x1b[0m(0): {"command":{"TAG":"Heartbeat","_0":"0.0.0"},"meta":${metaStr},"id":"0"}`
|
|
100
|
-
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"uiFragments":null,"structure":null,"dcbEventLog":null}},"meta":${metaStr},"id":"1"}`
|
|
101
|
+
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"uiFragments":null,"structure":null,"dcbEventLog":null,"kind":"Domain"}},"meta":${metaStr},"id":"1"}`
|
|
101
102
|
expect(commandJsonsToLogMessages(arr))->toEqual([expected1, expected2])
|
|
102
103
|
},
|
|
103
104
|
)
|
|
@@ -76,7 +76,8 @@ globalThis.describe("LogFormat", () => {
|
|
|
76
76
|
apiTarget: undefined,
|
|
77
77
|
uiFragments: undefined,
|
|
78
78
|
structure: undefined,
|
|
79
|
-
dcbEventLog: undefined
|
|
79
|
+
dcbEventLog: undefined,
|
|
80
|
+
kind: "Domain"
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
];
|
|
@@ -95,7 +96,7 @@ globalThis.describe("LogFormat", () => {
|
|
|
95
96
|
commandJson: Message$ReventlessCore.encode(command, PluginSpec$ReventlessCore.commandSchema)
|
|
96
97
|
}));
|
|
97
98
|
let expected1 = `1/2: \x1b[1mHeartbeat\x1b[0m(0): {"command":{"TAG":"Heartbeat","_0":"0.0.0"},"meta":` + metaStr + `,"id":"0"}`;
|
|
98
|
-
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"uiFragments":null,"structure":null,"dcbEventLog":null}},"meta":` + metaStr + `,"id":"1"}`;
|
|
99
|
+
let expected2 = `2/2: \x1b[1mConnect\x1b[0m(1): {"command":{"TAG":"Connect","_0":{"id":"id","name":"testName","version":"testVersion","extensionPoints":[{"name":"testExtensionPoint","commandTopic":"testCommandTopic","eventTopic":"testEventTopic"}],"extensions":[{"name":"testExtension","extensionPointName":"testExtensionPoint","dcbSources":[]}],"eventCollector":"testEventCollector","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"uiFragments":null,"structure":null,"dcbEventLog":null,"kind":"Domain"}},"meta":` + metaStr + `,"id":"1"}`;
|
|
99
100
|
globalThis.expect(LogFormat$ReventlessCore.commandJsonsToLogMessages(arr)).toEqual([
|
|
100
101
|
expected1,
|
|
101
102
|
expected2
|
|
@@ -43,7 +43,8 @@ globalThis.describe("Message should", () => {
|
|
|
43
43
|
apiTarget: undefined,
|
|
44
44
|
uiFragments: undefined,
|
|
45
45
|
structure: undefined,
|
|
46
|
-
dcbEventLog: undefined
|
|
46
|
+
dcbEventLog: undefined,
|
|
47
|
+
kind: "Domain"
|
|
47
48
|
}
|
|
48
49
|
};
|
|
49
50
|
let variantJson = Message$ReventlessCore.encode(variant, PluginSpec$ReventlessCore.commandSchema);
|