@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
@@ -48,6 +48,60 @@ let toRelativePath = (function toRelativePath(moduleUrl) {
48
48
  }
49
49
  });
50
50
 
51
+ function variantTag(variant) {
52
+ switch (variant.type) {
53
+ case "string" :
54
+ let tag = variant.const;
55
+ if (tag !== undefined) {
56
+ return tag;
57
+ } else {
58
+ return;
59
+ }
60
+ case "object" :
61
+ let match = variant.properties["TAG"];
62
+ if (match === undefined) {
63
+ return;
64
+ }
65
+ if (match.type !== "string") {
66
+ return;
67
+ }
68
+ let tag$1 = match.const;
69
+ if (tag$1 !== undefined) {
70
+ return tag$1;
71
+ } else {
72
+ return;
73
+ }
74
+ default:
75
+ return;
76
+ }
77
+ }
78
+
79
+ function mergedEventSchema(schemas) {
80
+ if (schemas.length === 1) {
81
+ return schemas[0];
82
+ }
83
+ let seen = new Set();
84
+ return S.union(schemas.flatMap(schema => {
85
+ if (schema.type === "union") {
86
+ return schema.anyOf;
87
+ } else {
88
+ return [schema];
89
+ }
90
+ }).filter(arm => {
91
+ let tag = variantTag(arm);
92
+ if (tag !== undefined) {
93
+ if (seen.has(tag)) {
94
+ return false;
95
+ } else {
96
+ seen.add(tag);
97
+ return true;
98
+ }
99
+ } else {
100
+ return true;
101
+ }
102
+ }));
103
+ }
104
+
51
105
  let emptyResult_stateChangeSlicesOutputs = {};
52
106
 
53
107
  let emptyResult_stateViewSlicesOutputs = {};
@@ -583,7 +637,7 @@ function Make(DcbEventLogStorage) {
583
637
  eventLogEntries: allProducedSchemas.length !== 0 ? [{
584
638
  busKey: name + "DcbEventLog",
585
639
  displayName: name,
586
- eventSchema: allProducedSchemas[0]
640
+ eventSchema: mergedEventSchema(allProducedSchemas)
587
641
  }] : []
588
642
  };
589
643
  };
@@ -596,6 +650,8 @@ function Make(DcbEventLogStorage) {
596
650
  export {
597
651
  log,
598
652
  toRelativePath,
653
+ variantTag,
654
+ mergedEventSchema,
599
655
  emptyResult,
600
656
  Make,
601
657
  }
@@ -26,13 +26,13 @@ let rec isLabelShape = (t: SchemaType.schemaType): bool =>
26
26
  | _ => false
27
27
  }
28
28
 
29
- // Whether a field can hold a lifecycle status: a closed set of values, or an
30
- // optional one. A free-text `status: string` is not a lifecycle — filtering a
31
- // command menu against `allowedStates` needs states to compare with.
32
- let rec isStatusShape = (t: SchemaType.schemaType): bool =>
29
+ // Whether a field can hold a lifecycle: a closed set of values, or an optional
30
+ // one. A free-text `lifecycle: string` is not a lifecycle — filtering a command
31
+ // menu against `allowedStates` needs states to compare with.
32
+ let rec isLifecycleShape = (t: SchemaType.schemaType): bool =>
33
33
  switch t {
34
34
  | Enum(_, _) => true
35
- | Nullable(inner) => isStatusShape(inner)
35
+ | Nullable(inner) => isLifecycleShape(inner)
36
36
  | _ => false
37
37
  }
38
38
 
@@ -44,18 +44,25 @@ let conventionalLabelNames = ["name", "title", "label", "displayname"]
44
44
  let shapeOfItem = (~entityName: string, item: S.item): SchemaType.schemaType =>
45
45
  SchemaType.fromSury(~parentName=entityName, ~fieldName=item.location, item.schema)
46
46
 
47
- // Resolve the field that holds the entity's lifecycle status, used to filter a
48
- // per-row command menu against each command's `allowedStates`. Resolution order:
49
- // 1. Field annotated `@status` (PPX-emitted; see StateAnnotations).
50
- // 2. Field literally named `"status"` whose IR shape is an enum (convention;
47
+ // Resolve the field that holds the entity's lifecycle, used to filter a per-row
48
+ // command menu against each command's `allowedStates`. Resolution order:
49
+ // 1. Field annotated `@lifecycle` (PPX-emitted; see StateAnnotations).
50
+ // 2. Field literally named `"lifecycle"` whose IR shape is an enum (convention;
51
51
  // mirrors how labelField falls back to a conventionally-named field).
52
52
  // 3. None — filter is inert for this read model.
53
- let statusFieldFromStateSchema = (
53
+ //
54
+ // The convention rung is keyed on `lifecycle` rather than `status` deliberately:
55
+ // `status` is a promiscuous name — geocoding progress, todo-queue progress and
56
+ // translation audit outcome all wear it — so a convention keyed on it guesses,
57
+ // and guesses often. `lifecycle` is a word nobody types by accident, so matching
58
+ // it is closer to a declaration written in the field name. A record whose field
59
+ // is honestly called something else annotates instead.
60
+ let lifecycleFieldFromStateSchema = (
54
61
  ~entityName: string,
55
62
  stateSchema: S.t<unknown>,
56
63
  ): option<string> => {
57
64
  let annotated = switch Reventless.StateAnnotations.getSpec(stateSchema) {
58
- | Some(spec) => spec.status
65
+ | Some(spec) => spec.lifecycle
59
66
  | None => None
60
67
  }
61
68
  switch annotated {
@@ -65,7 +72,7 @@ let statusFieldFromStateSchema = (
65
72
  | Object({items}) =>
66
73
  items
67
74
  ->Array.find(item =>
68
- item.location == "status" && isStatusShape(shapeOfItem(~entityName, item))
75
+ item.location == "lifecycle" && isLifecycleShape(shapeOfItem(~entityName, item))
69
76
  )
70
77
  ->Option.map(item => item.location)
71
78
  | _ => None
@@ -73,6 +80,90 @@ let statusFieldFromStateSchema = (
73
80
  }
74
81
  }
75
82
 
83
+ // The field whose truth withdraws a row from ordinary reads. Annotation-only:
84
+ // there is no convention rung here, and its absence is the point. `lifecycleField`
85
+ // may fall back to a field literally named "lifecycle" because guessing wrong makes
86
+ // a command menu filter oddly; guessing wrong here makes rows vanish for every
87
+ // caller who is not elevated, so a boolean named `archived` that nobody annotated
88
+ // stays exactly as visible as it was.
89
+ let retiredFromStateSchema = (
90
+ stateSchema: S.t<unknown>,
91
+ ): option<Reventless.StateAnnotations.retiredSpec> =>
92
+ switch Reventless.StateAnnotations.getSpec(stateSchema) {
93
+ | Some(spec) => spec.retired
94
+ | None => None
95
+ }
96
+
97
+ let retiredFieldFromStateSchema = (stateSchema: S.t<unknown>): option<string> =>
98
+ retiredFromStateSchema(stateSchema)->Option.map(r => r.field)
99
+
100
+ // The states a row is retired *in*, for the enum form. `None` is the boolean
101
+ // form, where the value is always `true` and naming it would be a parameter that
102
+ // can only hold one thing. A set, because a lifecycle may be withdrawn by more
103
+ // than one state, and a one-member set is the ordinary case.
104
+ //
105
+ // Published beside `retiredField` rather than left for a consumer to dig out of
106
+ // the schema: a client that has the def in hand has the whole predicate, and two
107
+ // places deriving one comparison is how they come to disagree about it.
108
+ let retiredValuesFromStateSchema = (stateSchema: S.t<unknown>): option<array<string>> =>
109
+ retiredFromStateSchema(stateSchema)->Option.flatMap(r => r.values)
110
+
111
+ // The check the PPX cannot make, in the one place that can: the payload is a
112
+ // constructor reference the PPX only ever sees as a name, and whether that name
113
+ // is a case of the field's enum needs the schema.
114
+ //
115
+ // Two rules, and the second is the one the form exists for. A `value` on a field
116
+ // that is not the record's lifecycle would keep the read narrowing while silently
117
+ // losing the command filtering that motivates it — `@allowedStates` is written in
118
+ // terms of the lifecycle field, so a retirement state anywhere else is a state no
119
+ // command can name.
120
+ let checkRetiredValue = (~entityName: string, stateSchema: S.t<unknown>): unit =>
121
+ switch retiredFromStateSchema(stateSchema) {
122
+ | Some({field, values: Some(values)}) =>
123
+ let named = values->Array.joinWith(", ")
124
+ let lifecycle = lifecycleFieldFromStateSchema(~entityName, stateSchema)
125
+ if lifecycle != Some(field) {
126
+ log.warn(
127
+ ~comp="Plugin_Structure",
128
+ `${entityName}: @retired(${named}) is on "${field}", which is not this record's lifecycle field${lifecycle
129
+ ->Option.map(f => ` (that is "${f}")`)
130
+ ->Option.getOr(
131
+ " (it declares none)",
132
+ )}. A retirement state no command's @allowedStates can name loses the command filtering the state form exists for.`,
133
+ )
134
+ }
135
+ let declared = switch stateSchema {
136
+ | Object({items}) =>
137
+ items
138
+ ->Array.find(item => item.location == field)
139
+ ->Option.map(item =>
140
+ switch shapeOfItem(~entityName, item) {
141
+ | Enum(_, values) => values
142
+ | Nullable(Enum(_, values)) => values
143
+ | _ => []
144
+ }
145
+ )
146
+ ->Option.getOr([])
147
+ | _ => []
148
+ }
149
+ // Reported per state rather than as a set: one wrong entry among three still
150
+ // narrows something, so the symptom is a subset of rows leaking rather than
151
+ // all of them — which is harder to spot than the single-value case was.
152
+ if Array.length(declared) > 0 {
153
+ values
154
+ ->Array.filter(v => !(declared->Array.includes(v)))
155
+ ->Array.forEach(v =>
156
+ log.warn(
157
+ ~comp="Plugin_Structure",
158
+ `${entityName}: @retired(${v}) names a state "${field}" does not declare — known values: ${declared->Array.joinWith(
159
+ ", ",
160
+ )}.`,
161
+ )
162
+ )
163
+ }
164
+ | _ => ()
165
+ }
166
+
76
167
  // Which rung of the ladder below produced the label. Published on `queryableDef`
77
168
  // as `labelFieldSource`, because the four rungs are not equally believable and a
78
169
  // consumer with a name rule of its own has to rank the declaration against it:
@@ -752,6 +843,7 @@ let make = (
752
843
  // edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
753
844
  // aggregate→view link is also drawn from the producer's linkedViews, deduped downstream).
754
845
  let consumed = qualify(~prefix=name, R.consumedEventNames)
846
+ checkRetiredValue(~entityName=R.Spec.name, stateSchema)
755
847
  ({
756
848
  Reventless.Plugin.name: R.Spec.name,
757
849
  queryField: qf.listFieldName,
@@ -761,10 +853,12 @@ let make = (
761
853
  labelField: label.field,
762
854
  searchableFields: label.searchableFields,
763
855
  labelFieldSource: Some(labelFieldSourceToString(label.source)),
764
- statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
765
- // Same schema `statusField` reads, so the two cannot disagree about which
856
+ lifecycleField: lifecycleFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
857
+ // Same schema `lifecycleField` reads, so the two cannot disagree about which
766
858
  // fields this view has.
767
859
  ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
860
+ retiredField: retiredFieldFromStateSchema(stateSchema),
861
+ retiredValues: retiredValuesFromStateSchema(stateSchema),
768
862
  visibility: visibilityTag(R.Spec.visibility),
769
863
  chapter: chapterOf(R.Spec.name),
770
864
  // Taken from the `qf` record, never re-derived: `Api_Naming` is the only
@@ -787,6 +881,7 @@ let make = (
787
881
  ~entityName=SVS.Spec.name,
788
882
  stateSchema,
789
883
  )
884
+ checkRetiredValue(~entityName=SVS.Spec.name, stateSchema)
790
885
  ({
791
886
  Reventless.Plugin.name: SVS.Spec.name,
792
887
  queryField: qf.listFieldName,
@@ -796,8 +891,10 @@ let make = (
796
891
  labelField: label.field,
797
892
  searchableFields: label.searchableFields,
798
893
  labelFieldSource: Some(labelFieldSourceToString(label.source)),
799
- statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
894
+ lifecycleField: lifecycleFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
800
895
  ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
896
+ retiredField: retiredFieldFromStateSchema(stateSchema),
897
+ retiredValues: retiredValuesFromStateSchema(stateSchema),
801
898
  visibility: visibilityTag(SVS.Spec.visibility),
802
899
  chapter: chapterOf(SVS.Spec.name),
803
900
  singleQueryField: Some(qf.singleFieldName),
@@ -39,7 +39,7 @@ function isLabelShape(_t) {
39
39
  };
40
40
  }
41
41
 
42
- function isStatusShape(_t) {
42
+ function isLifecycleShape(_t) {
43
43
  while (true) {
44
44
  let t = _t;
45
45
  if (typeof t !== "object") {
@@ -68,15 +68,15 @@ function shapeOfItem(entityName, item) {
68
68
  return SchemaType$ReventlessCore.fromSury(entityName, item.location, item.schema);
69
69
  }
70
70
 
71
- function statusFieldFromStateSchema(entityName, stateSchema) {
71
+ function lifecycleFieldFromStateSchema(entityName, stateSchema) {
72
72
  let spec = StateAnnotations$Reventless.getSpec(stateSchema);
73
- let annotated = spec !== undefined ? spec.status : undefined;
73
+ let annotated = spec !== undefined ? spec.lifecycle : undefined;
74
74
  if (annotated !== undefined) {
75
75
  return annotated;
76
76
  } else if (stateSchema.type === "object") {
77
77
  return Stdlib_Option.map(stateSchema.items.find(item => {
78
- if (item.location === "status") {
79
- return isStatusShape(shapeOfItem(entityName, item));
78
+ if (item.location === "lifecycle") {
79
+ return isLifecycleShape(shapeOfItem(entityName, item));
80
80
  } else {
81
81
  return false;
82
82
  }
@@ -86,6 +86,64 @@ function statusFieldFromStateSchema(entityName, stateSchema) {
86
86
  }
87
87
  }
88
88
 
89
+ function retiredFromStateSchema(stateSchema) {
90
+ let spec = StateAnnotations$Reventless.getSpec(stateSchema);
91
+ if (spec !== undefined) {
92
+ return spec.retired;
93
+ }
94
+ }
95
+
96
+ function retiredFieldFromStateSchema(stateSchema) {
97
+ return Stdlib_Option.map(retiredFromStateSchema(stateSchema), r => r.field);
98
+ }
99
+
100
+ function retiredValuesFromStateSchema(stateSchema) {
101
+ return Stdlib_Option.flatMap(retiredFromStateSchema(stateSchema), r => r.values);
102
+ }
103
+
104
+ function checkRetiredValue(entityName, stateSchema) {
105
+ let match = retiredFromStateSchema(stateSchema);
106
+ if (match === undefined) {
107
+ return;
108
+ }
109
+ let values = match.values;
110
+ if (values === undefined) {
111
+ return;
112
+ }
113
+ let field = match.field;
114
+ let named = values.join(", ");
115
+ let lifecycle = lifecycleFieldFromStateSchema(entityName, stateSchema);
116
+ if (Primitive_object.notequal(lifecycle, field)) {
117
+ log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + named + `) is on "` + field + `", which is not this record's lifecycle field` + Stdlib_Option.getOr(Stdlib_Option.map(lifecycle, f => ` (that is "` + f + `")`), " (it declares none)") + `. A retirement state no command's @allowedStates can name loses the command filtering the state form exists for.`);
118
+ }
119
+ let declared;
120
+ declared = stateSchema.type === "object" ? Stdlib_Option.getOr(Stdlib_Option.map(stateSchema.items.find(item => item.location === field), item => {
121
+ let match = shapeOfItem(entityName, item);
122
+ if (typeof match !== "object") {
123
+ return [];
124
+ }
125
+ switch (match.TAG) {
126
+ case "Nullable" :
127
+ let match$1 = match._0;
128
+ if (typeof match$1 !== "object") {
129
+ return [];
130
+ } else if (match$1.TAG === "Enum") {
131
+ return match$1._1;
132
+ } else {
133
+ return [];
134
+ }
135
+ case "Enum" :
136
+ return match._1;
137
+ default:
138
+ return [];
139
+ }
140
+ }), []) : [];
141
+ if (declared.length !== 0) {
142
+ values.filter(v => !declared.includes(v)).forEach(v => log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + v + `) names a state "` + field + `" does not declare — known values: ` + declared.join(", ") + `.`));
143
+ return;
144
+ }
145
+ }
146
+
89
147
  function labelFieldSourceToString(s) {
90
148
  switch (s) {
91
149
  case "Annotation" :
@@ -535,6 +593,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
535
593
  let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
536
594
  let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
537
595
  let consumed = qualify(name, R.consumedEventNames);
596
+ checkRetiredValue(R.Spec.name, stateSchema);
538
597
  return {
539
598
  name: R.Spec.name,
540
599
  queryField: qf.listFieldName,
@@ -544,8 +603,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
544
603
  labelField: label.field,
545
604
  searchableFields: label.searchableFields,
546
605
  labelFieldSource: labelFieldSourceToString(label.source),
547
- statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
606
+ lifecycleField: lifecycleFieldFromStateSchema(R.Spec.name, stateSchema),
548
607
  ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
608
+ retiredField: retiredFieldFromStateSchema(stateSchema),
609
+ retiredValues: retiredValuesFromStateSchema(stateSchema),
549
610
  visibility: visibilityTag(R.Spec.visibility),
550
611
  chapter: componentChapters[R.Spec.name],
551
612
  singleQueryField: qf.singleFieldName,
@@ -561,6 +622,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
561
622
  let stateSchema = SVS.Spec.stateSchema;
562
623
  let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
563
624
  let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
625
+ checkRetiredValue(SVS.Spec.name, stateSchema);
564
626
  return {
565
627
  name: SVS.Spec.name,
566
628
  queryField: qf.listFieldName,
@@ -570,8 +632,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
570
632
  labelField: label.field,
571
633
  searchableFields: label.searchableFields,
572
634
  labelFieldSource: labelFieldSourceToString(label.source),
573
- statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
635
+ lifecycleField: lifecycleFieldFromStateSchema(SVS.Spec.name, stateSchema),
574
636
  ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
637
+ retiredField: retiredFieldFromStateSchema(stateSchema),
638
+ retiredValues: retiredValuesFromStateSchema(stateSchema),
575
639
  visibility: visibilityTag(SVS.Spec.visibility),
576
640
  chapter: componentChapters[SVS.Spec.name],
577
641
  singleQueryField: qf.singleFieldName,
@@ -689,10 +753,14 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
689
753
  export {
690
754
  log,
691
755
  isLabelShape,
692
- isStatusShape,
756
+ isLifecycleShape,
693
757
  conventionalLabelNames,
694
758
  shapeOfItem,
695
- statusFieldFromStateSchema,
759
+ lifecycleFieldFromStateSchema,
760
+ retiredFromStateSchema,
761
+ retiredFieldFromStateSchema,
762
+ retiredValuesFromStateSchema,
763
+ checkRetiredValue,
696
764
  labelFieldSourceToString,
697
765
  labelFieldsFromStateSchema,
698
766
  extractReferences,
@@ -48,10 +48,11 @@ let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stat
48
48
  scanSort: [],
49
49
  semantic: [],
50
50
  metric: [],
51
- status: undefined,
51
+ lifecycle: undefined,
52
52
  groupBy: "kind",
53
53
  visibility: undefined,
54
- live: undefined
54
+ live: undefined,
55
+ retired: undefined
55
56
  });
56
57
 
57
58
  let name = "Plugins";
@@ -0,0 +1,49 @@
1
+ // The platform's own admin components, as `Platform_ComponentDefinitions` serves
2
+ // them. The Plugins read model is a framework-generated queryable: its field names
3
+ // are chosen here and read by every client, so the rule that applies to it is the
4
+ // opposite of the one that applies to an authored record.
5
+ //
6
+ // An authored record may follow the convention — name the field `lifecycle` and
7
+ // write no annotation. A generated row cannot, because its field name is a
8
+ // published wire name: `status` in the SDL, in stored QueryDb rows, and in any
9
+ // client selecting it. Renaming it to satisfy a convention would move the
10
+ // contract, break selecting clients and require re-projection. So it declares
11
+ // instead, and the declaration is what this asserts — the failure mode otherwise
12
+ // is silent (a board loses its columns, a command menu stops filtering, and
13
+ // nothing fails to compile).
14
+
15
+ open JestGlobals
16
+ open Reventless.Plugin
17
+
18
+ describe("the Plugins read model declares its lifecycle", () => {
19
+ let rm: queryableDef = Platform_Admin_Structure.pluginReadModel
20
+
21
+ testSync("names the field commands branch on", () =>
22
+ expect(rm.lifecycleField)->toEqual(Some("status"))
23
+ )
24
+
25
+ testSync("and the published wire name is unchanged by the rename", () => {
26
+ // Stated as its own assertion rather than folded into the one above: the two
27
+ // say different things. That the field is declared is the fix; that it is
28
+ // still called `status` is the constraint the fix had to respect.
29
+ let sdlDeclaresStatus =
30
+ rm.lifecycleField->Option.mapOr(false, f => f == "status") &&
31
+ rm.queryField->String.startsWith("Platform_")
32
+ expect(sdlDeclaresStatus)->toEqual(true)
33
+ })
34
+
35
+ testSync("is served on the encoded def under the new key", () => {
36
+ let json =
37
+ rm->Platform_ComponentDefinitionsApi.encodeQueryableDef->JSON.stringify
38
+ expect(json->String.includes(`"lifecycleField":"status"`))->toEqual(true)
39
+ })
40
+
41
+ testSync("and never under the old one", () =>
42
+ expect(
43
+ rm
44
+ ->Platform_ComponentDefinitionsApi.encodeQueryableDef
45
+ ->JSON.stringify
46
+ ->String.includes(`"statusField"`),
47
+ )->toEqual(false)
48
+ )
49
+ })
@@ -0,0 +1,24 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Platform_Admin_Structure$ReventlessCore from "../../src/admin/Platform_Admin_Structure.res.mjs";
5
+ import * as Platform_ComponentDefinitionsApi$ReventlessCore from "../../src/admin/Platform_ComponentDefinitionsApi.res.mjs";
6
+
7
+ globalThis.describe("the Plugins read model declares its lifecycle", () => {
8
+ globalThis.test("names the field commands branch on", () => {
9
+ globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginReadModel.lifecycleField).toEqual("status");
10
+ });
11
+ globalThis.test("and the published wire name is unchanged by the rename", () => {
12
+ let sdlDeclaresStatus = Stdlib_Option.mapOr(Platform_Admin_Structure$ReventlessCore.pluginReadModel.lifecycleField, false, f => f === "status") && Platform_Admin_Structure$ReventlessCore.pluginReadModel.queryField.startsWith("Platform_");
13
+ globalThis.expect(sdlDeclaresStatus).toEqual(true);
14
+ });
15
+ globalThis.test("is served on the encoded def under the new key", () => {
16
+ let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(Platform_Admin_Structure$ReventlessCore.pluginReadModel));
17
+ globalThis.expect(json.includes(`"lifecycleField":"status"`)).toEqual(true);
18
+ });
19
+ globalThis.test("and never under the old one", () => {
20
+ globalThis.expect(JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(Platform_Admin_Structure$ReventlessCore.pluginReadModel)).includes(`"statusField"`)).toEqual(false);
21
+ });
22
+ });
23
+
24
+ /* Not a pure module */
@@ -15,7 +15,7 @@ let queryable = (~name, ~visibility=?, ~ownerField=?, ()): queryableDef => {
15
15
  labelField: "displayName",
16
16
  searchableFields: ["displayName"],
17
17
  labelFieldSource: None,
18
- statusField: None,
18
+ lifecycleField: None,
19
19
  visibility,
20
20
  chapter: None,
21
21
  singleQueryField: Some(`Ordering_${name}Single`),
@@ -23,6 +23,8 @@ let queryable = (~name, ~visibility=?, ~ownerField=?, ()): queryableDef => {
23
23
  idFieldSource: Some("convention"),
24
24
  requiredAccess: None,
25
25
  ownerField,
26
+ retiredField: None,
27
+ retiredValues: None,
26
28
  }
27
29
 
28
30
  let command = (~name, ~references=[], ~ownerField=?, ()): commandDef => {
@@ -18,8 +18,10 @@ function queryable(name, visibility, ownerField, param) {
18
18
  labelField: "displayName",
19
19
  searchableFields: ["displayName"],
20
20
  labelFieldSource: undefined,
21
- statusField: undefined,
21
+ lifecycleField: undefined,
22
22
  ownerField: ownerField,
23
+ retiredField: undefined,
24
+ retiredValues: undefined,
23
25
  visibility: visibility,
24
26
  chapter: undefined,
25
27
  singleQueryField: `Ordering_` + name + `Single`,
@@ -30,7 +30,7 @@ let qbl: queryableDef = {
30
30
  labelField: "displayName",
31
31
  searchableFields: ["displayName"],
32
32
  labelFieldSource: Some("annotation"),
33
- statusField: None,
33
+ lifecycleField: None,
34
34
  visibility: None,
35
35
  chapter: None,
36
36
  singleQueryField: Some("Catalog_Product"),
@@ -38,6 +38,8 @@ let qbl: queryableDef = {
38
38
  idFieldSource: Some("convention"),
39
39
  requiredAccess: None,
40
40
  ownerField: None,
41
+ retiredField: None,
42
+ retiredValues: None,
41
43
  }
42
44
 
43
45
  let wbl: writableDef = {
@@ -162,8 +164,8 @@ describe("encodePluginStructureEntry", () => {
162
164
  expect(json->String.includes("\"apiExposed\":true"))->toEqual(true)
163
165
  )
164
166
 
165
- testSync("encodes None statusField as null", () =>
166
- expect(json->String.includes("\"statusField\":null"))->toEqual(true)
167
+ testSync("encodes None lifecycleField as null", () =>
168
+ expect(json->String.includes("\"lifecycleField\":null"))->toEqual(true)
167
169
  )
168
170
 
169
171
  // Phase 6.3: write-side emitted-event field schemas.
@@ -256,7 +258,7 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
256
258
  labelField: "name",
257
259
  searchableFields: ["name"],
258
260
  labelFieldSource: Some("convention"),
259
- statusField: None,
261
+ lifecycleField: None,
260
262
  visibility: Some("Internal"),
261
263
  chapter: None,
262
264
  singleQueryField: Some("Ordering_AvailableProduct"),
@@ -264,6 +266,8 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
264
266
  idFieldSource: Some("sole"),
265
267
  requiredAccess: None,
266
268
  ownerField: None,
269
+ retiredField: None,
270
+ retiredValues: None,
267
271
  }
268
272
  // A distinct name per source array: the complement has to be fed by both, and
269
273
  // reusing one def would hide a version that only walks `readModels`.
@@ -329,7 +333,7 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
329
333
  })
330
334
  })
331
335
 
332
- describe("allowedStates + statusField populated", () => {
336
+ describe("allowedStates + lifecycleField populated", () => {
333
337
  let cmdWithStates: commandDef = {
334
338
  name: "Activate",
335
339
  schema: "{}",
@@ -353,7 +357,7 @@ describe("allowedStates + statusField populated", () => {
353
357
  labelField: "name",
354
358
  searchableFields: ["name"],
355
359
  labelFieldSource: Some("convention"),
356
- statusField: Some("status"),
360
+ lifecycleField: Some("status"),
357
361
  visibility: None,
358
362
  chapter: None,
359
363
  singleQueryField: Some("Platform_Plugin"),
@@ -361,6 +365,8 @@ describe("allowedStates + statusField populated", () => {
361
365
  idFieldSource: None,
362
366
  requiredAccess: None,
363
367
  ownerField: None,
368
+ retiredField: None,
369
+ retiredValues: None,
364
370
  }
365
371
 
366
372
  let wblWithStates: writableDef = {
@@ -402,8 +408,8 @@ describe("allowedStates + statusField populated", () => {
402
408
  expect(json->String.includes("\"targetState\":\"Active\""))->toEqual(true)
403
409
  )
404
410
 
405
- testSync("encodes populated statusField as the field name string", () =>
406
- expect(json->String.includes("\"statusField\":\"status\""))->toEqual(true)
411
+ testSync("encodes populated lifecycleField as the field name string", () =>
412
+ expect(json->String.includes("\"lifecycleField\":\"status\""))->toEqual(true)
407
413
  )
408
414
 
409
415
  // "declares no errors" is an empty list, never null — the SDL types `errors` as