@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
|
@@ -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
|
|
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
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
let rec
|
|
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) =>
|
|
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
|
|
48
|
-
//
|
|
49
|
-
// 1. Field annotated `@
|
|
50
|
-
// 2. Field literally named `"
|
|
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
|
-
|
|
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.
|
|
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 == "
|
|
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
|
-
|
|
765
|
-
// Same schema `
|
|
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
|
-
|
|
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
|
|
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
|
|
71
|
+
function lifecycleFieldFromStateSchema(entityName, stateSchema) {
|
|
72
72
|
let spec = StateAnnotations$Reventless.getSpec(stateSchema);
|
|
73
|
-
let annotated = spec !== undefined ? spec.
|
|
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 === "
|
|
79
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
756
|
+
isLifecycleShape,
|
|
693
757
|
conventionalLabelNames,
|
|
694
758
|
shapeOfItem,
|
|
695
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
166
|
-
expect(json->String.includes("\"
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
|
406
|
-
expect(json->String.includes("\"
|
|
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
|