@reventlessdev/reventless-core 3.0.0-alpha.256 → 3.0.0-alpha.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/package.json +7 -7
- package/src/adapter/Monitoring/Monitoring.res +79 -5
- package/src/adapter/Monitoring/Monitoring.res.mjs +42 -2
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/admin/Platform_UiSlots.res +21 -0
- package/src/admin/Platform_UiSlots.res.mjs +15 -0
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +13 -4
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +6 -5
- package/src/components/Api/GraphQL_FragmentGenerator.res +2 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +4 -0
- package/src/components/Api/MCP_SchemaGenerator.res.mjs +2 -2
- package/src/components/Api/SchemaType.res +8 -0
- package/src/components/Api/SchemaType.res.mjs +15 -5
- package/src/components/Api/SuryToJsonSchema.res +70 -12
- package/src/components/Api/SuryToJsonSchema.res.mjs +43 -16
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +6 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
- package/src/plugin/component/Plugin.res +1 -0
- package/src/plugin/component/Plugin_Builder.res +2 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -2
- package/src/plugin/component/Plugin_Structure.res +105 -10
- package/src/plugin/component/Plugin_Structure.res.mjs +71 -27
- package/src/util/Validation.res +1 -1
- package/src/util/Validation.res.mjs +2 -2
- package/tests/adapter/MonitoringTest.res +46 -7
- package/tests/adapter/MonitoringTest.res.mjs +47 -5
- package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -0
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +2 -14
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +1 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +2 -0
- package/tests/aggregate/AggregateCacheTest.res.mjs +1 -1
- package/tests/aggregate/AggregateConflictTest.res.mjs +1 -1
- package/tests/aggregate/AggregateFixtures.res.mjs +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +1 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +6 -4
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +5 -5
- package/tests/api/SuryToJsonSchemaTest.res.mjs +57 -57
- package/tests/api/TaggedUnionSchemaTest.res.mjs +1 -1
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +1 -1
- package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +1 -1
- package/tests/dcb/DcbFixtures.res.mjs +1 -1
- package/tests/eventmapper/EventMapperFixtures.res.mjs +1 -1
- package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +1 -1
- package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +1 -1
- package/tests/plugin/ManifestVisibilityTest.res.mjs +1 -1
- package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
- package/tests/plugin/PluginStructureTest.res +184 -0
- package/tests/plugin/PluginStructureTest.res.mjs +142 -17
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +1 -1
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +1 -1
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +1 -1
- package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +1 -1
- package/tests/plugin/StateChangeSlice/PsReserveStock.res +14 -1
- package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +9 -2
- package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +1 -1
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +1 -1
- package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +1 -1
- package/tests/util/CommandPublisherTest.res.mjs +1 -1
|
@@ -241,6 +241,7 @@ describe("lifecycle topology", () => {
|
|
|
241
241
|
references: [],
|
|
242
242
|
allowedStates,
|
|
243
243
|
targetState,
|
|
244
|
+
allowedStatesSource: ?allowedStates->Option.map(_ => "declared"),
|
|
244
245
|
apiExposed: None,
|
|
245
246
|
requiredAccess: None,
|
|
246
247
|
ownerField: None,
|
|
@@ -344,6 +345,141 @@ describe("commandTransition — the edge as a switch", () => {
|
|
|
344
345
|
})
|
|
345
346
|
})
|
|
346
347
|
|
|
348
|
+
describe("the harvested model against the switch", () => {
|
|
349
|
+
// The switch is a claim; the component's own scenarios are evidence. Where the
|
|
350
|
+
// evidence says something it wins and the structure records that it did, so a
|
|
351
|
+
// consumer can rank an observed edge against an authored one. Where it says
|
|
352
|
+
// nothing the claim stands — an empty derivation published as `Some([])` would
|
|
353
|
+
// match no row's lifecycle and offer the command nowhere at all, while the
|
|
354
|
+
// annotation that would have been right sat unread beside it.
|
|
355
|
+
let edge = (
|
|
356
|
+
~command,
|
|
357
|
+
~level: option<Reventless.Plugin.commandLevel>=?,
|
|
358
|
+
~allowedStates=[],
|
|
359
|
+
~targets=[],
|
|
360
|
+
): Reventless.Plugin.derivedEdge => {
|
|
361
|
+
component: "TransitionSwitch",
|
|
362
|
+
command,
|
|
363
|
+
level: ?level,
|
|
364
|
+
allowedStates,
|
|
365
|
+
targets,
|
|
366
|
+
}
|
|
367
|
+
let build = lifecycleModel =>
|
|
368
|
+
Plugin_Structure.make(
|
|
369
|
+
~name="HarvestPlugin",
|
|
370
|
+
~stateChangeSlices=[module(PsTransitionSwitchSlice)],
|
|
371
|
+
~stateViewSlices=[],
|
|
372
|
+
~lifecycleModel,
|
|
373
|
+
)
|
|
374
|
+
let edgeOf = (model, name) => {
|
|
375
|
+
let slice = build(model).stateChangeSlices->Array.getUnsafe(0)
|
|
376
|
+
slice.commands
|
|
377
|
+
->Array.find(c => c.name == name)
|
|
378
|
+
->Option.map(c => (c.allowedStates, c.targetState, c.allowedStatesSource))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
testSync("an observed from-set replaces the declared one, and says where it came from", () => {
|
|
382
|
+
expect(
|
|
383
|
+
edgeOf([edge(~command="Book", ~allowedStates=["Booked"], ~targets=["Abandoned"])], "Book"),
|
|
384
|
+
)->toEqual(Some((Some(["Booked"]), Some("Abandoned"), Some("derived"))))
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
testSync("a corpus that observed nothing leaves the switch standing", () => {
|
|
388
|
+
// Present in the model and empty is the same answer as absent from it: the
|
|
389
|
+
// scenarios did not reach this command.
|
|
390
|
+
expect(edgeOf([edge(~command="Book")], "Book"))->toEqual(
|
|
391
|
+
Some((Some(["Draft"]), Some("Booked"), Some("declared"))),
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
testSync("the two halves resolve separately", () => {
|
|
396
|
+
// A corpus routinely shows a command taking effect without ever showing
|
|
397
|
+
// where it lands, and erasing the declared target for that would cost a
|
|
398
|
+
// consumer the edge it had.
|
|
399
|
+
expect(edgeOf([edge(~command="Book", ~allowedStates=["Booked"])], "Book"))->toEqual(
|
|
400
|
+
Some((Some(["Booked"]), Some("Booked"), Some("derived"))),
|
|
401
|
+
)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
testSync("two observed targets leave the declaration to speak", () => {
|
|
405
|
+
// `targetState` carries one state, so a branching command is an edge it
|
|
406
|
+
// cannot express. The harvest reports that as a contradiction; here the
|
|
407
|
+
// choice is between the declaration and picking one of the two arbitrarily.
|
|
408
|
+
expect(
|
|
409
|
+
edgeOf([edge(~command="Book", ~allowedStates=["Booked"], ~targets=["Booked", "Abandoned"])], "Book"),
|
|
410
|
+
)->toEqual(Some((Some(["Booked"]), Some("Booked"), Some("derived"))))
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
testSync("a command the model does not mention keeps its declaration", () => {
|
|
414
|
+
expect(edgeOf([edge(~command="Book", ~allowedStates=["Booked"])], "Rebook"))->toEqual(
|
|
415
|
+
Some((Some(["Booked"]), None, Some("declared"))),
|
|
416
|
+
)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
testSync("an authored Unrestricted publishes no from-set, and says it meant to", () => {
|
|
420
|
+
// Distinct from the field being absent, which is "nobody said". A consumer
|
|
421
|
+
// drawing a state machine can tell an edge left unconstrained on purpose
|
|
422
|
+
// from one nothing is known about.
|
|
423
|
+
expect(edgeOf([], "Abandon"))->toEqual(Some((None, None, Some("unrestricted"))))
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
testSync("and the scenarios may not narrow it", () => {
|
|
427
|
+
// "Legal in every state" is a claim; a corpus only covers the states
|
|
428
|
+
// somebody wrote a scenario for. Deriving a from-set here would shrink the
|
|
429
|
+
// claim to an accident of coverage and withdraw the command from every row
|
|
430
|
+
// nobody tested — which for `Customer.SetLocation`'s trait reports is the
|
|
431
|
+
// difference between a late geocode landing and a row parked in Failed.
|
|
432
|
+
expect(edgeOf([edge(~command="Abandon", ~allowedStates=["Booked"])], "Abandon"))->toEqual(
|
|
433
|
+
Some((None, None, Some("unrestricted"))),
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
testSync("a spec that wrote no switch is silence, which they may answer", () => {
|
|
438
|
+
// `PsPlaceOrder` declares no `commandTransition`, so the ppx injects
|
|
439
|
+
// `Undeclared`. It erases to the same pair of `None`s as `Unrestricted` and
|
|
440
|
+
// is the reason the two are separate constructors at all.
|
|
441
|
+
let structure = Plugin_Structure.make(
|
|
442
|
+
~name="SilentPlugin",
|
|
443
|
+
~stateChangeSlices=[module(PsPlaceOrderSlice)],
|
|
444
|
+
~stateViewSlices=[],
|
|
445
|
+
~lifecycleModel=[
|
|
446
|
+
{
|
|
447
|
+
component: "PlaceOrder",
|
|
448
|
+
command: "PlaceOrder",
|
|
449
|
+
allowedStates: ["Draft"],
|
|
450
|
+
targets: ["Placed"],
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
)
|
|
454
|
+
let slice = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
455
|
+
expect(
|
|
456
|
+
slice.commands
|
|
457
|
+
->Array.find(c => c.name == "PlaceOrder")
|
|
458
|
+
->Option.map(c => (c.allowedStates, c.targetState, c.allowedStatesSource)),
|
|
459
|
+
)->toEqual(Some((Some(["Draft"]), Some("Placed"), Some("derived"))))
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
let levelOf = (model, name) => {
|
|
463
|
+
let slice = build(model).stateChangeSlices->Array.getUnsafe(0)
|
|
464
|
+
slice.commands->Array.find(c => c.name == name)->Option.map(c => c.level)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
testSync("the level comes from the corpus where it has one", () => {
|
|
468
|
+
// `Abandon` starts with none of the create-command stems, so the name-stem
|
|
469
|
+
// guess makes it Instance-level. The scenarios say otherwise, and they have
|
|
470
|
+
// seen the histories.
|
|
471
|
+
expect(levelOf([edge(~command="Abandon", ~level=Collection)], "Abandon"))->toEqual(
|
|
472
|
+
Some(Reventless.Plugin.Collection),
|
|
473
|
+
)
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
testSync("and from the name stem where it does not", () => {
|
|
477
|
+
expect(levelOf([edge(~command="Abandon")], "Abandon"))->toEqual(
|
|
478
|
+
Some(Reventless.Plugin.Instance),
|
|
479
|
+
)
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
347
483
|
describe("traitDeclarations — a graft leaves a trace", () => {
|
|
348
484
|
// The one signal a graft leaves that survives into a deployed plugin. Every
|
|
349
485
|
// other one — the package dependency, the variant spread, the rules alias, the
|
|
@@ -1859,15 +1995,43 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1859
1995
|
|
|
1860
1996
|
// Scalar and plural in one command: the plural forms are the regression, the
|
|
1861
1997
|
// scalar one is here so a fix that reaches them by breaking it cannot pass.
|
|
1998
|
+
// The nested one rides alongside, named by its path rather than by the field
|
|
1999
|
+
// that holds it.
|
|
1862
2000
|
testSync("a command collects its array refs alongside its scalar one", () => {
|
|
1863
2001
|
let cmd = slice.commands->Array.getUnsafe(0)
|
|
1864
2002
|
expect(cmd.references->referencesOf)->toEqual([
|
|
1865
2003
|
("customerId", "Customers"),
|
|
1866
2004
|
("productIds", "AvailableProducts"),
|
|
1867
2005
|
("warehouseIds", "Warehouses"),
|
|
2006
|
+
("lineItems[].productId", "AvailableProducts"),
|
|
1868
2007
|
])
|
|
1869
2008
|
})
|
|
1870
2009
|
|
|
2010
|
+
// A reference declared on a field of a record the command holds belongs to
|
|
2011
|
+
// *that* field, so it is named by its path: `[]` for "each element of", `.`
|
|
2012
|
+
// for "property of". Attributing it to `lineItems` would name a field that
|
|
2013
|
+
// declares nothing, and dropping it would leave the consuming side free to
|
|
2014
|
+
// guess an entity from the name.
|
|
2015
|
+
testSync("a reference on a nested record's field is named by its path", () => {
|
|
2016
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2017
|
+
expect(
|
|
2018
|
+
cmd.references->Array.find(({fieldName}) => fieldName == "lineItems[].productId"),
|
|
2019
|
+
)->toEqual(
|
|
2020
|
+
Some({
|
|
2021
|
+
Reventless.Plugin.fieldName: "lineItems[].productId",
|
|
2022
|
+
entity: "AvailableProducts",
|
|
2023
|
+
plugin: None,
|
|
2024
|
+
}),
|
|
2025
|
+
)
|
|
2026
|
+
})
|
|
2027
|
+
|
|
2028
|
+
// The other half of the path contract: a flat field keeps its bare name, so
|
|
2029
|
+
// nothing reading today's paths has to learn the new form to go on working.
|
|
2030
|
+
testSync("a flat reference in the same variant keeps its bare name", () => {
|
|
2031
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2032
|
+
expect(cmd.references->Array.some(({fieldName}) => fieldName == "customerId"))->toBe(true)
|
|
2033
|
+
})
|
|
2034
|
+
|
|
1871
2035
|
// Optionality is a statement about presence. An optional array puts the
|
|
1872
2036
|
// element two wrappers down, which is the shape most likely to be missed by
|
|
1873
2037
|
// an unwrap that only handles one.
|
|
@@ -1885,6 +2049,26 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1885
2049
|
expect(evt.references->referencesOf)->toEqual([("productIds", "AvailableProducts")])
|
|
1886
2050
|
})
|
|
1887
2051
|
|
|
2052
|
+
// A client assembling this mutation declares a variable per argument, and
|
|
2053
|
+
// for `lineItems` it needs the *element* input type by name. The rendered
|
|
2054
|
+
// reference `x-reventless-graphql-type` publishes is keyed by argument, so it
|
|
2055
|
+
// stops at the top level; this one travels with the object itself, which is
|
|
2056
|
+
// what a reader that has descended into the list has in hand.
|
|
2057
|
+
//
|
|
2058
|
+
// The name is composed from the mutation field by a server-side convention,
|
|
2059
|
+
// so nothing downstream of the JSON Schema could reconstruct it.
|
|
2060
|
+
testSync("a nested input object publishes the type name the SDL declares", () => {
|
|
2061
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2062
|
+
let itemName =
|
|
2063
|
+
cmd.schema
|
|
2064
|
+
->JSON.parseOrThrow
|
|
2065
|
+
->getPropertyOf("lineItems")
|
|
2066
|
+
->Option.flatMap(s => getProperty(s, "items"))
|
|
2067
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-graphql-input"))
|
|
2068
|
+
->Option.flatMap(JSON.Decode.string)
|
|
2069
|
+
expect(itemName)->toEqual(Some(cmd.mutationField ++ "LineItems"))
|
|
2070
|
+
})
|
|
2071
|
+
|
|
1888
2072
|
// The declared entity, not the one `productIds` reads like. Before the
|
|
1889
2073
|
// element unwrap this field resolved by heuristic to `Products`.
|
|
1890
2074
|
testSync("the declared entity wins over the one the field name suggests", () => {
|
|
@@ -519,7 +519,7 @@ let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, un
|
|
|
519
519
|
], [
|
|
520
520
|
PsPlaceOrderSlice,
|
|
521
521
|
PsShipOrderSlice
|
|
522
|
-
], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
522
|
+
], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
523
523
|
|
|
524
524
|
globalThis.describe("lifecycle topology", () => {
|
|
525
525
|
let ordersStates = Object.fromEntries([[
|
|
@@ -550,6 +550,7 @@ globalThis.describe("lifecycle topology", () => {
|
|
|
550
550
|
references: [],
|
|
551
551
|
allowedStates: allowedStates,
|
|
552
552
|
targetState: targetState,
|
|
553
|
+
allowedStatesSource: Stdlib_Option.map(allowedStates, param => "declared"),
|
|
553
554
|
apiExposed: undefined,
|
|
554
555
|
requiredAccess: undefined,
|
|
555
556
|
ownerField: undefined
|
|
@@ -582,7 +583,7 @@ globalThis.describe("lifecycle topology", () => {
|
|
|
582
583
|
});
|
|
583
584
|
|
|
584
585
|
globalThis.describe("commandTransition — the edge as a switch", () => {
|
|
585
|
-
let structure = Plugin_Structure$ReventlessCore.make("TransitionSwitchPlugin", undefined, undefined, [], [PsTransitionSwitchSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
586
|
+
let structure = Plugin_Structure$ReventlessCore.make("TransitionSwitchPlugin", undefined, undefined, [], [PsTransitionSwitchSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
586
587
|
let slice = structure.stateChangeSlices[0];
|
|
587
588
|
let byName = name => slice.commands.find(c => c.name === name);
|
|
588
589
|
globalThis.test("the switch fills allowedStates and targetState", () => {
|
|
@@ -614,11 +615,114 @@ globalThis.describe("commandTransition — the edge as a switch", () => {
|
|
|
614
615
|
});
|
|
615
616
|
});
|
|
616
617
|
|
|
618
|
+
globalThis.describe("the harvested model against the switch", () => {
|
|
619
|
+
let edge = (command, level, allowedStatesOpt, targetsOpt) => {
|
|
620
|
+
let allowedStates = allowedStatesOpt !== undefined ? allowedStatesOpt : [];
|
|
621
|
+
let targets = targetsOpt !== undefined ? targetsOpt : [];
|
|
622
|
+
return {
|
|
623
|
+
component: "TransitionSwitch",
|
|
624
|
+
command: command,
|
|
625
|
+
level: level,
|
|
626
|
+
allowedStates: allowedStates,
|
|
627
|
+
targets: targets
|
|
628
|
+
};
|
|
629
|
+
};
|
|
630
|
+
let build = lifecycleModel => Plugin_Structure$ReventlessCore.make("HarvestPlugin", undefined, undefined, [], [PsTransitionSwitchSlice], undefined, undefined, undefined, undefined, undefined, undefined, lifecycleModel);
|
|
631
|
+
let edgeOf = (model, name) => {
|
|
632
|
+
let slice = build(model).stateChangeSlices[0];
|
|
633
|
+
return Stdlib_Option.map(slice.commands.find(c => c.name === name), c => [
|
|
634
|
+
c.allowedStates,
|
|
635
|
+
c.targetState,
|
|
636
|
+
c.allowedStatesSource
|
|
637
|
+
]);
|
|
638
|
+
};
|
|
639
|
+
globalThis.test("an observed from-set replaces the declared one, and says where it came from", () => {
|
|
640
|
+
globalThis.expect(edgeOf([edge("Book", undefined, ["Booked"], ["Abandoned"])], "Book")).toEqual([
|
|
641
|
+
["Booked"],
|
|
642
|
+
"Abandoned",
|
|
643
|
+
"derived"
|
|
644
|
+
]);
|
|
645
|
+
});
|
|
646
|
+
globalThis.test("a corpus that observed nothing leaves the switch standing", () => {
|
|
647
|
+
globalThis.expect(edgeOf([edge("Book", undefined, undefined, undefined)], "Book")).toEqual([
|
|
648
|
+
["Draft"],
|
|
649
|
+
"Booked",
|
|
650
|
+
"declared"
|
|
651
|
+
]);
|
|
652
|
+
});
|
|
653
|
+
globalThis.test("the two halves resolve separately", () => {
|
|
654
|
+
globalThis.expect(edgeOf([edge("Book", undefined, ["Booked"], undefined)], "Book")).toEqual([
|
|
655
|
+
["Booked"],
|
|
656
|
+
"Booked",
|
|
657
|
+
"derived"
|
|
658
|
+
]);
|
|
659
|
+
});
|
|
660
|
+
globalThis.test("two observed targets leave the declaration to speak", () => {
|
|
661
|
+
globalThis.expect(edgeOf([edge("Book", undefined, ["Booked"], [
|
|
662
|
+
"Booked",
|
|
663
|
+
"Abandoned"
|
|
664
|
+
])], "Book")).toEqual([
|
|
665
|
+
["Booked"],
|
|
666
|
+
"Booked",
|
|
667
|
+
"derived"
|
|
668
|
+
]);
|
|
669
|
+
});
|
|
670
|
+
globalThis.test("a command the model does not mention keeps its declaration", () => {
|
|
671
|
+
globalThis.expect(edgeOf([edge("Book", undefined, ["Booked"], undefined)], "Rebook")).toEqual([
|
|
672
|
+
["Booked"],
|
|
673
|
+
undefined,
|
|
674
|
+
"declared"
|
|
675
|
+
]);
|
|
676
|
+
});
|
|
677
|
+
globalThis.test("an authored Unrestricted publishes no from-set, and says it meant to", () => {
|
|
678
|
+
globalThis.expect(edgeOf([], "Abandon")).toEqual([
|
|
679
|
+
undefined,
|
|
680
|
+
undefined,
|
|
681
|
+
"unrestricted"
|
|
682
|
+
]);
|
|
683
|
+
});
|
|
684
|
+
globalThis.test("and the scenarios may not narrow it", () => {
|
|
685
|
+
globalThis.expect(edgeOf([edge("Abandon", undefined, ["Booked"], undefined)], "Abandon")).toEqual([
|
|
686
|
+
undefined,
|
|
687
|
+
undefined,
|
|
688
|
+
"unrestricted"
|
|
689
|
+
]);
|
|
690
|
+
});
|
|
691
|
+
globalThis.test("a spec that wrote no switch is silence, which they may answer", () => {
|
|
692
|
+
let structure = Plugin_Structure$ReventlessCore.make("SilentPlugin", undefined, undefined, [], [PsPlaceOrderSlice], undefined, undefined, undefined, undefined, undefined, undefined, [{
|
|
693
|
+
component: "PlaceOrder",
|
|
694
|
+
command: "PlaceOrder",
|
|
695
|
+
allowedStates: ["Draft"],
|
|
696
|
+
targets: ["Placed"]
|
|
697
|
+
}]);
|
|
698
|
+
let slice = structure.stateChangeSlices[0];
|
|
699
|
+
globalThis.expect(Stdlib_Option.map(slice.commands.find(c => c.name === "PlaceOrder"), c => [
|
|
700
|
+
c.allowedStates,
|
|
701
|
+
c.targetState,
|
|
702
|
+
c.allowedStatesSource
|
|
703
|
+
])).toEqual([
|
|
704
|
+
["Draft"],
|
|
705
|
+
"Placed",
|
|
706
|
+
"derived"
|
|
707
|
+
]);
|
|
708
|
+
});
|
|
709
|
+
let levelOf = (model, name) => {
|
|
710
|
+
let slice = build(model).stateChangeSlices[0];
|
|
711
|
+
return Stdlib_Option.map(slice.commands.find(c => c.name === name), c => c.level);
|
|
712
|
+
};
|
|
713
|
+
globalThis.test("the level comes from the corpus where it has one", () => {
|
|
714
|
+
globalThis.expect(levelOf([edge("Abandon", "Collection", undefined, undefined)], "Abandon")).toEqual("Collection");
|
|
715
|
+
});
|
|
716
|
+
globalThis.test("and from the name stem where it does not", () => {
|
|
717
|
+
globalThis.expect(levelOf([edge("Abandon", undefined, undefined, undefined)], "Abandon")).toEqual("Instance");
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
617
721
|
globalThis.describe("traitDeclarations — a graft leaves a trace", () => {
|
|
618
722
|
let structure = Plugin_Structure$ReventlessCore.make("TraitPlugin", undefined, undefined, [], [
|
|
619
723
|
PsTransitionSwitchSlice,
|
|
620
724
|
PsShipOrderSlice
|
|
621
|
-
], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
725
|
+
], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
622
726
|
globalThis.test("a declaring component's trait reaches the structure", () => {
|
|
623
727
|
globalThis.expect(Stdlib_Option.getOr(structure.traitDeclarations, []).map(d => [
|
|
624
728
|
d.trait,
|
|
@@ -647,7 +751,7 @@ globalThis.describe("traitDeclarations — a graft leaves a trace", () => {
|
|
|
647
751
|
globalThis.describe("declared-edge cross-check", () => {
|
|
648
752
|
let buildBad = () => {
|
|
649
753
|
try {
|
|
650
|
-
Plugin_Structure$ReventlessCore.make("TransitionPlugin", undefined, undefined, [PsShipmentsViewSlice], [PsDispatchShipmentSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
754
|
+
Plugin_Structure$ReventlessCore.make("TransitionPlugin", undefined, undefined, [PsShipmentsViewSlice], [PsDispatchShipmentSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
651
755
|
return;
|
|
652
756
|
} catch (raw_e) {
|
|
653
757
|
let e = Primitive_exceptions.internalToException(raw_e);
|
|
@@ -1094,7 +1198,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1094
1198
|
});
|
|
1095
1199
|
});
|
|
1096
1200
|
globalThis.describe("singleQueryField — the singular the schema actually serves", () => {
|
|
1097
|
-
let withIrregularPlural = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [PsCategoriesViewSlice], [], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1201
|
+
let withIrregularPlural = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [PsCategoriesViewSlice], [], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1098
1202
|
let categories = withIrregularPlural.stateViewSlices[0];
|
|
1099
1203
|
globalThis.test("an `-ies` plural publishes the `y` singular, not a truncation", () => {
|
|
1100
1204
|
globalThis.expect([
|
|
@@ -1115,7 +1219,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1115
1219
|
q.idField,
|
|
1116
1220
|
q.idFieldSource
|
|
1117
1221
|
];
|
|
1118
|
-
let withCategories = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [PsCategoriesViewSlice], [], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1222
|
+
let withCategories = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [PsCategoriesViewSlice], [], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1119
1223
|
globalThis.test("a name-matching `*Id` field is a guess the consumer can also make", () => {
|
|
1120
1224
|
globalThis.expect(keyOf(structure.stateViewSlices[0])).toEqual([
|
|
1121
1225
|
"orderId",
|
|
@@ -1499,7 +1603,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1499
1603
|
"Orders",
|
|
1500
1604
|
"Order"
|
|
1501
1605
|
]
|
|
1502
|
-
]));
|
|
1606
|
+
]), undefined);
|
|
1503
1607
|
globalThis.test("a chaptered state-change slice carries Some(chapter)", () => {
|
|
1504
1608
|
let placeOrder = chaptered.stateChangeSlices[0];
|
|
1505
1609
|
globalThis.expect(placeOrder.chapter).toEqual("Order");
|
|
@@ -1518,7 +1622,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1518
1622
|
});
|
|
1519
1623
|
});
|
|
1520
1624
|
globalThis.describe("requiredStores", () => {
|
|
1521
|
-
let withStores = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsAttachInvoiceSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1625
|
+
let withStores = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsAttachInvoiceSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1522
1626
|
let stores = Stdlib_Option.getOr(withStores.requiredStores, []);
|
|
1523
1627
|
globalThis.test("an unqualified store resolves against the declaring plugin", () => {
|
|
1524
1628
|
globalThis.expect(stores.includes("TestPlugin.documents")).toBe(true);
|
|
@@ -1674,7 +1778,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1674
1778
|
});
|
|
1675
1779
|
});
|
|
1676
1780
|
globalThis.describe("uploadable types derive their store from the field name", () => {
|
|
1677
|
-
let withUploads = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadImagesSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1781
|
+
let withUploads = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadImagesSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1678
1782
|
let stores = Stdlib_Option.getOr(withUploads.requiredStores, []);
|
|
1679
1783
|
globalThis.test("the derived store is the field name, pluralised", () => {
|
|
1680
1784
|
globalThis.expect(stores.includes("TestPlugin.productImages")).toBe(true);
|
|
@@ -1735,7 +1839,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1735
1839
|
globalThis.describe("near-duplicate stores are refused", () => {
|
|
1736
1840
|
let failureText = () => {
|
|
1737
1841
|
try {
|
|
1738
|
-
Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsTypoStoreSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1842
|
+
Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsTypoStoreSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1739
1843
|
return "";
|
|
1740
1844
|
} catch (raw_e) {
|
|
1741
1845
|
let e = Primitive_exceptions.internalToException(raw_e);
|
|
@@ -1764,7 +1868,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1764
1868
|
]);
|
|
1765
1869
|
});
|
|
1766
1870
|
globalThis.test("the four stores of the uploadable fixture do not collide", () => {
|
|
1767
|
-
globalThis.expect(Capability_Inference$ReventlessCore.collisions(Stdlib_Option.getOr(Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadImagesSlice], undefined, undefined, undefined, undefined, undefined, undefined).requiredStoreDeclarations, []))).toEqual([]);
|
|
1871
|
+
globalThis.expect(Capability_Inference$ReventlessCore.collisions(Stdlib_Option.getOr(Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadImagesSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined).requiredStoreDeclarations, []))).toEqual([]);
|
|
1768
1872
|
});
|
|
1769
1873
|
globalThis.test("one edit, transpositions included; two edits not", () => {
|
|
1770
1874
|
globalThis.expect([
|
|
@@ -1801,7 +1905,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1801
1905
|
});
|
|
1802
1906
|
});
|
|
1803
1907
|
globalThis.describe("optional carriers keep their declarations", () => {
|
|
1804
|
-
let withOptional = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadAvatarSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1908
|
+
let withOptional = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadAvatarSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1805
1909
|
globalThis.test("an optional storage ref still requires its store", () => {
|
|
1806
1910
|
globalThis.expect(withOptional.requiredStores).toEqual(["TestPlugin.avatars"]);
|
|
1807
1911
|
});
|
|
@@ -1852,7 +1956,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1852
1956
|
globalThis.expect(eventWarnings).toEqual(expected);
|
|
1853
1957
|
});
|
|
1854
1958
|
globalThis.test("a heuristic-only match reaches no manifest entry", () => {
|
|
1855
|
-
let withHeuristicField = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsChangePhotoSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1959
|
+
let withHeuristicField = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsChangePhotoSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1856
1960
|
globalThis.expect(withHeuristicField.requiredStores).toEqual(["TestPlugin.productPhotos"]);
|
|
1857
1961
|
let manifest = CapabilityManifest$Reventless.fromStructure(withHeuristicField);
|
|
1858
1962
|
globalThis.expect(manifest.capabilities.every(entry => entry.declaredBy.every(site => Primitive_object.notequal(site.field, "photoUrl")))).toBe(true);
|
|
@@ -1879,7 +1983,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1879
1983
|
});
|
|
1880
1984
|
});
|
|
1881
1985
|
globalThis.describe("plural references reach the manifest", () => {
|
|
1882
|
-
let withPluralRefs = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsReserveStockSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1986
|
+
let withPluralRefs = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsReserveStockSlice], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
1883
1987
|
let slice = withPluralRefs.stateChangeSlices[0];
|
|
1884
1988
|
let referencesOf = refs => refs.map(param => [
|
|
1885
1989
|
param.fieldName,
|
|
@@ -1899,9 +2003,25 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1899
2003
|
[
|
|
1900
2004
|
"warehouseIds",
|
|
1901
2005
|
"Warehouses"
|
|
2006
|
+
],
|
|
2007
|
+
[
|
|
2008
|
+
"lineItems[].productId",
|
|
2009
|
+
"AvailableProducts"
|
|
1902
2010
|
]
|
|
1903
2011
|
]);
|
|
1904
2012
|
});
|
|
2013
|
+
globalThis.test("a reference on a nested record's field is named by its path", () => {
|
|
2014
|
+
let cmd = slice.commands[0];
|
|
2015
|
+
globalThis.expect(cmd.references.find(param => param.fieldName === "lineItems[].productId")).toEqual({
|
|
2016
|
+
fieldName: "lineItems[].productId",
|
|
2017
|
+
entity: "AvailableProducts",
|
|
2018
|
+
plugin: undefined
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
globalThis.test("a flat reference in the same variant keeps its bare name", () => {
|
|
2022
|
+
let cmd = slice.commands[0];
|
|
2023
|
+
globalThis.expect(cmd.references.some(param => param.fieldName === "customerId")).toBe(true);
|
|
2024
|
+
});
|
|
1905
2025
|
globalThis.test("an optional array ref is collected under the field's own name", () => {
|
|
1906
2026
|
let cmd = slice.commands[0];
|
|
1907
2027
|
globalThis.expect(cmd.references.find(param => param.fieldName === "warehouseIds")).toEqual({
|
|
@@ -1917,6 +2037,11 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1917
2037
|
"AvailableProducts"
|
|
1918
2038
|
]]);
|
|
1919
2039
|
});
|
|
2040
|
+
globalThis.test("a nested input object publishes the type name the SDL declares", () => {
|
|
2041
|
+
let cmd = slice.commands[0];
|
|
2042
|
+
let itemName = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(getPropertyOf(JSON.parse(cmd.schema), "lineItems"), s => getProperty(s, "items")), s => getProperty(s, "x-reventless-graphql-input")), Stdlib_JSON.Decode.string);
|
|
2043
|
+
globalThis.expect(itemName).toEqual(cmd.mutationField + "LineItems");
|
|
2044
|
+
});
|
|
1920
2045
|
globalThis.test("the declared entity wins over the one the field name suggests", () => {
|
|
1921
2046
|
let cmd = slice.commands[0];
|
|
1922
2047
|
globalThis.expect(Stdlib_Option.map(cmd.references.find(param => param.fieldName === "productIds"), param => param.entity)).toEqual("AvailableProducts");
|
|
@@ -2058,7 +2183,7 @@ globalThis.describe("duplicate DCB command names", () => {
|
|
|
2058
2183
|
Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [
|
|
2059
2184
|
PsShipOrderSlice,
|
|
2060
2185
|
PsShipOrderSlice
|
|
2061
|
-
], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2186
|
+
], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2062
2187
|
tmp = false;
|
|
2063
2188
|
} catch (exn) {
|
|
2064
2189
|
tmp = true;
|
|
@@ -2071,7 +2196,7 @@ globalThis.describe("duplicate DCB command names", () => {
|
|
|
2071
2196
|
val = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [
|
|
2072
2197
|
PsShipOrderSlice,
|
|
2073
2198
|
PsShipOrderSlice
|
|
2074
|
-
], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2199
|
+
], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2075
2200
|
} catch (raw_e) {
|
|
2076
2201
|
let e = Primitive_exceptions.internalToException(raw_e);
|
|
2077
2202
|
if (e.RE_EXN_ID === "JsExn") {
|
|
@@ -2095,7 +2220,7 @@ globalThis.describe("duplicate DCB command names", () => {
|
|
|
2095
2220
|
Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [
|
|
2096
2221
|
PsPlaceOrderSlice,
|
|
2097
2222
|
PsShipOrderSlice
|
|
2098
|
-
], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2223
|
+
], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2099
2224
|
tmp = true;
|
|
2100
2225
|
} catch (exn) {
|
|
2101
2226
|
tmp = false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Test fixture spec where every reference carrier is
|
|
1
|
+
// Test fixture spec where every reference carrier is one the naive walk misses:
|
|
2
|
+
// PLURAL, or declared on a field of a record the command holds.
|
|
2
3
|
//
|
|
3
4
|
// `Reference.to_` returns an *element* schema, so the ppx annotates the `string`
|
|
4
5
|
// inside `array<string>` and the field's own schema carries no marker. A walk
|
|
@@ -11,6 +12,11 @@
|
|
|
11
12
|
// shows up as one of the two disappearing rather than as an empty list, and
|
|
12
13
|
// `warehouseIds?` covers the plural-inside-optional shape, where the array sits
|
|
13
14
|
// one wrapper further down still.
|
|
15
|
+
//
|
|
16
|
+
// `lineItems` covers the other shape the walk has to descend into: a reference
|
|
17
|
+
// declared on a field of a record the command holds, which belongs to that field
|
|
18
|
+
// and is named by its path. A named record type, not an inline one — the ppx
|
|
19
|
+
// walks a type declaration, which is what puts the marker on `productId` at all.
|
|
14
20
|
|
|
15
21
|
@@reventless.spec("ReserveStock")
|
|
16
22
|
|
|
@@ -22,6 +28,12 @@ type consumedEvent = StockReserved({reservationId: string})
|
|
|
22
28
|
|
|
23
29
|
let evolve = (_state, _event) => true
|
|
24
30
|
|
|
31
|
+
@schema
|
|
32
|
+
type lineItem = {
|
|
33
|
+
@ref("AvailableProducts") productId: string,
|
|
34
|
+
quantity: int,
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
@schema
|
|
26
38
|
type command =
|
|
27
39
|
| ReserveStock({
|
|
@@ -29,6 +41,7 @@ type command =
|
|
|
29
41
|
@ref("Customers") customerId: string,
|
|
30
42
|
@ref("AvailableProducts") productIds: array<string>,
|
|
31
43
|
@ref("Warehouses") warehouseIds?: array<string>,
|
|
44
|
+
lineItems: array<lineItem>,
|
|
32
45
|
})
|
|
33
46
|
|
|
34
47
|
@schema
|
|
@@ -13,12 +13,18 @@ function evolve(_state, _event) {
|
|
|
13
13
|
return true;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
let lineItemSchema = Sury.$schema(s => ({
|
|
17
|
+
productId: s.m(Reference$Reventless.to_(undefined, undefined, "AvailableProducts")),
|
|
18
|
+
quantity: s.m(Sury.int)
|
|
19
|
+
}));
|
|
20
|
+
|
|
16
21
|
let commandSchema = Sury.$schema(s => ({
|
|
17
22
|
TAG: "ReserveStock",
|
|
18
23
|
reservationId: s.m(DcbTag$Reventless.partition),
|
|
19
24
|
customerId: s.m(Reference$Reventless.to_(undefined, undefined, "Customers")),
|
|
20
25
|
productIds: s.m(Sury.array(Reference$Reventless.to_(undefined, "productId", "AvailableProducts"))),
|
|
21
|
-
warehouseIds: s.m(Sury.$option(Sury.array(Reference$Reventless.to_(undefined, "warehouseId", "Warehouses"))))
|
|
26
|
+
warehouseIds: s.m(Sury.$option(Sury.array(Reference$Reventless.to_(undefined, "warehouseId", "Warehouses")))),
|
|
27
|
+
lineItems: s.m(Sury.array(lineItemSchema))
|
|
22
28
|
}));
|
|
23
29
|
|
|
24
30
|
let errorSchema = Sury.literal("OutOfStock");
|
|
@@ -41,7 +47,7 @@ function commandAuthorization(param) {
|
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
function commandTransition(param) {
|
|
44
|
-
return "
|
|
50
|
+
return "Undeclared";
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
let traits = [];
|
|
@@ -62,6 +68,7 @@ export {
|
|
|
62
68
|
initialState,
|
|
63
69
|
consumedEventSchema,
|
|
64
70
|
evolve,
|
|
71
|
+
lineItemSchema,
|
|
65
72
|
commandSchema,
|
|
66
73
|
errorSchema,
|
|
67
74
|
eventSchema,
|