@reventlessdev/reventless-core 3.0.0-alpha.235 → 3.0.0-alpha.237
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 +21 -0
- package/package.json +7 -7
- package/src/admin/Platform_Admin_Structure.res +5 -1
- package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
- package/src/admin/Platform_BakedManifest.res +52 -0
- package/src/admin/Platform_BakedManifest.res.mjs +38 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +4 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +11 -3
- package/src/components/Api/GraphQL_FragmentGenerator.res +14 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +1 -1
- package/src/components/Api/QueryDbListQuery.res +25 -1
- package/src/components/Api/QueryDbListQuery.res.mjs +11 -4
- package/src/components/Api/SuryToJsonSchema.res +39 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +34 -3
- package/src/components/Dcb/Dcb_Builder.res +52 -3
- package/src/components/Dcb/Dcb_Builder.res.mjs +57 -1
- package/src/plugin/component/Plugin_Structure.res +112 -15
- package/src/plugin/component/Plugin_Structure.res.mjs +77 -9
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +3 -2
- package/tests/admin/Platform_Admin_StructureTest.res +49 -0
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +24 -0
- package/tests/admin/Platform_BakedManifestTest.res +3 -1
- package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +14 -8
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -12
- package/tests/admin/Platform_PluginStructuresApiTest.res +3 -1
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
- package/tests/api/Api_IdsTest.res.mjs +1 -1
- package/tests/api/SuryToJsonSchemaTest.res +141 -1
- package/tests/api/SuryToJsonSchemaTest.res.mjs +269 -54
- package/tests/dcb/DcbEventLogSchemaTest.res +77 -0
- package/tests/dcb/DcbEventLogSchemaTest.res.mjs +90 -0
- package/tests/plugin/PluginStructureTest.res +168 -12
- package/tests/plugin/PluginStructureTest.res.mjs +146 -9
- package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +3 -2
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +2 -0
|
@@ -51,8 +51,10 @@ let qbl = {
|
|
|
51
51
|
labelField: "displayName",
|
|
52
52
|
searchableFields: qbl_searchableFields,
|
|
53
53
|
labelFieldSource: qbl_labelFieldSource,
|
|
54
|
-
|
|
54
|
+
lifecycleField: undefined,
|
|
55
55
|
ownerField: undefined,
|
|
56
|
+
retiredField: undefined,
|
|
57
|
+
retiredValues: undefined,
|
|
56
58
|
visibility: undefined,
|
|
57
59
|
chapter: undefined,
|
|
58
60
|
singleQueryField: qbl_singleQueryField,
|
|
@@ -198,8 +200,8 @@ globalThis.describe("encodePluginStructureEntry", () => {
|
|
|
198
200
|
globalThis.test("encodes the command's apiExposed flag (drives the event-graph API badge)", () => {
|
|
199
201
|
globalThis.expect(json.includes("\"apiExposed\":true")).toEqual(true);
|
|
200
202
|
});
|
|
201
|
-
globalThis.test("encodes None
|
|
202
|
-
globalThis.expect(json.includes("\"
|
|
203
|
+
globalThis.test("encodes None lifecycleField as null", () => {
|
|
204
|
+
globalThis.expect(json.includes("\"lifecycleField\":null")).toEqual(true);
|
|
203
205
|
});
|
|
204
206
|
globalThis.test("encodes writableDef events", () => {
|
|
205
207
|
globalThis.expect(json.includes("\"events\":[{\"name\":\"ProductAdded\"")).toEqual(true);
|
|
@@ -282,8 +284,10 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
282
284
|
labelField: "name",
|
|
283
285
|
searchableFields: internalQbl_searchableFields,
|
|
284
286
|
labelFieldSource: internalQbl_labelFieldSource,
|
|
285
|
-
|
|
287
|
+
lifecycleField: undefined,
|
|
286
288
|
ownerField: undefined,
|
|
289
|
+
retiredField: undefined,
|
|
290
|
+
retiredValues: undefined,
|
|
287
291
|
visibility: internalQbl_visibility,
|
|
288
292
|
chapter: undefined,
|
|
289
293
|
singleQueryField: internalQbl_singleQueryField,
|
|
@@ -308,8 +312,10 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
308
312
|
labelField: "name",
|
|
309
313
|
searchableFields: internalSlice_searchableFields,
|
|
310
314
|
labelFieldSource: internalSlice_labelFieldSource,
|
|
311
|
-
|
|
315
|
+
lifecycleField: undefined,
|
|
312
316
|
ownerField: undefined,
|
|
317
|
+
retiredField: undefined,
|
|
318
|
+
retiredValues: undefined,
|
|
313
319
|
visibility: internalSlice_visibility,
|
|
314
320
|
chapter: undefined,
|
|
315
321
|
singleQueryField: internalSlice_singleQueryField,
|
|
@@ -378,7 +384,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
378
384
|
});
|
|
379
385
|
});
|
|
380
386
|
|
|
381
|
-
globalThis.describe("allowedStates +
|
|
387
|
+
globalThis.describe("allowedStates + lifecycleField populated", () => {
|
|
382
388
|
let cmdWithStates_aggregateIdField = "id";
|
|
383
389
|
let cmdWithStates_references = [];
|
|
384
390
|
let cmdWithStates_allowedStates = ["Inactive"];
|
|
@@ -401,7 +407,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
401
407
|
let qblWithStatus_linkedWriteSide = ["Plugin"];
|
|
402
408
|
let qblWithStatus_searchableFields = ["name"];
|
|
403
409
|
let qblWithStatus_labelFieldSource = "convention";
|
|
404
|
-
let
|
|
410
|
+
let qblWithStatus_lifecycleField = "status";
|
|
405
411
|
let qblWithStatus_singleQueryField = "Platform_Plugin";
|
|
406
412
|
let qblWithStatus = {
|
|
407
413
|
name: "Plugin",
|
|
@@ -412,8 +418,10 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
412
418
|
labelField: "name",
|
|
413
419
|
searchableFields: qblWithStatus_searchableFields,
|
|
414
420
|
labelFieldSource: qblWithStatus_labelFieldSource,
|
|
415
|
-
|
|
421
|
+
lifecycleField: qblWithStatus_lifecycleField,
|
|
416
422
|
ownerField: undefined,
|
|
423
|
+
retiredField: undefined,
|
|
424
|
+
retiredValues: undefined,
|
|
417
425
|
visibility: undefined,
|
|
418
426
|
chapter: undefined,
|
|
419
427
|
singleQueryField: qblWithStatus_singleQueryField,
|
|
@@ -466,8 +474,8 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
466
474
|
globalThis.test("round-trips a declared targetState as the status string", () => {
|
|
467
475
|
globalThis.expect(json.includes("\"targetState\":\"Active\"")).toEqual(true);
|
|
468
476
|
});
|
|
469
|
-
globalThis.test("encodes populated
|
|
470
|
-
globalThis.expect(json.includes("\"
|
|
477
|
+
globalThis.test("encodes populated lifecycleField as the field name string", () => {
|
|
478
|
+
globalThis.expect(json.includes("\"lifecycleField\":\"status\"")).toEqual(true);
|
|
471
479
|
});
|
|
472
480
|
globalThis.test("encodes a component with no declared errors as an empty array, not null", () => {
|
|
473
481
|
globalThis.expect(json.includes("\"errors\":[]")).toEqual(true);
|
|
@@ -491,8 +499,10 @@ globalThis.describe("singleQueryField reaches the caller", () => {
|
|
|
491
499
|
labelField: "displayName",
|
|
492
500
|
searchableFields: qbl_searchableFields,
|
|
493
501
|
labelFieldSource: "annotation",
|
|
494
|
-
|
|
502
|
+
lifecycleField: undefined,
|
|
495
503
|
ownerField: undefined,
|
|
504
|
+
retiredField: undefined,
|
|
505
|
+
retiredValues: undefined,
|
|
496
506
|
visibility: undefined,
|
|
497
507
|
chapter: undefined,
|
|
498
508
|
singleQueryField: undefined,
|
|
@@ -536,8 +546,10 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
|
|
|
536
546
|
labelField: "displayName",
|
|
537
547
|
searchableFields: qbl_searchableFields,
|
|
538
548
|
labelFieldSource: "annotation",
|
|
539
|
-
|
|
549
|
+
lifecycleField: undefined,
|
|
540
550
|
ownerField: undefined,
|
|
551
|
+
retiredField: undefined,
|
|
552
|
+
retiredValues: undefined,
|
|
541
553
|
visibility: undefined,
|
|
542
554
|
chapter: undefined,
|
|
543
555
|
singleQueryField: "Catalog_Product",
|
|
@@ -16,7 +16,7 @@ let publicRm: queryableDef = {
|
|
|
16
16
|
labelField: "displayName",
|
|
17
17
|
searchableFields: ["displayName"],
|
|
18
18
|
labelFieldSource: Some("annotation"),
|
|
19
|
-
|
|
19
|
+
lifecycleField: None,
|
|
20
20
|
visibility: None,
|
|
21
21
|
chapter: Some("Inventory"),
|
|
22
22
|
singleQueryField: Some("Catalog_Product"),
|
|
@@ -24,6 +24,8 @@ let publicRm: queryableDef = {
|
|
|
24
24
|
idFieldSource: Some("convention"),
|
|
25
25
|
requiredAccess: None,
|
|
26
26
|
ownerField: None,
|
|
27
|
+
retiredField: None,
|
|
28
|
+
retiredValues: None,
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// The component `Platform_ComponentDefinitions` drops and this query must keep.
|
|
@@ -31,8 +31,10 @@ let publicRm = {
|
|
|
31
31
|
labelField: "displayName",
|
|
32
32
|
searchableFields: publicRm_searchableFields,
|
|
33
33
|
labelFieldSource: publicRm_labelFieldSource,
|
|
34
|
-
|
|
34
|
+
lifecycleField: undefined,
|
|
35
35
|
ownerField: undefined,
|
|
36
|
+
retiredField: undefined,
|
|
37
|
+
retiredValues: undefined,
|
|
36
38
|
visibility: undefined,
|
|
37
39
|
chapter: publicRm_chapter,
|
|
38
40
|
singleQueryField: publicRm_singleQueryField,
|
|
@@ -68,8 +70,10 @@ let internalRm = {
|
|
|
68
70
|
labelField: "displayName",
|
|
69
71
|
searchableFields: internalRm_searchableFields,
|
|
70
72
|
labelFieldSource: internalRm_labelFieldSource,
|
|
71
|
-
|
|
73
|
+
lifecycleField: undefined,
|
|
72
74
|
ownerField: undefined,
|
|
75
|
+
retiredField: undefined,
|
|
76
|
+
retiredValues: undefined,
|
|
73
77
|
visibility: internalRm_visibility,
|
|
74
78
|
chapter: internalRm_chapter,
|
|
75
79
|
singleQueryField: internalRm_singleQueryField,
|
|
@@ -77,7 +77,7 @@ globalThis.describe("filter.ids matches whichever form each side holds", () => {
|
|
|
77
77
|
"ids",
|
|
78
78
|
filterIds.map(prim => prim)
|
|
79
79
|
]]);
|
|
80
|
-
let conn = QueryDbListQuery$ReventlessCore.run([row(rowId)], args, GraphQL_FragmentGenerator$ReventlessCore.emptyCapability, "name", undefined, undefined);
|
|
80
|
+
let conn = QueryDbListQuery$ReventlessCore.run([row(rowId)], args, GraphQL_FragmentGenerator$ReventlessCore.emptyCapability, "name", undefined, undefined, undefined);
|
|
81
81
|
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(conn), d => d["edges"]), Stdlib_JSON.Decode.array), []).length;
|
|
82
82
|
};
|
|
83
83
|
globalThis.test("storage key row, storage key filter", () => {
|
|
@@ -31,10 +31,11 @@ let emptySpec: Reventless.StateAnnotations.stateAnnotationSpec = {
|
|
|
31
31
|
scanSort: [],
|
|
32
32
|
semantic: [],
|
|
33
33
|
metric: [],
|
|
34
|
-
|
|
34
|
+
lifecycle: None,
|
|
35
35
|
groupBy: None,
|
|
36
36
|
visibility: None,
|
|
37
37
|
live: None,
|
|
38
|
+
retired: None,
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
describe("SuryToJsonSchema:", () => {
|
|
@@ -420,6 +421,145 @@ describe("SuryToJsonSchema:", () => {
|
|
|
420
421
|
))->toEqual((Some("sum"), None))
|
|
421
422
|
})
|
|
422
423
|
|
|
424
|
+
testSync("emits x-reventless-retired {label,showWhenFalse} on the retired field", () => {
|
|
425
|
+
let schema = S.schema(s =>
|
|
426
|
+
{
|
|
427
|
+
"id": s.matches(S.string),
|
|
428
|
+
"archived": s.matches(S.bool),
|
|
429
|
+
}
|
|
430
|
+
)->S.castToUnknown
|
|
431
|
+
let schema' =
|
|
432
|
+
schema->withSpec({
|
|
433
|
+
...emptySpec,
|
|
434
|
+
retired: Some({field: "archived", label: "Archived", showWhenFalse: true, values: None}),
|
|
435
|
+
})
|
|
436
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema')
|
|
437
|
+
let retiredObj =
|
|
438
|
+
getPropertyOf(json, "archived")->Option.flatMap(s => getProperty(s, "x-reventless-retired"))
|
|
439
|
+
expect((
|
|
440
|
+
retiredObj->Option.flatMap(r => getProperty(r, "label"))->Option.flatMap(JSON.Decode.string),
|
|
441
|
+
retiredObj
|
|
442
|
+
->Option.flatMap(r => getProperty(r, "showWhenFalse"))
|
|
443
|
+
->Option.flatMap(JSON.Decode.bool),
|
|
444
|
+
// Only the named field carries it.
|
|
445
|
+
getPropertyOf(json, "id")->Option.flatMap(s => getProperty(s, "x-reventless-retired")),
|
|
446
|
+
))->toEqual((Some("Archived"), Some(true), None))
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// The state form. `values` is what tells a consumer which of the two forms a
|
|
450
|
+
// view declared, so it travels on the same key and only when present.
|
|
451
|
+
let stateFormJson = (~values: array<string>) => {
|
|
452
|
+
let schema = S.schema(s =>
|
|
453
|
+
{
|
|
454
|
+
"customerId": s.matches(S.string),
|
|
455
|
+
"accountStatus": s.matches(S.union([S.literal("Active"), S.literal("Deactivated")])),
|
|
456
|
+
}
|
|
457
|
+
)->S.castToUnknown
|
|
458
|
+
schema
|
|
459
|
+
->withSpec({
|
|
460
|
+
...emptySpec,
|
|
461
|
+
lifecycle: Some("accountStatus"),
|
|
462
|
+
retired: Some({
|
|
463
|
+
field: "accountStatus",
|
|
464
|
+
label: "",
|
|
465
|
+
showWhenFalse: false,
|
|
466
|
+
values: Some(values),
|
|
467
|
+
}),
|
|
468
|
+
})
|
|
469
|
+
->SuryToJsonSchema.deriveObjectSchema
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let retiredOn = (json, field) =>
|
|
473
|
+
getPropertyOf(json, field)->Option.flatMap(s => getProperty(s, "x-reventless-retired"))
|
|
474
|
+
|
|
475
|
+
let stringsAt = (retiredObj, key) =>
|
|
476
|
+
retiredObj
|
|
477
|
+
->Option.flatMap(r => getProperty(r, key))
|
|
478
|
+
->Option.flatMap(JSON.Decode.array)
|
|
479
|
+
->Option.map(a => a->Array.filterMap(JSON.Decode.string))
|
|
480
|
+
|
|
481
|
+
testSync("carries one retirement state as a one-element set", () => {
|
|
482
|
+
expect(
|
|
483
|
+
stateFormJson(~values=["Deactivated"])->retiredOn("accountStatus")->stringsAt("values"),
|
|
484
|
+
)->toEqual(Some(["Deactivated"]))
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
testSync("carries every retirement state a lifecycle declares", () => {
|
|
488
|
+
expect(
|
|
489
|
+
stateFormJson(~values=["Archived", "Discontinued"])
|
|
490
|
+
->retiredOn("accountStatus")
|
|
491
|
+
->stringsAt("values"),
|
|
492
|
+
)->toEqual(Some(["Archived", "Discontinued"]))
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// The transitional key, for one release. A consumer pinned before the set
|
|
496
|
+
// degrades to nothing at all rather than to a missing badge — and enforcement
|
|
497
|
+
// never depended on it, so the whole of the lag is cosmetic either way.
|
|
498
|
+
testSync("emits the singular value beside a one-element set", () => {
|
|
499
|
+
expect(
|
|
500
|
+
stateFormJson(~values=["Deactivated"])
|
|
501
|
+
->retiredOn("accountStatus")
|
|
502
|
+
->Option.flatMap(r => getProperty(r, "value"))
|
|
503
|
+
->Option.flatMap(JSON.Decode.string),
|
|
504
|
+
)->toEqual(Some("Deactivated"))
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
// There is no singular reading of two states, so the shim simply stops. A
|
|
508
|
+
// consumer that understands only `value` sees no retirement rather than one
|
|
509
|
+
// of the two picked arbitrarily.
|
|
510
|
+
testSync("omits the singular value for a set of two", () => {
|
|
511
|
+
expect(
|
|
512
|
+
stateFormJson(~values=["Archived", "Discontinued"])
|
|
513
|
+
->retiredOn("accountStatus")
|
|
514
|
+
->Option.flatMap(r => getProperty(r, "value")),
|
|
515
|
+
)->toEqual(None)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
// Absent rather than null: a consumer reads the key's absence as "the boolean
|
|
519
|
+
// form", the same way it reads an absent label as "derive one from the name".
|
|
520
|
+
testSync("omits both value and values entirely on the boolean form", () => {
|
|
521
|
+
let schema = S.schema(s => {"archived": s.matches(S.bool)})->S.castToUnknown
|
|
522
|
+
let schema' =
|
|
523
|
+
schema->withSpec({
|
|
524
|
+
...emptySpec,
|
|
525
|
+
retired: Some({field: "archived", label: "", showWhenFalse: false, values: None}),
|
|
526
|
+
})
|
|
527
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema')
|
|
528
|
+
let retiredObj = json->retiredOn("archived")
|
|
529
|
+
expect((
|
|
530
|
+
retiredObj->Option.flatMap(r => getProperty(r, "value")),
|
|
531
|
+
retiredObj->Option.flatMap(r => getProperty(r, "values")),
|
|
532
|
+
))->toEqual((None, None))
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
testSync("omits the retired label when empty but always states showWhenFalse", () => {
|
|
536
|
+
let schema = S.schema(s => {"deactivated": s.matches(S.bool)})->S.castToUnknown
|
|
537
|
+
let schema' =
|
|
538
|
+
schema->withSpec({
|
|
539
|
+
...emptySpec,
|
|
540
|
+
retired: Some({field: "deactivated", label: "", showWhenFalse: false, values: None}),
|
|
541
|
+
})
|
|
542
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema')
|
|
543
|
+
let retiredObj =
|
|
544
|
+
getPropertyOf(json, "deactivated")->Option.flatMap(s =>
|
|
545
|
+
getProperty(s, "x-reventless-retired")
|
|
546
|
+
)
|
|
547
|
+
expect((
|
|
548
|
+
retiredObj->Option.flatMap(r => getProperty(r, "label")),
|
|
549
|
+
retiredObj
|
|
550
|
+
->Option.flatMap(r => getProperty(r, "showWhenFalse"))
|
|
551
|
+
->Option.flatMap(JSON.Decode.bool),
|
|
552
|
+
))->toEqual((None, Some(false)))
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
testSync("emits nothing for a schema with no retired annotation", () => {
|
|
556
|
+
let schema = S.schema(s => {"archived": s.matches(S.bool)})->S.castToUnknown
|
|
557
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema->withSpec(emptySpec))
|
|
558
|
+
expect(
|
|
559
|
+
getPropertyOf(json, "archived")->Option.flatMap(s => getProperty(s, "x-reventless-retired")),
|
|
560
|
+
)->toBe(None)
|
|
561
|
+
})
|
|
562
|
+
|
|
423
563
|
testSync("emits x-reventless-group-by on the field named by groupBy", () => {
|
|
424
564
|
let schema = S.schema(s =>
|
|
425
565
|
{
|