@reventlessdev/reventless-core 3.0.0-alpha.236 → 3.0.0-alpha.238
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 +23 -0
- package/package.json +6 -6
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/Platform_Admin_Structure.res +5 -1
- package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res +4 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +11 -3
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
- package/src/components/Api/ApiTargetStateHelpers.res +4 -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 +79 -16
- package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
- package/src/components/EventLog/EventLog.res +1 -1
- package/src/plugin/component/Plugin_Structure.res +377 -18
- package/src/plugin/component/Plugin_Structure.res.mjs +204 -9
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- 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/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- 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/commandgenerator/OwnerStampingTest.res +57 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
- package/tests/message/MessageTest.res +1 -1
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
- package/tests/plugin/PluginStructureTest.res +332 -15
- package/tests/plugin/PluginStructureTest.res.mjs +316 -10
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
- package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +3 -2
- package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +2 -0
- package/tests/util/CsvStreamTest.res +1 -1
|
@@ -5,6 +5,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
5
5
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
7
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
8
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
9
|
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
9
10
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
10
11
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
@@ -39,7 +40,7 @@ function isLabelShape(_t) {
|
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function
|
|
43
|
+
function isLifecycleShape(_t) {
|
|
43
44
|
while (true) {
|
|
44
45
|
let t = _t;
|
|
45
46
|
if (typeof t !== "object") {
|
|
@@ -68,15 +69,15 @@ function shapeOfItem(entityName, item) {
|
|
|
68
69
|
return SchemaType$ReventlessCore.fromSury(entityName, item.location, item.schema);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function
|
|
72
|
+
function lifecycleFieldFromStateSchema(entityName, stateSchema) {
|
|
72
73
|
let spec = StateAnnotations$Reventless.getSpec(stateSchema);
|
|
73
|
-
let annotated = spec !== undefined ? spec.
|
|
74
|
+
let annotated = spec !== undefined ? spec.lifecycle : undefined;
|
|
74
75
|
if (annotated !== undefined) {
|
|
75
76
|
return annotated;
|
|
76
77
|
} else if (stateSchema.type === "object") {
|
|
77
78
|
return Stdlib_Option.map(stateSchema.items.find(item => {
|
|
78
|
-
if (item.location === "
|
|
79
|
-
return
|
|
79
|
+
if (item.location === "lifecycle") {
|
|
80
|
+
return isLifecycleShape(shapeOfItem(entityName, item));
|
|
80
81
|
} else {
|
|
81
82
|
return false;
|
|
82
83
|
}
|
|
@@ -86,6 +87,174 @@ function statusFieldFromStateSchema(entityName, stateSchema) {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function retiredFromStateSchema(stateSchema) {
|
|
91
|
+
let spec = StateAnnotations$Reventless.getSpec(stateSchema);
|
|
92
|
+
if (spec !== undefined) {
|
|
93
|
+
return spec.retired;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function retiredFieldFromStateSchema(stateSchema) {
|
|
98
|
+
return Stdlib_Option.map(retiredFromStateSchema(stateSchema), r => r.field);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function retiredValuesFromStateSchema(stateSchema) {
|
|
102
|
+
return Stdlib_Option.flatMap(retiredFromStateSchema(stateSchema), r => r.values);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function checkRetiredValue(entityName, stateSchema) {
|
|
106
|
+
let match = retiredFromStateSchema(stateSchema);
|
|
107
|
+
if (match === undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let values = match.values;
|
|
111
|
+
if (values === undefined) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let field = match.field;
|
|
115
|
+
let named = values.join(", ");
|
|
116
|
+
let lifecycle = lifecycleFieldFromStateSchema(entityName, stateSchema);
|
|
117
|
+
if (Primitive_object.notequal(lifecycle, field)) {
|
|
118
|
+
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 @transition can name loses the command filtering the state form exists for.`);
|
|
119
|
+
}
|
|
120
|
+
let declared;
|
|
121
|
+
declared = stateSchema.type === "object" ? Stdlib_Option.getOr(Stdlib_Option.map(stateSchema.items.find(item => item.location === field), item => {
|
|
122
|
+
let match = shapeOfItem(entityName, item);
|
|
123
|
+
if (typeof match !== "object") {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
switch (match.TAG) {
|
|
127
|
+
case "Nullable" :
|
|
128
|
+
let match$1 = match._0;
|
|
129
|
+
if (typeof match$1 !== "object") {
|
|
130
|
+
return [];
|
|
131
|
+
} else if (match$1.TAG === "Enum") {
|
|
132
|
+
return match$1._1;
|
|
133
|
+
} else {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
case "Enum" :
|
|
137
|
+
return match._1;
|
|
138
|
+
default:
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}), []) : [];
|
|
142
|
+
if (declared.length !== 0) {
|
|
143
|
+
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(", ") + `.`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function lifecycleStatesFromStateSchema(entityName, stateSchema) {
|
|
149
|
+
return Stdlib_Option.flatMap(lifecycleFieldFromStateSchema(entityName, stateSchema), field => {
|
|
150
|
+
if (stateSchema.type === "object") {
|
|
151
|
+
return Stdlib_Option.map(stateSchema.items.find(item => item.location === field), item => {
|
|
152
|
+
let match = shapeOfItem(entityName, item);
|
|
153
|
+
if (typeof match !== "object") {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
switch (match.TAG) {
|
|
157
|
+
case "Nullable" :
|
|
158
|
+
let match$1 = match._0;
|
|
159
|
+
if (typeof match$1 !== "object") {
|
|
160
|
+
return [];
|
|
161
|
+
} else if (match$1.TAG === "Enum") {
|
|
162
|
+
return match$1._1;
|
|
163
|
+
} else {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
case "Enum" :
|
|
167
|
+
return match._1;
|
|
168
|
+
default:
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView) {
|
|
177
|
+
let unvalidated = {
|
|
178
|
+
contents: 0
|
|
179
|
+
};
|
|
180
|
+
let failures = [];
|
|
181
|
+
writables.forEach(w => {
|
|
182
|
+
w.commands.forEach(cmd => {
|
|
183
|
+
let t = cmd.targetState;
|
|
184
|
+
let declared = Stdlib_Option.getOr(cmd.allowedStates, []).concat(t !== undefined ? [t] : []);
|
|
185
|
+
if (declared.length === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let known = Stdlib_Array.reduce(w.linkedViews, [], (acc, view) => {
|
|
189
|
+
let states = lifecycleStatesByView[view];
|
|
190
|
+
if (states !== undefined) {
|
|
191
|
+
return acc.concat(states);
|
|
192
|
+
} else {
|
|
193
|
+
return acc;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (known.length === 0) {
|
|
197
|
+
unvalidated.contents = unvalidated.contents + 1 | 0;
|
|
198
|
+
} else {
|
|
199
|
+
declared.filter(state => !known.includes(state)).forEach(state => {
|
|
200
|
+
failures.push(w.name + `.` + cmd.name + `: @transition names "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
if (unvalidated.contents > 0) {
|
|
206
|
+
log.warn("Plugin_Structure", undefined, pluginName + `: ` + unvalidated.contents.toString() + ` command(s) declare a @transition but no linked view declares a lifecycle to check it against.`);
|
|
207
|
+
}
|
|
208
|
+
if (failures.length !== 0) {
|
|
209
|
+
return Stdlib_JsError.throwWithMessage(pluginName + `: @transition names states that do not exist.\n` + failures.join("\n"));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function lifecycleTopologyFindings(writables, lifecycleStatesByView) {
|
|
214
|
+
let findings = [];
|
|
215
|
+
Object.entries(lifecycleStatesByView).forEach(param => {
|
|
216
|
+
let states = param[1];
|
|
217
|
+
let view = param[0];
|
|
218
|
+
let edges = Stdlib_Array.reduce(writables, [], (acc, w) => {
|
|
219
|
+
if (w.linkedViews.includes(view)) {
|
|
220
|
+
return acc.concat(Stdlib_Array.reduce(w.commands, [], (inner, cmd) => {
|
|
221
|
+
let match = cmd.allowedStates;
|
|
222
|
+
let match$1 = cmd.targetState;
|
|
223
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
224
|
+
return inner.concat(match.map(from => [
|
|
225
|
+
from,
|
|
226
|
+
match$1
|
|
227
|
+
]));
|
|
228
|
+
} else {
|
|
229
|
+
return inner;
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
} else {
|
|
233
|
+
return acc;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
if (edges.length === 0) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
let initial = states[0];
|
|
240
|
+
let reachable = edges.map(param => param[1]);
|
|
241
|
+
states.forEach(state => {
|
|
242
|
+
if (!reachable.includes(state) && Primitive_object.notequal(state, initial)) {
|
|
243
|
+
findings.push([
|
|
244
|
+
view,
|
|
245
|
+
`no command declares a transition INTO "` + state + `" — it is unreachable unless something outside this plugin's declarations puts a row there.`
|
|
246
|
+
]);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
return findings;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function checkLifecycleTopology(pluginName, writables, lifecycleStatesByView) {
|
|
255
|
+
lifecycleTopologyFindings(writables, lifecycleStatesByView).forEach(param => log.warn("Plugin_Structure", undefined, pluginName + `/` + param[0] + `: ` + param[1]));
|
|
256
|
+
}
|
|
257
|
+
|
|
89
258
|
function labelFieldSourceToString(s) {
|
|
90
259
|
switch (s) {
|
|
91
260
|
case "Annotation" :
|
|
@@ -529,12 +698,22 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
529
698
|
return "Internal";
|
|
530
699
|
}
|
|
531
700
|
};
|
|
701
|
+
let lifecycleStatesByView = {};
|
|
702
|
+
let recordLifecycle = (entityName, stateSchema) => {
|
|
703
|
+
let states = lifecycleStatesFromStateSchema(entityName, stateSchema);
|
|
704
|
+
if (states !== undefined && states.length !== 0) {
|
|
705
|
+
lifecycleStatesByView[entityName] = states;
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
532
709
|
let readModelDefs = readModels.map(R => {
|
|
533
710
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
|
|
534
711
|
let stateSchema = R.Spec.stateSchema;
|
|
535
712
|
let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
|
|
536
713
|
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
|
|
537
714
|
let consumed = qualify(name, R.consumedEventNames);
|
|
715
|
+
checkRetiredValue(R.Spec.name, stateSchema);
|
|
716
|
+
recordLifecycle(R.Spec.name, stateSchema);
|
|
538
717
|
return {
|
|
539
718
|
name: R.Spec.name,
|
|
540
719
|
queryField: qf.listFieldName,
|
|
@@ -544,8 +723,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
544
723
|
labelField: label.field,
|
|
545
724
|
searchableFields: label.searchableFields,
|
|
546
725
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
547
|
-
|
|
726
|
+
lifecycleField: lifecycleFieldFromStateSchema(R.Spec.name, stateSchema),
|
|
548
727
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
728
|
+
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
729
|
+
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
549
730
|
visibility: visibilityTag(R.Spec.visibility),
|
|
550
731
|
chapter: componentChapters[R.Spec.name],
|
|
551
732
|
singleQueryField: qf.singleFieldName,
|
|
@@ -561,6 +742,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
561
742
|
let stateSchema = SVS.Spec.stateSchema;
|
|
562
743
|
let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
|
|
563
744
|
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
|
|
745
|
+
checkRetiredValue(SVS.Spec.name, stateSchema);
|
|
746
|
+
recordLifecycle(SVS.Spec.name, stateSchema);
|
|
564
747
|
return {
|
|
565
748
|
name: SVS.Spec.name,
|
|
566
749
|
queryField: qf.listFieldName,
|
|
@@ -570,8 +753,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
570
753
|
labelField: label.field,
|
|
571
754
|
searchableFields: label.searchableFields,
|
|
572
755
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
573
|
-
|
|
756
|
+
lifecycleField: lifecycleFieldFromStateSchema(SVS.Spec.name, stateSchema),
|
|
574
757
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
758
|
+
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
759
|
+
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
575
760
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
576
761
|
chapter: componentChapters[SVS.Spec.name],
|
|
577
762
|
singleQueryField: qf.singleFieldName,
|
|
@@ -671,6 +856,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
671
856
|
commandTypes: Belt_SetString.toArray(Belt_SetString.fromArray(match[2]))
|
|
672
857
|
};
|
|
673
858
|
});
|
|
859
|
+
checkDeclaredTransitions(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
860
|
+
checkLifecycleTopology(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
674
861
|
return {
|
|
675
862
|
readModels: readModelDefs,
|
|
676
863
|
stateViewSlices: stateViewDefs,
|
|
@@ -689,10 +876,18 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
689
876
|
export {
|
|
690
877
|
log,
|
|
691
878
|
isLabelShape,
|
|
692
|
-
|
|
879
|
+
isLifecycleShape,
|
|
693
880
|
conventionalLabelNames,
|
|
694
881
|
shapeOfItem,
|
|
695
|
-
|
|
882
|
+
lifecycleFieldFromStateSchema,
|
|
883
|
+
retiredFromStateSchema,
|
|
884
|
+
retiredFieldFromStateSchema,
|
|
885
|
+
retiredValuesFromStateSchema,
|
|
886
|
+
checkRetiredValue,
|
|
887
|
+
lifecycleStatesFromStateSchema,
|
|
888
|
+
checkDeclaredTransitions,
|
|
889
|
+
lifecycleTopologyFindings,
|
|
890
|
+
checkLifecycleTopology,
|
|
696
891
|
labelFieldSourceToString,
|
|
697
892
|
labelFieldsFromStateSchema,
|
|
698
893
|
extractReferences,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Second mapping on the admin PluginExtensionPoint, routing UI-fragment lifecycle to the
|
|
2
|
-
// admin UiFragmentRegistry StateChangeSlice (docs/plans/event-sourced-fragment-registries.md).
|
|
2
|
+
// admin UiFragmentRegistry StateChangeSlice (docs/plans/done/event-sourced-fragment-registries.md).
|
|
3
3
|
//
|
|
4
4
|
// The EP runtime fans every incoming command through ALL mappings and flattens the results
|
|
5
5
|
// (ExtensionPoint_Callback.mapIncomingCommands). This mapping's Delegate is the slice, so its
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Tests for the Aggregate_Callback in-process replay cache (Phase 1 of
|
|
2
|
-
// docs/plans/aggregate-snapshotting.md): a warm same-id command skips the
|
|
2
|
+
// docs/plans/done/aggregate-snapshotting.md): a warm same-id command skips the
|
|
3
3
|
// event-log replay and decides on the cached (state, sequenceNr); the OCC
|
|
4
4
|
// append fences staleness — a stale cache conflicts, invalidates, and the
|
|
5
5
|
// retry replays cold.
|