@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
|
@@ -144,6 +144,29 @@ module PsReserveStockSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
144
144
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
148
|
+
module Spec = PsDispatchShipment
|
|
149
|
+
module Behavior = {
|
|
150
|
+
type state = PsDispatchShipment.state
|
|
151
|
+
let initialState = PsDispatchShipment.initialState
|
|
152
|
+
let evolve = PsDispatchShipment.evolve
|
|
153
|
+
let decide = PsDispatchShipment.decide
|
|
154
|
+
let moduleUrl = PsDispatchShipment.moduleUrl
|
|
155
|
+
}
|
|
156
|
+
let isAsync = false
|
|
157
|
+
type component = scsComponent
|
|
158
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
159
|
+
}
|
|
160
|
+
module PsShipmentsViewSlice: ReventlessInfra.StateViewSlice.T = {
|
|
161
|
+
module Spec = PsShipmentsView
|
|
162
|
+
module Projection = {
|
|
163
|
+
let project = PsShipmentsView.project
|
|
164
|
+
let moduleUrl = PsShipmentsView.moduleUrl
|
|
165
|
+
}
|
|
166
|
+
type component = svsComponent
|
|
167
|
+
let make = (~dcbEventLog as _, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
168
|
+
}
|
|
169
|
+
|
|
147
170
|
let structure = Plugin_Structure.make(
|
|
148
171
|
~name="TestPlugin",
|
|
149
172
|
~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
|
|
@@ -155,6 +178,132 @@ let structure = Plugin_Structure.make(
|
|
|
155
178
|
],
|
|
156
179
|
)
|
|
157
180
|
|
|
181
|
+
describe("lifecycle topology", () => {
|
|
182
|
+
let ordersStates = Dict.fromArray([("Orders", ["Placed", "Shipped", "Cancelled"])])
|
|
183
|
+
let writableWith = (commands): Reventless.Plugin.writableDef => {
|
|
184
|
+
name: "Ordering",
|
|
185
|
+
commands,
|
|
186
|
+
producedEventTypes: [],
|
|
187
|
+
consumedEventTypes: [],
|
|
188
|
+
linkedViews: ["Orders"],
|
|
189
|
+
consistencyRead: None,
|
|
190
|
+
events: [],
|
|
191
|
+
errors: [],
|
|
192
|
+
chapter: None,
|
|
193
|
+
}
|
|
194
|
+
let command = (~name, ~allowedStates=?, ~targetState=?, ()): Reventless.Plugin.commandDef => {
|
|
195
|
+
name,
|
|
196
|
+
schema: "",
|
|
197
|
+
level: Instance,
|
|
198
|
+
aggregateIdField: None,
|
|
199
|
+
mutationField: "",
|
|
200
|
+
references: [],
|
|
201
|
+
allowedStates,
|
|
202
|
+
targetState,
|
|
203
|
+
apiExposed: None,
|
|
204
|
+
requiredAccess: None,
|
|
205
|
+
ownerField: None,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
testSync("names a state no command can reach", () => {
|
|
209
|
+
// `Cancelled` is in the enum and nothing declares an edge into it.
|
|
210
|
+
let findings = Plugin_Structure.lifecycleTopologyFindings(
|
|
211
|
+
~writables=[
|
|
212
|
+
writableWith([command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ())]),
|
|
213
|
+
],
|
|
214
|
+
~lifecycleStatesByView=ordersStates,
|
|
215
|
+
)
|
|
216
|
+
expect(findings->Array.map(((_, m)) => m->String.includes("Cancelled")))->toEqual([true])
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
testSync("says nothing when every state is reachable", () => {
|
|
220
|
+
let findings = Plugin_Structure.lifecycleTopologyFindings(
|
|
221
|
+
~writables=[
|
|
222
|
+
writableWith([
|
|
223
|
+
command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ()),
|
|
224
|
+
command(~name="Cancel", ~allowedStates=["Placed"], ~targetState="Cancelled", ()),
|
|
225
|
+
]),
|
|
226
|
+
],
|
|
227
|
+
~lifecycleStatesByView=ordersStates,
|
|
228
|
+
)
|
|
229
|
+
expect(findings->Array.length)->toBe(0)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// The first declared state is where rows begin, so nothing pointing at it is
|
|
233
|
+
// expected rather than suspicious.
|
|
234
|
+
testSync("does not call the initial state unreachable", () => {
|
|
235
|
+
let findings = Plugin_Structure.lifecycleTopologyFindings(
|
|
236
|
+
~writables=[
|
|
237
|
+
writableWith([
|
|
238
|
+
command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ()),
|
|
239
|
+
command(~name="Cancel", ~allowedStates=["Placed"], ~targetState="Cancelled", ()),
|
|
240
|
+
]),
|
|
241
|
+
],
|
|
242
|
+
~lifecycleStatesByView=ordersStates,
|
|
243
|
+
)
|
|
244
|
+
expect(findings->Array.map(((_, m)) => m->String.includes("Placed")))->toEqual([])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// A terminal state is not a finding. `Shipped` and `Refunded` have no way out
|
|
248
|
+
// in the shipped aggregates example, and both are correct — which is why the
|
|
249
|
+
// "dead end" rule this pass was going to carry was dropped rather than
|
|
250
|
+
// silenced. See the comment in Plugin_Structure.
|
|
251
|
+
testSync("does not report a state with no way out", () => {
|
|
252
|
+
let findings = Plugin_Structure.lifecycleTopologyFindings(
|
|
253
|
+
~writables=[
|
|
254
|
+
writableWith([
|
|
255
|
+
command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ()),
|
|
256
|
+
command(~name="Cancel", ~allowedStates=["Placed"], ~targetState="Cancelled", ()),
|
|
257
|
+
]),
|
|
258
|
+
],
|
|
259
|
+
~lifecycleStatesByView=ordersStates,
|
|
260
|
+
)
|
|
261
|
+
expect(findings->Array.length)->toBe(0)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe("@transition cross-check", () => {
|
|
266
|
+
// The exit criterion: a state no linked view declares stops the build, and the
|
|
267
|
+
// message says which command, which state, and what it was checked against.
|
|
268
|
+
// `DispatchShipment` produces `ShipmentDispatched`, which the `Shipments` view
|
|
269
|
+
// consumes — so the two are linked, the view's lifecycle is in hand, and
|
|
270
|
+
// "Dispatchd" has somewhere to be wrong.
|
|
271
|
+
let buildBad = () =>
|
|
272
|
+
try {
|
|
273
|
+
Plugin_Structure.make(
|
|
274
|
+
~name="TransitionPlugin",
|
|
275
|
+
~stateChangeSlices=[module(PsDispatchShipmentSlice)],
|
|
276
|
+
~stateViewSlices=[module(PsShipmentsViewSlice)],
|
|
277
|
+
)->ignore
|
|
278
|
+
None
|
|
279
|
+
} catch {
|
|
280
|
+
| Exn.Error(e) => Some(Exn.message(e)->Option.getOr(""))
|
|
281
|
+
| _ => Some("")
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
testSync("a state no linked view declares fails the build", () => {
|
|
285
|
+
expect(buildBad()->Option.isSome)->toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
testSync("the failure names the command, the state and the view", () => {
|
|
289
|
+
let message = buildBad()->Option.getOr("")
|
|
290
|
+
expect((
|
|
291
|
+
message->String.includes("DispatchShipment"),
|
|
292
|
+
message->String.includes("Dispatchd"),
|
|
293
|
+
message->String.includes("Shipments"),
|
|
294
|
+
message->String.includes("Booked"),
|
|
295
|
+
))->toEqual((true, true, true, true))
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// The other half of the rule: a correctly-spelled edge passes, and a command
|
|
299
|
+
// whose linked views declare no lifecycle is warned about rather than failed —
|
|
300
|
+
// `PsShipOrder` is exactly that shape, and the suite's main `structure` above
|
|
301
|
+
// builds without throwing, which is that path already exercised.
|
|
302
|
+
testSync("a plugin whose views declare no lifecycle still builds", () => {
|
|
303
|
+
expect(structure.stateChangeSlices->Array.length)->toBe(2)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
158
307
|
describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
159
308
|
describe("stateChangeSlices", () => {
|
|
160
309
|
testSync("produces two SCS entries in declaration order", () => {
|
|
@@ -233,8 +382,8 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
233
382
|
))->toEqual((Some(true), Some(false)))
|
|
234
383
|
})
|
|
235
384
|
|
|
236
|
-
testSync("ShipOrder: @
|
|
237
|
-
// End-to-end: the reventless-ppx @
|
|
385
|
+
testSync("ShipOrder: @transition's target flows through the PPX to commandDef.targetState", () => {
|
|
386
|
+
// End-to-end: the reventless-ppx @transition annotation → markTargetState
|
|
238
387
|
// metadata → ApiTargetStateHelpers.getTargetState → commandDef. The
|
|
239
388
|
// un-annotated CancelShipment carries None (resolver falls back to
|
|
240
389
|
// name-stem).
|
|
@@ -246,6 +395,18 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
246
395
|
))->toEqual((Some("Shipped"), None))
|
|
247
396
|
})
|
|
248
397
|
|
|
398
|
+
testSync("ShipOrder: @transition's from-set flows through to commandDef.allowedStates", () => {
|
|
399
|
+
// The other half of the same annotation, which the removed pair spelled
|
|
400
|
+
// separately: one attribute now fills both fields, so a command that
|
|
401
|
+
// declares a target cannot end up without the states it may run from.
|
|
402
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
403
|
+
let byName = name => shipOrder.commands->Array.find(c => c.name == name)
|
|
404
|
+
expect((
|
|
405
|
+
byName("ShipOrder")->Option.flatMap(c => c.allowedStates),
|
|
406
|
+
byName("CancelShipment")->Option.flatMap(c => c.allowedStates),
|
|
407
|
+
))->toEqual((Some(["Placed"]), None))
|
|
408
|
+
})
|
|
409
|
+
|
|
249
410
|
testSync("ShipOrder: the @noApi variant carries no callable mutation field (no sibling leak)", () => {
|
|
250
411
|
// Regression: for a single-exposed-command slice, `mutationFieldFor`
|
|
251
412
|
// resolves every variant — including the @noApi one — to the slice's one
|
|
@@ -777,7 +938,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
777
938
|
groupBy: None,
|
|
778
939
|
visibility: None,
|
|
779
940
|
live: None,
|
|
780
|
-
retired: Some({field, label: "", showWhenFalse: false, values: None}),
|
|
941
|
+
retired: Some({field, label: "", showWhenFalse: false, values: None, namedWhenRetired: false}),
|
|
781
942
|
},
|
|
782
943
|
)
|
|
783
944
|
|
|
@@ -812,6 +973,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
812
973
|
label: "",
|
|
813
974
|
showWhenFalse: false,
|
|
814
975
|
values: Some(["Deactivated", "Closed"]),
|
|
976
|
+
namedWhenRetired: false,
|
|
815
977
|
}),
|
|
816
978
|
},
|
|
817
979
|
)
|
|
@@ -856,6 +1018,108 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
856
1018
|
})
|
|
857
1019
|
})
|
|
858
1020
|
|
|
1021
|
+
// A misspelled retirement state used to be fatal or merely logged depending on
|
|
1022
|
+
// where the enum happened to be declared — the PPX refuses it when the enum is
|
|
1023
|
+
// in the same file, and this pass, which sees the resolved schema, only warned.
|
|
1024
|
+
// File layout is not a defensible arbiter of whether a data-exposure bug stops
|
|
1025
|
+
// a build, so this rung was raised to match the other.
|
|
1026
|
+
describe("@retired state names are checked against the field's own enum", () => {
|
|
1027
|
+
let withRetiredStates = (schema, ~field, ~lifecycle, ~values) =>
|
|
1028
|
+
schema->S.castToUnknown->S.Metadata.set(
|
|
1029
|
+
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
1030
|
+
{
|
|
1031
|
+
ids: [],
|
|
1032
|
+
compositeIds: [],
|
|
1033
|
+
subIds: [],
|
|
1034
|
+
compositeSubIds: [],
|
|
1035
|
+
indexes: [],
|
|
1036
|
+
hidden: [],
|
|
1037
|
+
summary: [],
|
|
1038
|
+
drillTargets: [],
|
|
1039
|
+
drillTargetKeys: [],
|
|
1040
|
+
collapsed: [],
|
|
1041
|
+
scan: [],
|
|
1042
|
+
scanSort: [],
|
|
1043
|
+
semantic: [],
|
|
1044
|
+
metric: [],
|
|
1045
|
+
lifecycle,
|
|
1046
|
+
groupBy: None,
|
|
1047
|
+
visibility: None,
|
|
1048
|
+
live: None,
|
|
1049
|
+
retired: Some({field, label: "", showWhenFalse: false, values, namedWhenRetired: false}),
|
|
1050
|
+
},
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
let shelf = values =>
|
|
1054
|
+
S.schema(s =>
|
|
1055
|
+
{
|
|
1056
|
+
"shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
|
|
1057
|
+
}
|
|
1058
|
+
)->withRetiredStates(~field="shelf", ~lifecycle=Some("shelf"), ~values=Some(values))
|
|
1059
|
+
|
|
1060
|
+
let check = schema => Plugin_Structure.checkRetiredValue(~entityName="Products", schema)
|
|
1061
|
+
|
|
1062
|
+
testSync("a declared state passes", () => {
|
|
1063
|
+
expect(check(shelf(["Archived"])))->toEqual(Plugin_Structure.Checked([]))
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
// Reported per state rather than as a set: one wrong entry among several
|
|
1067
|
+
// still narrows something, so the symptom is a subset of rows leaking.
|
|
1068
|
+
testSync("each undeclared state is reported on its own", () => {
|
|
1069
|
+
switch check(shelf(["Archived", "Discontinud", "Withdrawn"])) {
|
|
1070
|
+
| Checked(failures) =>
|
|
1071
|
+
expect(failures->Array.length)->toBe(2)
|
|
1072
|
+
expect(failures->Array.some(f => f->String.includes("Discontinud")))->toBe(true)
|
|
1073
|
+
expect(failures->Array.some(f => f->String.includes("Withdrawn")))->toBe(true)
|
|
1074
|
+
// Naming what the field does declare is what turns the failure into a fix.
|
|
1075
|
+
expect(failures->Array.every(f => f->String.includes("Listed, Archived")))->toBe(true)
|
|
1076
|
+
| other => expect(other)->toEqual(Plugin_Structure.Checked([]))
|
|
1077
|
+
}
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
// The condition that came with promoting this: a fatal rule that is silent
|
|
1081
|
+
// when it cannot run looks exactly like one that passed.
|
|
1082
|
+
testSync("a field with no cases to compare against says so instead of passing", () => {
|
|
1083
|
+
let schema =
|
|
1084
|
+
S.schema(s => {"shelf": s.matches(S.string)})->withRetiredStates(
|
|
1085
|
+
~field="shelf",
|
|
1086
|
+
~lifecycle=Some("shelf"),
|
|
1087
|
+
~values=Some(["Archived"]),
|
|
1088
|
+
)
|
|
1089
|
+
switch check(schema) {
|
|
1090
|
+
| Unchecked(why) => expect(why->String.includes("no cases"))->toBe(true)
|
|
1091
|
+
| other => expect(other)->toEqual(Plugin_Structure.Unchecked(""))
|
|
1092
|
+
}
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
// The boolean form names no state, so there is nothing to compare — which is
|
|
1096
|
+
// not the same as having failed to compare it.
|
|
1097
|
+
testSync("the boolean form is not a skip", () => {
|
|
1098
|
+
let schema =
|
|
1099
|
+
S.schema(s => {"archived": s.matches(S.bool)})->withRetiredStates(
|
|
1100
|
+
~field="archived",
|
|
1101
|
+
~lifecycle=None,
|
|
1102
|
+
~values=None,
|
|
1103
|
+
)
|
|
1104
|
+
expect(Plugin_Structure.checkRetiredValue(~entityName="Products", schema))->toEqual(
|
|
1105
|
+
Plugin_Structure.NotDeclared,
|
|
1106
|
+
)
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
// The second rule stays a warning: retirement still works, and which field
|
|
1110
|
+
// carries it is a modelling judgement rather than a name that is wrong.
|
|
1111
|
+
testSync("a state on a non-lifecycle field is not a failure", () => {
|
|
1112
|
+
let schema =
|
|
1113
|
+
S.schema(s =>
|
|
1114
|
+
{
|
|
1115
|
+
"shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
|
|
1116
|
+
"lifecycle": s.matches(S.union([S.literal("Draft"), S.literal("Live")])),
|
|
1117
|
+
}
|
|
1118
|
+
)->withRetiredStates(~field="shelf", ~lifecycle=Some("lifecycle"), ~values=Some(["Archived"]))
|
|
1119
|
+
expect(check(schema))->toEqual(Plugin_Structure.Checked([]))
|
|
1120
|
+
})
|
|
1121
|
+
})
|
|
1122
|
+
|
|
859
1123
|
// Shared by the read-side and write-side schema cases below, which ask the
|
|
860
1124
|
// same question of the same emitter.
|
|
861
1125
|
let getProperty = (json: JSON.t, key: string): option<JSON.t> =>
|
|
@@ -962,7 +1226,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
962
1226
|
// The generator captures each component's chapter (source-folder grouping band)
|
|
963
1227
|
// and passes it as ~componentChapters; Plugin_Structure looks it up by Spec.name
|
|
964
1228
|
// and threads it onto every def, so a deployed-graph consumer can render chapter
|
|
965
|
-
// bands without workspace access. See docs/plans/deployed-chapter-grouping.md.
|
|
1229
|
+
// bands without workspace access. See docs/plans/done/deployed-chapter-grouping.md.
|
|
966
1230
|
describe("componentChapters threading", () => {
|
|
967
1231
|
let chaptered = Plugin_Structure.make(
|
|
968
1232
|
~name="TestPlugin",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Exn from "@rescript/runtime/lib/es6/Stdlib_Exn.js";
|
|
4
5
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
7
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
@@ -8,6 +9,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
8
9
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
9
10
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
11
|
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
12
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
13
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
12
14
|
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
13
15
|
import * as Api_Naming$ReventlessCore from "../../src/components/Api/Api_Naming.res.mjs";
|
|
@@ -22,8 +24,10 @@ import * as PsUploadAvatar$ReventlessCore from "./StateChangeSlice/PsUploadAvata
|
|
|
22
24
|
import * as PsAnnotatedView$ReventlessCore from "./StateViewSlice/PsAnnotatedView.res.mjs";
|
|
23
25
|
import * as PsAttachInvoice$ReventlessCore from "./StateChangeSlice/PsAttachInvoice.res.mjs";
|
|
24
26
|
import * as PsCustomersView$ReventlessCore from "./StateViewSlice/PsCustomersView.res.mjs";
|
|
27
|
+
import * as PsShipmentsView$ReventlessCore from "./StateViewSlice/PsShipmentsView.res.mjs";
|
|
25
28
|
import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
|
|
26
29
|
import * as PsCategoriesView$ReventlessCore from "./StateViewSlice/PsCategoriesView.res.mjs";
|
|
30
|
+
import * as PsDispatchShipment$ReventlessCore from "./StateChangeSlice/PsDispatchShipment.res.mjs";
|
|
27
31
|
import * as Capability_Inference$ReventlessCore from "../../src/plugin/component/Capability_Inference.res.mjs";
|
|
28
32
|
import * as PsAvailableProductsView$ReventlessCore from "./StateViewSlice/PsAvailableProductsView.res.mjs";
|
|
29
33
|
|
|
@@ -337,6 +341,62 @@ let PsReserveStockSlice = {
|
|
|
337
341
|
make: make$10
|
|
338
342
|
};
|
|
339
343
|
|
|
344
|
+
let Behavior$6 = {
|
|
345
|
+
initialState: false,
|
|
346
|
+
evolve: PsDispatchShipment$ReventlessCore.evolve,
|
|
347
|
+
decide: PsDispatchShipment$ReventlessCore.decide,
|
|
348
|
+
moduleUrl: PsDispatchShipment$ReventlessCore.moduleUrl
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
function make$11(param, param$1, param$2, param$3, param$4, param$5) {
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let PsDispatchShipmentSlice_Spec = {
|
|
356
|
+
name: PsDispatchShipment$ReventlessCore.name,
|
|
357
|
+
moduleUrl: PsDispatchShipment$ReventlessCore.moduleUrl,
|
|
358
|
+
Id: Id$Reventless.$$String,
|
|
359
|
+
consumedEventSchema: PsDispatchShipment$ReventlessCore.consumedEventSchema,
|
|
360
|
+
errorSchema: PsDispatchShipment$ReventlessCore.errorSchema,
|
|
361
|
+
eventSchema: PsDispatchShipment$ReventlessCore.eventSchema,
|
|
362
|
+
commandSchema: PsDispatchShipment$ReventlessCore.commandSchema,
|
|
363
|
+
commandAuthorization: PsDispatchShipment$ReventlessCore.commandAuthorization,
|
|
364
|
+
readConsistency: PsDispatchShipment$ReventlessCore.readConsistency
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
let PsDispatchShipmentSlice = {
|
|
368
|
+
Spec: PsDispatchShipmentSlice_Spec,
|
|
369
|
+
Behavior: Behavior$6,
|
|
370
|
+
isAsync: false,
|
|
371
|
+
make: make$11
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
let Projection$5 = {
|
|
375
|
+
project: PsShipmentsView$ReventlessCore.project,
|
|
376
|
+
moduleUrl: PsShipmentsView$ReventlessCore.moduleUrl
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
function make$12(param, param$1, param$2) {
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let PsShipmentsViewSlice_Spec = {
|
|
384
|
+
name: PsShipmentsView$ReventlessCore.name,
|
|
385
|
+
moduleUrl: PsShipmentsView$ReventlessCore.moduleUrl,
|
|
386
|
+
stateSchema: PsShipmentsView$ReventlessCore.stateSchema,
|
|
387
|
+
consumedEventSchema: PsShipmentsView$ReventlessCore.consumedEventSchema,
|
|
388
|
+
config: PsShipmentsView$ReventlessCore.config,
|
|
389
|
+
subIdConfig: undefined,
|
|
390
|
+
authorization: PsShipmentsView$ReventlessCore.authorization,
|
|
391
|
+
visibility: PsShipmentsView$ReventlessCore.visibility
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
let PsShipmentsViewSlice = {
|
|
395
|
+
Spec: PsShipmentsViewSlice_Spec,
|
|
396
|
+
Projection: Projection$5,
|
|
397
|
+
make: make$12
|
|
398
|
+
};
|
|
399
|
+
|
|
340
400
|
let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [
|
|
341
401
|
PsOrdersViewSlice,
|
|
342
402
|
PsAvailableProductsViewSlice,
|
|
@@ -347,6 +407,102 @@ let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, un
|
|
|
347
407
|
PsShipOrderSlice
|
|
348
408
|
], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
349
409
|
|
|
410
|
+
globalThis.describe("lifecycle topology", () => {
|
|
411
|
+
let ordersStates = Object.fromEntries([[
|
|
412
|
+
"Orders",
|
|
413
|
+
[
|
|
414
|
+
"Placed",
|
|
415
|
+
"Shipped",
|
|
416
|
+
"Cancelled"
|
|
417
|
+
]
|
|
418
|
+
]]);
|
|
419
|
+
let writableWith = commands => ({
|
|
420
|
+
name: "Ordering",
|
|
421
|
+
commands: commands,
|
|
422
|
+
producedEventTypes: [],
|
|
423
|
+
consumedEventTypes: [],
|
|
424
|
+
linkedViews: ["Orders"],
|
|
425
|
+
consistencyRead: undefined,
|
|
426
|
+
events: [],
|
|
427
|
+
errors: [],
|
|
428
|
+
chapter: undefined
|
|
429
|
+
});
|
|
430
|
+
let command = (name, allowedStates, targetState, param) => ({
|
|
431
|
+
name: name,
|
|
432
|
+
schema: "",
|
|
433
|
+
level: "Instance",
|
|
434
|
+
aggregateIdField: undefined,
|
|
435
|
+
mutationField: "",
|
|
436
|
+
references: [],
|
|
437
|
+
allowedStates: allowedStates,
|
|
438
|
+
targetState: targetState,
|
|
439
|
+
apiExposed: undefined,
|
|
440
|
+
requiredAccess: undefined,
|
|
441
|
+
ownerField: undefined
|
|
442
|
+
});
|
|
443
|
+
globalThis.test("names a state no command can reach", () => {
|
|
444
|
+
let findings = Plugin_Structure$ReventlessCore.lifecycleTopologyFindings([writableWith([command("Ship", ["Placed"], "Shipped", undefined)])], ordersStates);
|
|
445
|
+
globalThis.expect(findings.map(param => param[1].includes("Cancelled"))).toEqual([true]);
|
|
446
|
+
});
|
|
447
|
+
globalThis.test("says nothing when every state is reachable", () => {
|
|
448
|
+
let findings = Plugin_Structure$ReventlessCore.lifecycleTopologyFindings([writableWith([
|
|
449
|
+
command("Ship", ["Placed"], "Shipped", undefined),
|
|
450
|
+
command("Cancel", ["Placed"], "Cancelled", undefined)
|
|
451
|
+
])], ordersStates);
|
|
452
|
+
globalThis.expect(findings.length).toBe(0);
|
|
453
|
+
});
|
|
454
|
+
globalThis.test("does not call the initial state unreachable", () => {
|
|
455
|
+
let findings = Plugin_Structure$ReventlessCore.lifecycleTopologyFindings([writableWith([
|
|
456
|
+
command("Ship", ["Placed"], "Shipped", undefined),
|
|
457
|
+
command("Cancel", ["Placed"], "Cancelled", undefined)
|
|
458
|
+
])], ordersStates);
|
|
459
|
+
globalThis.expect(findings.map(param => param[1].includes("Placed"))).toEqual([]);
|
|
460
|
+
});
|
|
461
|
+
globalThis.test("does not report a state with no way out", () => {
|
|
462
|
+
let findings = Plugin_Structure$ReventlessCore.lifecycleTopologyFindings([writableWith([
|
|
463
|
+
command("Ship", ["Placed"], "Shipped", undefined),
|
|
464
|
+
command("Cancel", ["Placed"], "Cancelled", undefined)
|
|
465
|
+
])], ordersStates);
|
|
466
|
+
globalThis.expect(findings.length).toBe(0);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
globalThis.describe("@transition cross-check", () => {
|
|
471
|
+
let buildBad = () => {
|
|
472
|
+
try {
|
|
473
|
+
Plugin_Structure$ReventlessCore.make("TransitionPlugin", undefined, undefined, [PsShipmentsViewSlice], [PsDispatchShipmentSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
474
|
+
return;
|
|
475
|
+
} catch (raw_e) {
|
|
476
|
+
let e = Primitive_exceptions.internalToException(raw_e);
|
|
477
|
+
if (e.RE_EXN_ID === Stdlib_Exn.$$Error) {
|
|
478
|
+
return Stdlib_Option.getOr(e._1.message, "");
|
|
479
|
+
} else {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
globalThis.test("a state no linked view declares fails the build", () => {
|
|
485
|
+
globalThis.expect(Stdlib_Option.isSome(buildBad())).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
globalThis.test("the failure names the command, the state and the view", () => {
|
|
488
|
+
let message = Stdlib_Option.getOr(buildBad(), "");
|
|
489
|
+
globalThis.expect([
|
|
490
|
+
message.includes("DispatchShipment"),
|
|
491
|
+
message.includes("Dispatchd"),
|
|
492
|
+
message.includes("Shipments"),
|
|
493
|
+
message.includes("Booked")
|
|
494
|
+
]).toEqual([
|
|
495
|
+
true,
|
|
496
|
+
true,
|
|
497
|
+
true,
|
|
498
|
+
true
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
globalThis.test("a plugin whose views declare no lifecycle still builds", () => {
|
|
502
|
+
globalThis.expect(structure.stateChangeSlices.length).toBe(2);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
350
506
|
globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
351
507
|
globalThis.describe("stateChangeSlices", () => {
|
|
352
508
|
globalThis.test("produces two SCS entries in declaration order", () => {
|
|
@@ -424,7 +580,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
424
580
|
false
|
|
425
581
|
]);
|
|
426
582
|
});
|
|
427
|
-
globalThis.test("ShipOrder: @
|
|
583
|
+
globalThis.test("ShipOrder: @transition's target flows through the PPX to commandDef.targetState", () => {
|
|
428
584
|
let shipOrder = structure.stateChangeSlices[1];
|
|
429
585
|
let byName = name => shipOrder.commands.find(c => c.name === name);
|
|
430
586
|
globalThis.expect([
|
|
@@ -435,6 +591,17 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
435
591
|
undefined
|
|
436
592
|
]);
|
|
437
593
|
});
|
|
594
|
+
globalThis.test("ShipOrder: @transition's from-set flows through to commandDef.allowedStates", () => {
|
|
595
|
+
let shipOrder = structure.stateChangeSlices[1];
|
|
596
|
+
let byName = name => shipOrder.commands.find(c => c.name === name);
|
|
597
|
+
globalThis.expect([
|
|
598
|
+
Stdlib_Option.flatMap(byName("ShipOrder"), c => c.allowedStates),
|
|
599
|
+
Stdlib_Option.flatMap(byName("CancelShipment"), c => c.allowedStates)
|
|
600
|
+
]).toEqual([
|
|
601
|
+
["Placed"],
|
|
602
|
+
undefined
|
|
603
|
+
]);
|
|
604
|
+
});
|
|
438
605
|
globalThis.test("ShipOrder: the @noApi variant carries no callable mutation field (no sibling leak)", () => {
|
|
439
606
|
let shipOrder = structure.stateChangeSlices[1];
|
|
440
607
|
let byName = name => shipOrder.commands.find(c => c.name === name);
|
|
@@ -886,7 +1053,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
886
1053
|
field: field,
|
|
887
1054
|
label: "",
|
|
888
1055
|
showWhenFalse: false,
|
|
889
|
-
values: undefined
|
|
1056
|
+
values: undefined,
|
|
1057
|
+
namedWhenRetired: false
|
|
890
1058
|
}
|
|
891
1059
|
});
|
|
892
1060
|
globalThis.test("publishes the retirement state beside the field", () => {
|
|
@@ -916,7 +1084,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
916
1084
|
values: [
|
|
917
1085
|
"Deactivated",
|
|
918
1086
|
"Closed"
|
|
919
|
-
]
|
|
1087
|
+
],
|
|
1088
|
+
namedWhenRetired: false
|
|
920
1089
|
}
|
|
921
1090
|
});
|
|
922
1091
|
let schema = withState(S.schema(s => ({
|
|
@@ -959,6 +1128,116 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
959
1128
|
globalThis.expect(Plugin_Structure$ReventlessCore.retiredFieldFromStateSchema(schema)).toEqual(undefined);
|
|
960
1129
|
});
|
|
961
1130
|
});
|
|
1131
|
+
globalThis.describe("@retired state names are checked against the field's own enum", () => {
|
|
1132
|
+
let withRetiredStates = (schema, field, lifecycle, values) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, {
|
|
1133
|
+
ids: [],
|
|
1134
|
+
compositeIds: [],
|
|
1135
|
+
subIds: [],
|
|
1136
|
+
compositeSubIds: [],
|
|
1137
|
+
indexes: [],
|
|
1138
|
+
hidden: [],
|
|
1139
|
+
summary: [],
|
|
1140
|
+
drillTargets: [],
|
|
1141
|
+
drillTargetKeys: [],
|
|
1142
|
+
collapsed: [],
|
|
1143
|
+
scan: [],
|
|
1144
|
+
scanSort: [],
|
|
1145
|
+
semantic: [],
|
|
1146
|
+
metric: [],
|
|
1147
|
+
lifecycle: lifecycle,
|
|
1148
|
+
groupBy: undefined,
|
|
1149
|
+
visibility: undefined,
|
|
1150
|
+
live: undefined,
|
|
1151
|
+
retired: {
|
|
1152
|
+
field: field,
|
|
1153
|
+
label: "",
|
|
1154
|
+
showWhenFalse: false,
|
|
1155
|
+
values: values,
|
|
1156
|
+
namedWhenRetired: false
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
let shelf = values => withRetiredStates(S.schema(s => ({
|
|
1160
|
+
shelf: s.m(S.union([
|
|
1161
|
+
S.literal("Listed"),
|
|
1162
|
+
S.literal("Archived")
|
|
1163
|
+
]))
|
|
1164
|
+
})), "shelf", "shelf", values);
|
|
1165
|
+
globalThis.test("a declared state passes", () => {
|
|
1166
|
+
globalThis.expect(Plugin_Structure$ReventlessCore.checkRetiredValue("Products", shelf(["Archived"]))).toEqual({
|
|
1167
|
+
TAG: "Checked",
|
|
1168
|
+
_0: []
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
globalThis.test("each undeclared state is reported on its own", () => {
|
|
1172
|
+
let failures = Plugin_Structure$ReventlessCore.checkRetiredValue("Products", shelf([
|
|
1173
|
+
"Archived",
|
|
1174
|
+
"Discontinud",
|
|
1175
|
+
"Withdrawn"
|
|
1176
|
+
]));
|
|
1177
|
+
if (typeof failures !== "object") {
|
|
1178
|
+
globalThis.expect(failures).toEqual({
|
|
1179
|
+
TAG: "Checked",
|
|
1180
|
+
_0: []
|
|
1181
|
+
});
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (failures.TAG === "Unchecked") {
|
|
1185
|
+
globalThis.expect(failures).toEqual({
|
|
1186
|
+
TAG: "Checked",
|
|
1187
|
+
_0: []
|
|
1188
|
+
});
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
let failures$1 = failures._0;
|
|
1192
|
+
globalThis.expect(failures$1.length).toBe(2);
|
|
1193
|
+
globalThis.expect(failures$1.some(f => f.includes("Discontinud"))).toBe(true);
|
|
1194
|
+
globalThis.expect(failures$1.some(f => f.includes("Withdrawn"))).toBe(true);
|
|
1195
|
+
globalThis.expect(failures$1.every(f => f.includes("Listed, Archived"))).toBe(true);
|
|
1196
|
+
});
|
|
1197
|
+
globalThis.test("a field with no cases to compare against says so instead of passing", () => {
|
|
1198
|
+
let schema = withRetiredStates(S.schema(s => ({
|
|
1199
|
+
shelf: s.m(S.string)
|
|
1200
|
+
})), "shelf", "shelf", ["Archived"]);
|
|
1201
|
+
let why = Plugin_Structure$ReventlessCore.checkRetiredValue("Products", schema);
|
|
1202
|
+
if (typeof why !== "object") {
|
|
1203
|
+
globalThis.expect(why).toEqual({
|
|
1204
|
+
TAG: "Unchecked",
|
|
1205
|
+
_0: ""
|
|
1206
|
+
});
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (why.TAG === "Unchecked") {
|
|
1210
|
+
globalThis.expect(why._0.includes("no cases")).toBe(true);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
globalThis.expect(why).toEqual({
|
|
1214
|
+
TAG: "Unchecked",
|
|
1215
|
+
_0: ""
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
globalThis.test("the boolean form is not a skip", () => {
|
|
1219
|
+
let schema = withRetiredStates(S.schema(s => ({
|
|
1220
|
+
archived: s.m(S.bool)
|
|
1221
|
+
})), "archived", undefined, undefined);
|
|
1222
|
+
globalThis.expect(Plugin_Structure$ReventlessCore.checkRetiredValue("Products", schema)).toEqual("NotDeclared");
|
|
1223
|
+
});
|
|
1224
|
+
globalThis.test("a state on a non-lifecycle field is not a failure", () => {
|
|
1225
|
+
let schema = withRetiredStates(S.schema(s => ({
|
|
1226
|
+
shelf: s.m(S.union([
|
|
1227
|
+
S.literal("Listed"),
|
|
1228
|
+
S.literal("Archived")
|
|
1229
|
+
])),
|
|
1230
|
+
lifecycle: s.m(S.union([
|
|
1231
|
+
S.literal("Draft"),
|
|
1232
|
+
S.literal("Live")
|
|
1233
|
+
]))
|
|
1234
|
+
})), "shelf", "lifecycle", ["Archived"]);
|
|
1235
|
+
globalThis.expect(Plugin_Structure$ReventlessCore.checkRetiredValue("Products", schema)).toEqual({
|
|
1236
|
+
TAG: "Checked",
|
|
1237
|
+
_0: []
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
962
1241
|
let getProperty = (json, key) => {
|
|
963
1242
|
let obj = Stdlib_JSON.Decode.object(json);
|
|
964
1243
|
if (obj !== undefined) {
|
|
@@ -1275,6 +1554,8 @@ export {
|
|
|
1275
1554
|
PsUploadAvatarSlice,
|
|
1276
1555
|
PsChangePhotoSlice,
|
|
1277
1556
|
PsReserveStockSlice,
|
|
1557
|
+
PsDispatchShipmentSlice,
|
|
1558
|
+
PsShipmentsViewSlice,
|
|
1278
1559
|
structure,
|
|
1279
1560
|
}
|
|
1280
1561
|
/* structure Not a pure module */
|