@reventlessdev/reventless-core 3.0.0-alpha.237 → 3.0.0-alpha.239
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 +24 -0
- package/package.json +8 -8
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/Platform_Admin_Structure.res +1 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- 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 +122 -12
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +41 -8
- package/src/components/Api/SuryToJsonSchema.res +8 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
- 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_Builder.res +7 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/src/plugin/component/Plugin_Structure.res +376 -27
- package/src/plugin/component/Plugin_Structure.res.mjs +175 -8
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/tests/admin/Platform_BakedManifestTest.res +1 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
- package/tests/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +171 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +108 -0
- package/tests/api/SuryToJsonSchemaTest.res +30 -3
- package/tests/api/SuryToJsonSchemaTest.res.mjs +49 -4
- 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 +268 -4
- package/tests/plugin/PluginStructureTest.res.mjs +284 -3
- 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/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -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";
|
|
@@ -101,20 +102,24 @@ function retiredValuesFromStateSchema(stateSchema) {
|
|
|
101
102
|
return Stdlib_Option.flatMap(retiredFromStateSchema(stateSchema), r => r.values);
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
function namedWhenRetiredFromStateSchema(stateSchema) {
|
|
106
|
+
return Stdlib_Option.mapOr(retiredFromStateSchema(stateSchema), false, r => r.namedWhenRetired);
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
function checkRetiredValue(entityName, stateSchema) {
|
|
105
110
|
let match = retiredFromStateSchema(stateSchema);
|
|
106
111
|
if (match === undefined) {
|
|
107
|
-
return;
|
|
112
|
+
return "NotDeclared";
|
|
108
113
|
}
|
|
109
114
|
let values = match.values;
|
|
110
115
|
if (values === undefined) {
|
|
111
|
-
return;
|
|
116
|
+
return "NotDeclared";
|
|
112
117
|
}
|
|
113
118
|
let field = match.field;
|
|
114
119
|
let named = values.join(", ");
|
|
115
120
|
let lifecycle = lifecycleFieldFromStateSchema(entityName, stateSchema);
|
|
116
121
|
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 @
|
|
122
|
+
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.`);
|
|
118
123
|
}
|
|
119
124
|
let declared;
|
|
120
125
|
declared = stateSchema.type === "object" ? Stdlib_Option.getOr(Stdlib_Option.map(stateSchema.items.find(item => item.location === field), item => {
|
|
@@ -138,12 +143,138 @@ function checkRetiredValue(entityName, stateSchema) {
|
|
|
138
143
|
return [];
|
|
139
144
|
}
|
|
140
145
|
}), []) : [];
|
|
141
|
-
if (declared.length
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
if (declared.length === 0) {
|
|
147
|
+
return {
|
|
148
|
+
TAG: "Unchecked",
|
|
149
|
+
_0: entityName + `: @retired(` + named + `) is on "` + field + `", whose shape carries no cases to check the names against.`
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
return {
|
|
153
|
+
TAG: "Checked",
|
|
154
|
+
_0: values.filter(v => !declared.includes(v)).map(v => entityName + `: @retired(` + v + `) names a state "` + field + `" does not declare — known values: ` + declared.join(", ") + `.`)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function reportRetiredStates(pluginName, failures, unchecked) {
|
|
160
|
+
if (unchecked.length !== 0) {
|
|
161
|
+
log.warn("Plugin_Structure", undefined, pluginName + `: ` + unchecked.length.toString() + ` @retired declaration(s) could not be checked.\n` + unchecked.join("\n"));
|
|
162
|
+
}
|
|
163
|
+
if (failures.length !== 0) {
|
|
164
|
+
return Stdlib_JsError.throwWithMessage(pluginName + `: @retired names states that do not exist.\n` + failures.join("\n"));
|
|
144
165
|
}
|
|
145
166
|
}
|
|
146
167
|
|
|
168
|
+
function lifecycleStatesFromStateSchema(entityName, stateSchema) {
|
|
169
|
+
return Stdlib_Option.flatMap(lifecycleFieldFromStateSchema(entityName, stateSchema), field => {
|
|
170
|
+
if (stateSchema.type === "object") {
|
|
171
|
+
return Stdlib_Option.map(stateSchema.items.find(item => item.location === field), item => {
|
|
172
|
+
let match = shapeOfItem(entityName, item);
|
|
173
|
+
if (typeof match !== "object") {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
switch (match.TAG) {
|
|
177
|
+
case "Nullable" :
|
|
178
|
+
let match$1 = match._0;
|
|
179
|
+
if (typeof match$1 !== "object") {
|
|
180
|
+
return [];
|
|
181
|
+
} else if (match$1.TAG === "Enum") {
|
|
182
|
+
return match$1._1;
|
|
183
|
+
} else {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
case "Enum" :
|
|
187
|
+
return match._1;
|
|
188
|
+
default:
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView) {
|
|
197
|
+
let unvalidated = {
|
|
198
|
+
contents: 0
|
|
199
|
+
};
|
|
200
|
+
let failures = [];
|
|
201
|
+
writables.forEach(w => {
|
|
202
|
+
w.commands.forEach(cmd => {
|
|
203
|
+
let t = cmd.targetState;
|
|
204
|
+
let declared = Stdlib_Option.getOr(cmd.allowedStates, []).concat(t !== undefined ? [t] : []);
|
|
205
|
+
if (declared.length === 0) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let known = Stdlib_Array.reduce(w.linkedViews, [], (acc, view) => {
|
|
209
|
+
let states = lifecycleStatesByView[view];
|
|
210
|
+
if (states !== undefined) {
|
|
211
|
+
return acc.concat(states);
|
|
212
|
+
} else {
|
|
213
|
+
return acc;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
if (known.length === 0) {
|
|
217
|
+
unvalidated.contents = unvalidated.contents + 1 | 0;
|
|
218
|
+
} else {
|
|
219
|
+
declared.filter(state => !known.includes(state)).forEach(state => {
|
|
220
|
+
failures.push(w.name + `.` + cmd.name + `: @transition names "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
if (unvalidated.contents > 0) {
|
|
226
|
+
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.`);
|
|
227
|
+
}
|
|
228
|
+
if (failures.length !== 0) {
|
|
229
|
+
return Stdlib_JsError.throwWithMessage(pluginName + `: @transition names states that do not exist.\n` + failures.join("\n"));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function lifecycleTopologyFindings(writables, lifecycleStatesByView) {
|
|
234
|
+
let findings = [];
|
|
235
|
+
Object.entries(lifecycleStatesByView).forEach(param => {
|
|
236
|
+
let states = param[1];
|
|
237
|
+
let view = param[0];
|
|
238
|
+
let edges = Stdlib_Array.reduce(writables, [], (acc, w) => {
|
|
239
|
+
if (w.linkedViews.includes(view)) {
|
|
240
|
+
return acc.concat(Stdlib_Array.reduce(w.commands, [], (inner, cmd) => {
|
|
241
|
+
let match = cmd.allowedStates;
|
|
242
|
+
let match$1 = cmd.targetState;
|
|
243
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
244
|
+
return inner.concat(match.map(from => [
|
|
245
|
+
from,
|
|
246
|
+
match$1
|
|
247
|
+
]));
|
|
248
|
+
} else {
|
|
249
|
+
return inner;
|
|
250
|
+
}
|
|
251
|
+
}));
|
|
252
|
+
} else {
|
|
253
|
+
return acc;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
if (edges.length === 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let initial = states[0];
|
|
260
|
+
let reachable = edges.map(param => param[1]);
|
|
261
|
+
states.forEach(state => {
|
|
262
|
+
if (!reachable.includes(state) && Primitive_object.notequal(state, initial)) {
|
|
263
|
+
findings.push([
|
|
264
|
+
view,
|
|
265
|
+
`no command declares a transition INTO "` + state + `" — it is unreachable unless something outside this plugin's declarations puts a row there.`
|
|
266
|
+
]);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
return findings;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function checkLifecycleTopology(pluginName, writables, lifecycleStatesByView) {
|
|
275
|
+
lifecycleTopologyFindings(writables, lifecycleStatesByView).forEach(param => log.warn("Plugin_Structure", undefined, pluginName + `/` + param[0] + `: ` + param[1]));
|
|
276
|
+
}
|
|
277
|
+
|
|
147
278
|
function labelFieldSourceToString(s) {
|
|
148
279
|
switch (s) {
|
|
149
280
|
case "Annotation" :
|
|
@@ -587,13 +718,37 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
587
718
|
return "Internal";
|
|
588
719
|
}
|
|
589
720
|
};
|
|
721
|
+
let lifecycleStatesByView = {};
|
|
722
|
+
let recordLifecycle = (entityName, stateSchema) => {
|
|
723
|
+
let states = lifecycleStatesFromStateSchema(entityName, stateSchema);
|
|
724
|
+
if (states !== undefined && states.length !== 0) {
|
|
725
|
+
lifecycleStatesByView[entityName] = states;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
let retiredFailures = [];
|
|
730
|
+
let retiredUnchecked = [];
|
|
731
|
+
let recordRetired = (entityName, stateSchema) => {
|
|
732
|
+
let why = checkRetiredValue(entityName, stateSchema);
|
|
733
|
+
if (typeof why !== "object") {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (why.TAG === "Unchecked") {
|
|
737
|
+
retiredUnchecked.push(why._0);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
why._0.forEach(f => {
|
|
741
|
+
retiredFailures.push(f);
|
|
742
|
+
});
|
|
743
|
+
};
|
|
590
744
|
let readModelDefs = readModels.map(R => {
|
|
591
745
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
|
|
592
746
|
let stateSchema = R.Spec.stateSchema;
|
|
593
747
|
let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
|
|
594
748
|
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
|
|
595
749
|
let consumed = qualify(name, R.consumedEventNames);
|
|
596
|
-
|
|
750
|
+
recordRetired(R.Spec.name, stateSchema);
|
|
751
|
+
recordLifecycle(R.Spec.name, stateSchema);
|
|
597
752
|
return {
|
|
598
753
|
name: R.Spec.name,
|
|
599
754
|
queryField: qf.listFieldName,
|
|
@@ -607,6 +762,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
607
762
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
608
763
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
609
764
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
765
|
+
namedWhenRetired: namedWhenRetiredFromStateSchema(stateSchema),
|
|
610
766
|
visibility: visibilityTag(R.Spec.visibility),
|
|
611
767
|
chapter: componentChapters[R.Spec.name],
|
|
612
768
|
singleQueryField: qf.singleFieldName,
|
|
@@ -622,7 +778,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
622
778
|
let stateSchema = SVS.Spec.stateSchema;
|
|
623
779
|
let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
|
|
624
780
|
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
|
|
625
|
-
|
|
781
|
+
recordRetired(SVS.Spec.name, stateSchema);
|
|
782
|
+
recordLifecycle(SVS.Spec.name, stateSchema);
|
|
626
783
|
return {
|
|
627
784
|
name: SVS.Spec.name,
|
|
628
785
|
queryField: qf.listFieldName,
|
|
@@ -636,6 +793,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
636
793
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
637
794
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
638
795
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
796
|
+
namedWhenRetired: namedWhenRetiredFromStateSchema(stateSchema),
|
|
639
797
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
640
798
|
chapter: componentChapters[SVS.Spec.name],
|
|
641
799
|
singleQueryField: qf.singleFieldName,
|
|
@@ -735,6 +893,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
735
893
|
commandTypes: Belt_SetString.toArray(Belt_SetString.fromArray(match[2]))
|
|
736
894
|
};
|
|
737
895
|
});
|
|
896
|
+
checkDeclaredTransitions(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
897
|
+
checkLifecycleTopology(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
898
|
+
reportRetiredStates(name, retiredFailures, retiredUnchecked);
|
|
738
899
|
return {
|
|
739
900
|
readModels: readModelDefs,
|
|
740
901
|
stateViewSlices: stateViewDefs,
|
|
@@ -760,7 +921,13 @@ export {
|
|
|
760
921
|
retiredFromStateSchema,
|
|
761
922
|
retiredFieldFromStateSchema,
|
|
762
923
|
retiredValuesFromStateSchema,
|
|
924
|
+
namedWhenRetiredFromStateSchema,
|
|
763
925
|
checkRetiredValue,
|
|
926
|
+
reportRetiredStates,
|
|
927
|
+
lifecycleStatesFromStateSchema,
|
|
928
|
+
checkDeclaredTransitions,
|
|
929
|
+
lifecycleTopologyFindings,
|
|
930
|
+
checkLifecycleTopology,
|
|
764
931
|
labelFieldSourceToString,
|
|
765
932
|
labelFieldsFromStateSchema,
|
|
766
933
|
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
|
|
@@ -22,6 +22,7 @@ function queryable(name, visibility, ownerField, param) {
|
|
|
22
22
|
ownerField: ownerField,
|
|
23
23
|
retiredField: undefined,
|
|
24
24
|
retiredValues: undefined,
|
|
25
|
+
namedWhenRetired: undefined,
|
|
25
26
|
visibility: visibility,
|
|
26
27
|
chapter: undefined,
|
|
27
28
|
singleQueryField: `Ordering_` + name + `Single`,
|
|
@@ -40,6 +40,7 @@ let qbl: queryableDef = {
|
|
|
40
40
|
ownerField: None,
|
|
41
41
|
retiredField: None,
|
|
42
42
|
retiredValues: None,
|
|
43
|
+
namedWhenRetired: None,
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
let wbl: writableDef = {
|
|
@@ -268,6 +269,7 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
|
|
|
268
269
|
ownerField: None,
|
|
269
270
|
retiredField: None,
|
|
270
271
|
retiredValues: None,
|
|
272
|
+
namedWhenRetired: None,
|
|
271
273
|
}
|
|
272
274
|
// A distinct name per source array: the complement has to be fed by both, and
|
|
273
275
|
// reusing one def would hide a version that only walks `readModels`.
|
|
@@ -367,6 +369,7 @@ describe("allowedStates + lifecycleField populated", () => {
|
|
|
367
369
|
ownerField: None,
|
|
368
370
|
retiredField: None,
|
|
369
371
|
retiredValues: None,
|
|
372
|
+
namedWhenRetired: None,
|
|
370
373
|
}
|
|
371
374
|
|
|
372
375
|
let wblWithStates: writableDef = {
|
|
@@ -55,6 +55,7 @@ let qbl = {
|
|
|
55
55
|
ownerField: undefined,
|
|
56
56
|
retiredField: undefined,
|
|
57
57
|
retiredValues: undefined,
|
|
58
|
+
namedWhenRetired: undefined,
|
|
58
59
|
visibility: undefined,
|
|
59
60
|
chapter: undefined,
|
|
60
61
|
singleQueryField: qbl_singleQueryField,
|
|
@@ -288,6 +289,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
288
289
|
ownerField: undefined,
|
|
289
290
|
retiredField: undefined,
|
|
290
291
|
retiredValues: undefined,
|
|
292
|
+
namedWhenRetired: undefined,
|
|
291
293
|
visibility: internalQbl_visibility,
|
|
292
294
|
chapter: undefined,
|
|
293
295
|
singleQueryField: internalQbl_singleQueryField,
|
|
@@ -316,6 +318,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
316
318
|
ownerField: undefined,
|
|
317
319
|
retiredField: undefined,
|
|
318
320
|
retiredValues: undefined,
|
|
321
|
+
namedWhenRetired: undefined,
|
|
319
322
|
visibility: internalSlice_visibility,
|
|
320
323
|
chapter: undefined,
|
|
321
324
|
singleQueryField: internalSlice_singleQueryField,
|
|
@@ -422,6 +425,7 @@ globalThis.describe("allowedStates + lifecycleField populated", () => {
|
|
|
422
425
|
ownerField: undefined,
|
|
423
426
|
retiredField: undefined,
|
|
424
427
|
retiredValues: undefined,
|
|
428
|
+
namedWhenRetired: undefined,
|
|
425
429
|
visibility: undefined,
|
|
426
430
|
chapter: undefined,
|
|
427
431
|
singleQueryField: qblWithStatus_singleQueryField,
|
|
@@ -503,6 +507,7 @@ globalThis.describe("singleQueryField reaches the caller", () => {
|
|
|
503
507
|
ownerField: undefined,
|
|
504
508
|
retiredField: undefined,
|
|
505
509
|
retiredValues: undefined,
|
|
510
|
+
namedWhenRetired: undefined,
|
|
506
511
|
visibility: undefined,
|
|
507
512
|
chapter: undefined,
|
|
508
513
|
singleQueryField: undefined,
|
|
@@ -550,6 +555,7 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
|
|
|
550
555
|
ownerField: undefined,
|
|
551
556
|
retiredField: undefined,
|
|
552
557
|
retiredValues: undefined,
|
|
558
|
+
namedWhenRetired: undefined,
|
|
553
559
|
visibility: undefined,
|
|
554
560
|
chapter: undefined,
|
|
555
561
|
singleQueryField: "Catalog_Product",
|
|
@@ -35,6 +35,7 @@ let publicRm = {
|
|
|
35
35
|
ownerField: undefined,
|
|
36
36
|
retiredField: undefined,
|
|
37
37
|
retiredValues: undefined,
|
|
38
|
+
namedWhenRetired: undefined,
|
|
38
39
|
visibility: undefined,
|
|
39
40
|
chapter: publicRm_chapter,
|
|
40
41
|
singleQueryField: publicRm_singleQueryField,
|
|
@@ -74,6 +75,7 @@ let internalRm = {
|
|
|
74
75
|
ownerField: undefined,
|
|
75
76
|
retiredField: undefined,
|
|
76
77
|
retiredValues: undefined,
|
|
78
|
+
namedWhenRetired: undefined,
|
|
77
79
|
visibility: internalRm_visibility,
|
|
78
80
|
chapter: internalRm_chapter,
|
|
79
81
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Step 5 of docs/plans/aggregate-snapshotting.md — the Aggregate_Callback
|
|
1
|
+
// Step 5 of docs/plans/done/aggregate-snapshotting.md — the Aggregate_Callback
|
|
2
2
|
// snapshot wiring: a snapshot-enabled behavior seeds cold replays from the
|
|
3
3
|
// latest persisted snapshot (hash-gated) and writes a fresh one every
|
|
4
4
|
// `interval` events, fire-and-forget. Snapshots never affect correctness — the
|
|
@@ -340,3 +340,174 @@ describe("GraphQL_FragmentGenerator.mutationArgTypes", () => {
|
|
|
340
340
|
expect((mismatched, field->String.length > 0))->toEqual(([], true))
|
|
341
341
|
})
|
|
342
342
|
})
|
|
343
|
+
|
|
344
|
+
module RefDoorRow = {
|
|
345
|
+
@schema
|
|
346
|
+
type state = {id: string, name: string}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let queryFor = (fragment, needle) =>
|
|
350
|
+
GraphQL_Stitcher.decode(fragment).queries->Array.find(q => q->String.includes(needle))
|
|
351
|
+
|
|
352
|
+
let refDoorFragment = (~subIdField=?) =>
|
|
353
|
+
GraphQL_FragmentGenerator.generate(
|
|
354
|
+
~mutationEntries=[],
|
|
355
|
+
~queryEntries=[
|
|
356
|
+
{
|
|
357
|
+
ReventlessInfra.Api.singleFieldName: "Catalog_Product",
|
|
358
|
+
listFieldName: "Catalog_Products",
|
|
359
|
+
returnTypeName: "Catalog_Product",
|
|
360
|
+
stateSchema: RefDoorRow.stateSchema->S.castToUnknown,
|
|
361
|
+
authorization: None,
|
|
362
|
+
connectionSpec: true,
|
|
363
|
+
subIdField: ?subIdField,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
// What a caller holding a pointer to a withheld row may learn about it, and what
|
|
369
|
+
// the two doors that could never answer for one can now be asked.
|
|
370
|
+
describe("the reference door and the archive argument", () => {
|
|
371
|
+
open Expect
|
|
372
|
+
|
|
373
|
+
// The narrowness is the type's, not a rule each backend re-implements: a caller
|
|
374
|
+
// cannot ask this door for a price, because there is no price on it.
|
|
375
|
+
testSync("projects a reference to id, label and the state that retired it", () => {
|
|
376
|
+
let sdl = GraphQL_FragmentGenerator.deriveRefTypeSdl(~returnTypeName="Catalog_Product")
|
|
377
|
+
expect((
|
|
378
|
+
sdl->String.includes("type Catalog_ProductRef"),
|
|
379
|
+
sdl->String.includes("label: String!"),
|
|
380
|
+
sdl->String.includes("retiredState: String"),
|
|
381
|
+
sdl->String.includes("price"),
|
|
382
|
+
))->toEqual((true, true, true, false))
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Emitted for every view rather than only the annotated ones, on the reasoning
|
|
386
|
+
// `includeRetired` already states: a field that appears with an annotation makes
|
|
387
|
+
// adding or removing it a breaking schema change.
|
|
388
|
+
testSync("names the door after the list field it resolves against", () =>
|
|
389
|
+
expect(
|
|
390
|
+
GraphQL_FragmentGenerator.deriveRefsQueryField(
|
|
391
|
+
~listFieldName="Catalog_Products",
|
|
392
|
+
~returnTypeName="Catalog_Product",
|
|
393
|
+
),
|
|
394
|
+
)->toEqual(" Catalog_ProductsRefs(ids: [ID!]!): [Catalog_ProductRef!]!")
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
// Every assertion above reads a `derive*` helper directly, which is how a door
|
|
398
|
+
// that no `generate` call emitted still passed them all: the SDL both AppSync
|
|
399
|
+
// APIs are built from comes from `generate`, while the adapter provisions a
|
|
400
|
+
// `<list>Refs` resolver per queryable on the premise that it is declared there.
|
|
401
|
+
// AppSync rejects a resolver for a field its schema does not have, so the
|
|
402
|
+
// mismatch surfaced as a failed deploy rather than a missing feature.
|
|
403
|
+
testSync("emits the door from generate, not only from the helper", () =>
|
|
404
|
+
expect((
|
|
405
|
+
queryFor(refDoorFragment(), "Catalog_ProductsRefs(ids: [ID!]!)")->Option.isSome,
|
|
406
|
+
typeDefFor(refDoorFragment(), "type Catalog_ProductRef")->Option.isSome,
|
|
407
|
+
))->toEqual((true, true))
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
// The condition the resolver side uses, mirrored: a composite-key view gets no
|
|
411
|
+
// door, because the read behind it is a BatchGetItem that would need both keys.
|
|
412
|
+
// Emitting the field for one would restore the same mismatch, reversed.
|
|
413
|
+
testSync("withholds the door from a composite-key view", () =>
|
|
414
|
+
expect((
|
|
415
|
+
queryFor(refDoorFragment(~subIdField="lineNo"), "Catalog_ProductsRefs")->Option.isSome,
|
|
416
|
+
typeDefFor(refDoorFragment(~subIdField="lineNo"), "type Catalog_ProductRef")->Option.isSome,
|
|
417
|
+
))->toEqual((false, false))
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
// The dead end this closes: the archive toggle put retired rows on screen and
|
|
421
|
+
// clicking one read a door that refused them to every caller alive, elevated
|
|
422
|
+
// included, because there was no way to ask.
|
|
423
|
+
testSync("lets the single-entity and by-ids doors be asked for the archive", () =>
|
|
424
|
+
expect((
|
|
425
|
+
GraphQL_FragmentGenerator.deriveObjectQueryField(
|
|
426
|
+
~singleFieldName="Catalog_Product",
|
|
427
|
+
~typeName="Catalog_Product",
|
|
428
|
+
)->String.includes("includeRetired: Boolean"),
|
|
429
|
+
GraphQL_FragmentGenerator.deriveByIdsQueryField(
|
|
430
|
+
~listFieldName="Catalog_Products",
|
|
431
|
+
~returnTypeName="Catalog_Product",
|
|
432
|
+
)->String.includes("includeRetired: Boolean"),
|
|
433
|
+
))->toEqual((true, true))
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
// A sub-id read keeps its sort-key argument beside the new one; dropping it
|
|
437
|
+
// would silently turn a two-key door into a one-key one.
|
|
438
|
+
testSync("keeps the sub-id argument when the view has one", () =>
|
|
439
|
+
expect(
|
|
440
|
+
GraphQL_FragmentGenerator.deriveObjectQueryField(
|
|
441
|
+
~singleFieldName="Ordering_OrderLine",
|
|
442
|
+
~typeName="Ordering_OrderLine",
|
|
443
|
+
~subIdField="lineNo",
|
|
444
|
+
),
|
|
445
|
+
)->toEqual(" Ordering_OrderLine(id: ID!, lineNo: String!, includeRetired: Boolean): Ordering_OrderLine")
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// The by-index door is asserted whole rather than by `String.includes`, because
|
|
450
|
+
// what was wrong with it was never a missing substring: the field declared an
|
|
451
|
+
// `id` its resolver did not read and omitted the key its resolver did, so every
|
|
452
|
+
// part of the signature is load-bearing.
|
|
453
|
+
describe("the by-index door", () => {
|
|
454
|
+
let index = (~index, ~idField=?, ~subIdField=?): Reventless.ReadModel.indexConfig => {
|
|
455
|
+
index,
|
|
456
|
+
type_: "S",
|
|
457
|
+
idField: ?idField,
|
|
458
|
+
subIdField: ?subIdField,
|
|
459
|
+
projectionType: ALL,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
testSync("takes the index value it filters on, and can be asked for the archive", () =>
|
|
463
|
+
expect(
|
|
464
|
+
GraphQL_FragmentGenerator.deriveIndexQueryField(
|
|
465
|
+
~singleFieldName="Catalog_Product",
|
|
466
|
+
~indexConfig=index(~index="categoryId"),
|
|
467
|
+
~connectionTypeName="Catalog_ProductConnection",
|
|
468
|
+
),
|
|
469
|
+
)->toEqual(
|
|
470
|
+
" Catalog_ProductByCategoryId(categoryId: String!, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): Catalog_ProductConnection!",
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
// A named index has two different strings in play — `byOwner` names the index,
|
|
475
|
+
// `ownerId` names the column. The field is named after the first and keyed on
|
|
476
|
+
// the second, and swapping them is how the local adapter came to offer an
|
|
477
|
+
// argument the row had no field for.
|
|
478
|
+
testSync("names the field after the index and keys it on the column", () =>
|
|
479
|
+
expect(
|
|
480
|
+
GraphQL_FragmentGenerator.deriveIndexQueryField(
|
|
481
|
+
~singleFieldName="Ordering_Order",
|
|
482
|
+
~indexConfig=index(~index="byOwner", ~idField="ownerId"),
|
|
483
|
+
~connectionTypeName="Ordering_OrderConnection",
|
|
484
|
+
),
|
|
485
|
+
)->toEqual(
|
|
486
|
+
" Ordering_OrderByOwner(ownerId: String!, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): Ordering_OrderConnection!",
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
// Optional, unlike the partition argument: naming the index value is the point
|
|
491
|
+
// of the door, narrowing to one sort value is a refinement.
|
|
492
|
+
testSync("offers the sort key the sort template has always read", () =>
|
|
493
|
+
expect(
|
|
494
|
+
GraphQL_FragmentGenerator.deriveIndexQueryField(
|
|
495
|
+
~singleFieldName="Ordering_Order",
|
|
496
|
+
~indexConfig=index(~index="byCustomer", ~idField="customerId", ~subIdField="placedAt"),
|
|
497
|
+
~connectionTypeName="Ordering_OrderConnection",
|
|
498
|
+
),
|
|
499
|
+
)->toEqual(
|
|
500
|
+
" Ordering_OrderByCustomer(customerId: String!, placedAt: String, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): Ordering_OrderConnection!",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
// One derivation, called by every backend. The local adapter used to spell the
|
|
505
|
+
// name out itself and produced `XByByOwner` where this produces `XByOwner`.
|
|
506
|
+
testSync("drops a leading `by` exactly once", () =>
|
|
507
|
+
expect((
|
|
508
|
+
GraphQL_FragmentGenerator.indexQueryFieldName(~singleFieldName="X", ~index="byOwner"),
|
|
509
|
+
GraphQL_FragmentGenerator.indexQueryFieldName(~singleFieldName="X", ~index="ownerId"),
|
|
510
|
+
GraphQL_FragmentGenerator.indexQueryFieldName(~singleFieldName="X", ~index="by"),
|
|
511
|
+
))->toEqual(("XByOwner", "XByOwnerId", "XByBy"))
|
|
512
|
+
)
|
|
513
|
+
})
|