@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +7 -7
  3. package/src/admin/Platform_Admin_Structure.res +5 -1
  4. package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
  5. package/src/admin/Platform_BakedManifest.res +52 -0
  6. package/src/admin/Platform_BakedManifest.res.mjs +38 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +4 -2
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +11 -3
  9. package/src/components/Api/GraphQL_FragmentGenerator.res +14 -1
  10. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +1 -1
  11. package/src/components/Api/QueryDbListQuery.res +25 -1
  12. package/src/components/Api/QueryDbListQuery.res.mjs +11 -4
  13. package/src/components/Api/SuryToJsonSchema.res +39 -0
  14. package/src/components/Api/SuryToJsonSchema.res.mjs +34 -3
  15. package/src/components/Dcb/Dcb_Builder.res +52 -3
  16. package/src/components/Dcb/Dcb_Builder.res.mjs +57 -1
  17. package/src/plugin/component/Plugin_Structure.res +112 -15
  18. package/src/plugin/component/Plugin_Structure.res.mjs +77 -9
  19. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +3 -2
  20. package/tests/admin/Platform_Admin_StructureTest.res +49 -0
  21. package/tests/admin/Platform_Admin_StructureTest.res.mjs +24 -0
  22. package/tests/admin/Platform_BakedManifestTest.res +3 -1
  23. package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
  24. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +14 -8
  25. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -12
  26. package/tests/admin/Platform_PluginStructuresApiTest.res +3 -1
  27. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
  28. package/tests/api/Api_IdsTest.res.mjs +1 -1
  29. package/tests/api/SuryToJsonSchemaTest.res +141 -1
  30. package/tests/api/SuryToJsonSchemaTest.res.mjs +269 -54
  31. package/tests/dcb/DcbEventLogSchemaTest.res +77 -0
  32. package/tests/dcb/DcbEventLogSchemaTest.res.mjs +90 -0
  33. package/tests/plugin/PluginStructureTest.res +168 -12
  34. package/tests/plugin/PluginStructureTest.res.mjs +146 -9
  35. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +3 -2
  36. 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
- statusField: undefined,
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 statusField as null", () => {
202
- globalThis.expect(json.includes("\"statusField\":null")).toEqual(true);
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
- statusField: undefined,
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
- statusField: undefined,
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 + statusField populated", () => {
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 qblWithStatus_statusField = "status";
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
- statusField: qblWithStatus_statusField,
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 statusField as the field name string", () => {
470
- globalThis.expect(json.includes("\"statusField\":\"status\"")).toEqual(true);
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
- statusField: undefined,
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
- statusField: undefined,
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
- statusField: None,
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
- statusField: undefined,
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
- statusField: undefined,
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
- status: None,
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
  {