@reventlessdev/reventless-core 3.0.0-alpha.256 → 3.0.0-alpha.258

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +8 -8
  3. package/src/adapter/Monitoring/Monitoring.res +79 -5
  4. package/src/adapter/Monitoring/Monitoring.res.mjs +42 -2
  5. package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
  6. package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
  7. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  8. package/src/admin/Platform_UiSlots.res +21 -0
  9. package/src/admin/Platform_UiSlots.res.mjs +15 -0
  10. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +1 -1
  11. package/src/components/Aggregate/Aggregate_Callback.res +13 -4
  12. package/src/components/Aggregate/Aggregate_Callback.res.mjs +6 -5
  13. package/src/components/Api/GraphQL_FragmentGenerator.res +2 -0
  14. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +4 -0
  15. package/src/components/Api/MCP_SchemaGenerator.res.mjs +2 -2
  16. package/src/components/Api/SchemaType.res +8 -0
  17. package/src/components/Api/SchemaType.res.mjs +15 -5
  18. package/src/components/Api/SuryToJsonSchema.res +70 -12
  19. package/src/components/Api/SuryToJsonSchema.res.mjs +43 -16
  20. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +6 -1
  21. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
  22. package/src/components/StateViewSlice/StateViewSlice_Builder.res +7 -2
  23. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +4 -1
  24. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  25. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +1 -1
  26. package/src/plugin/component/Plugin.res +1 -0
  27. package/src/plugin/component/Plugin_Builder.res +2 -0
  28. package/src/plugin/component/Plugin_Builder.res.mjs +3 -2
  29. package/src/plugin/component/Plugin_Structure.res +105 -10
  30. package/src/plugin/component/Plugin_Structure.res.mjs +71 -27
  31. package/src/util/Validation.res +1 -1
  32. package/src/util/Validation.res.mjs +2 -2
  33. package/tests/adapter/MonitoringTest.res +46 -7
  34. package/tests/adapter/MonitoringTest.res.mjs +47 -5
  35. package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
  36. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -0
  37. package/tests/admin/Platform_Admin_StructureTest.res.mjs +2 -14
  38. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +1 -0
  39. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +2 -0
  40. package/tests/aggregate/AggregateCacheTest.res.mjs +1 -1
  41. package/tests/aggregate/AggregateConflictTest.res.mjs +1 -1
  42. package/tests/aggregate/AggregateFixtures.res.mjs +1 -1
  43. package/tests/aggregate/AggregateSnapshotTest.res.mjs +1 -1
  44. package/tests/api/GraphQL_FragmentGeneratorTest.res +6 -4
  45. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +5 -5
  46. package/tests/api/SuryToJsonSchemaTest.res.mjs +57 -57
  47. package/tests/api/TaggedUnionSchemaTest.res.mjs +1 -1
  48. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +1 -1
  49. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +1 -1
  50. package/tests/dcb/DcbFixtures.res.mjs +1 -1
  51. package/tests/eventmapper/EventMapperFixtures.res.mjs +1 -1
  52. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +1 -1
  53. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +1 -1
  54. package/tests/plugin/ManifestVisibilityTest.res.mjs +1 -1
  55. package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
  56. package/tests/plugin/PluginStructureTest.res +184 -0
  57. package/tests/plugin/PluginStructureTest.res.mjs +142 -17
  58. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +1 -1
  59. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +1 -1
  60. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +1 -1
  61. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +1 -1
  62. package/tests/plugin/StateChangeSlice/PsReserveStock.res +14 -1
  63. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +9 -2
  64. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +1 -1
  65. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +1 -1
  66. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +1 -1
  67. package/tests/util/CommandPublisherTest.res.mjs +1 -1
@@ -4,6 +4,10 @@
4
4
 
5
5
  let log = Logger.fromEnv()
6
6
 
7
+ // Set by `check:lifecycle`, and by nothing else. See the note where it is read.
8
+ @val external _declaredTransitionsOnly: option<string> = "process.env.REVENTLESS_DECLARED_TRANSITIONS_ONLY"
9
+ let declaredTransitionsOnly = _declaredTransitionsOnly->Option.isSome
10
+
7
11
  // Whether a field can name a record, stated over `SchemaType`'s IR so every
8
12
  // shape it grows is excluded before it exists. `Nullable` unwraps; a `Semantic`
9
13
  // wrapper is refused — a bucket key or URL is not prose.
@@ -413,6 +417,15 @@ let checkDeclaredTransitions = (
413
417
  if Array.length(known) == 0 {
414
418
  unvalidated := unvalidated.contents + 1
415
419
  } else {
420
+ // Said, because the two are fixed differently: an authored edge is
421
+ // edited, a harvested one is re-derived. A harvested state the view no
422
+ // longer declares means the committed model went stale, and stopping is
423
+ // right — published, it would offer the command on no row at all.
424
+ let hint = switch cmd.allowedStatesSource {
425
+ | Some("derived") =>
426
+ " This edge came from the component's own scenarios, so the committed lifecycle model is stale."
427
+ | _ => ""
428
+ }
416
429
  declared
417
430
  ->Array.filter(state => !(known->Array.includes(state)))
418
431
  ->Array.forEach(state =>
@@ -421,7 +434,7 @@ let checkDeclaredTransitions = (
421
434
  `${w.name}.${cmd.name} declares state "${state}", which none of its ` ++
422
435
  `linked views declare — ${w.linkedViews->Array.join(
423
436
  ", ",
424
- )} know ${known->Array.join(", ")}.`,
437
+ )} know ${known->Array.join(", ")}.${hint}`,
425
438
  )
426
439
  ->ignore
427
440
  )
@@ -443,7 +456,7 @@ let checkDeclaredTransitions = (
443
456
 
444
457
  if Array.length(failures) > 0 {
445
458
  JsError.throwWithMessage(
446
- `${pluginName}: a declared transition names states that do not exist.\n` ++
459
+ `${pluginName}: a transition names states that do not exist.\n` ++
447
460
  failures->Array.join("\n"),
448
461
  )
449
462
  }
@@ -580,10 +593,18 @@ let extractReferences = (properties: dict<S.t<unknown>>): array<
580
593
  > =>
581
594
  properties
582
595
  ->Dict.toArray
583
- ->Array.filterMap(((fieldName, fieldSchema)) =>
584
- Reventless.Reference.getFieldTarget(fieldSchema)->Option.map(target => (
596
+ ->Array.flatMap(((fieldName, fieldSchema)) =>
597
+ // `collectFieldTargets`, not `getFieldTarget`: a reference declared on a field
598
+ // of a record the field holds (an order line's `productId`) is one this
599
+ // command declares, and it arrives named by its own path. Commands and events
600
+ // both come through here, which is what stops one of them learning about
601
+ // nesting and the other not.
602
+ Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)->Array.map(((
603
+ path,
604
+ target,
605
+ )) => (
585
606
  {
586
- Reventless.Plugin.fieldName,
607
+ Reventless.Plugin.fieldName: path,
587
608
  entity: target.entity,
588
609
  plugin: target.plugin,
589
610
  }: Reventless.Plugin.fieldReference
@@ -637,6 +658,10 @@ let extractErrorDefs = (errorSchema: S.t<unknown>): array<Reventless.Plugin.erro
637
658
 
638
659
  // Aggregate commands that initialize a new aggregate instance are Collection-level
639
660
  // (shown as table-top buttons); all others are Instance-level (shown per-row).
661
+ //
662
+ // A guess from the command's name stem, and it misreads `Enroll`, `Provision`,
663
+ // `Onboard`. It answers only where the harvested model does not: a plugin with no
664
+ // corpus, or one whose linked views declare no lifecycle to label a history with.
640
665
  let isCreateCommandName = name =>
641
666
  ["Add", "Create", "Register", "Open", "Initialize", "Submit", "Start", "Place"]->Array.some(p =>
642
667
  name->String.startsWith(p)
@@ -724,13 +749,17 @@ let toCommandDef = (
724
749
  // (see the call sites below), so `Transition` itself asserts nothing about
725
750
  // representation and stays parameterised all the way down.
726
751
  ~commandTransition: unknown => Reventless.Transition.t<string>,
752
+ // What this component's own scenarios say about each command, harvested at
753
+ // build time and committed beside the plugin. `None` for a command no corpus
754
+ // covers, which is most of them in a plugin that ships no tests.
755
+ ~derivedEdgeFor: string => option<Reventless.Plugin.derivedEdge>,
727
756
  v: S.t<unknown>,
728
757
  ): option<Reventless.Plugin.commandDef> => {
729
758
  // Build a commandDef for one variant. `properties` is the variant's field dict —
730
759
  // empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
731
760
  // `S.literal("Archive")` string rather than an `{TAG, ...}` object.
732
761
  let mkDef = (~variantName, ~properties) => {
733
- let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
762
+ let (guessedLevel, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
734
763
  let references = extractReferences(properties)
735
764
  // Evaluated against a synthetic value per constructor, the same shape the
736
765
  // resolver builds at call time: a payload-bearing variant compiles to
@@ -742,12 +771,57 @@ let toCommandDef = (
742
771
  // The spec's own switch, which is exhaustive — so it also speaks for a
743
772
  // constructor the host did not declare but spliced from a trait.
744
773
  //
745
- // An edge is ONE declaration, so the two fields are read off it together.
746
774
  // `targetState: None` ⇒ AutoUI's board resolver falls back to its name-stem
747
775
  // heuristic.
748
776
  let declared = commandTransition(syntheticCommand)
749
- let allowedStates = Reventless.Transition.allowedStates(declared)
750
- let targetState = Reventless.Transition.targetState(declared)
777
+ let derived = derivedEdgeFor(variantName)
778
+
779
+ // Where the scenarios answer, they answer; where they are silent, the
780
+ // annotation stands. The halves are resolved separately because they are
781
+ // silent separately — a corpus routinely shows a command taking effect
782
+ // without ever showing where it lands — and because an empty derivation is
783
+ // not a derivation: `Some([])` matches no row's lifecycle tag, so publishing
784
+ // one offers the command on no row at all while the annotation that would
785
+ // have been right sits unread beside it.
786
+ //
787
+ // The one claim the scenarios may not answer for is `Unrestricted`: it says
788
+ // the command is legal in EVERY state, and a corpus only covers the states
789
+ // somebody wrote a scenario for, so deriving a from-set there would shrink a
790
+ // deliberate claim to an accident of coverage and withdraw the command from
791
+ // the rows nobody tested. `Undeclared` — the ppx's injected default — erases
792
+ // to the same pair of `None`s and is silence, which the scenarios may answer.
793
+ let unrestricted = Reventless.Transition.isUnrestricted(declared)
794
+ let (allowedStates, allowedStatesSource) = switch (
795
+ unrestricted
796
+ ? None
797
+ : derived->Option.flatMap(d =>
798
+ Array.length(d.allowedStates) > 0 ? Some(d.allowedStates) : None
799
+ ),
800
+ Reventless.Transition.allowedStates(declared),
801
+ ) {
802
+ | (Some(observed), _) => (Some(observed), Some("derived"))
803
+ | (None, Some(states)) => (Some(states), Some("declared"))
804
+ | (None, None) => (None, unrestricted ? Some("unrestricted") : None)
805
+ }
806
+ // The target is not protected the same way, because the two halves fail
807
+ // differently: a from-set RESTRICTS, and narrowing one wrongly takes a
808
+ // command away from rows it belongs on, while a target only tells a diagram
809
+ // where an edge lands. Supplying one the author left unstated adds
810
+ // information and withdraws nothing — which is what `Customer.Register`'s
811
+ // "the row's status is the view's to derive" asks for.
812
+ let targetState = switch derived->Option.flatMap(d =>
813
+ // Two observed targets are an edge `targetState` cannot express. Reported
814
+ // as a contradiction by the harvest; here the declaration is left to speak
815
+ // rather than one of the two picked.
816
+ switch d.targets {
817
+ | [only] => Some(only)
818
+ | _ => None
819
+ }
820
+ ) {
821
+ | Some(observed) => Some(observed)
822
+ | None => Reventless.Transition.targetState(declared)
823
+ }
824
+ let level = derived->Option.flatMap(d => d.level)->Option.getOr(guessedLevel)
751
825
  // API-exposed iff the whole command isn't @noApi and this variant
752
826
  // isn't in its @noApi-variants set — mirrors the API-generation filter
753
827
  // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
@@ -763,7 +837,12 @@ let toCommandDef = (
763
837
  // argument type names are composed *from* that field name, so there is
764
838
  // nothing to publish for it either.
765
839
  let mutationField = apiExposed ? mutationFieldFor(variantName) : ""
766
- let jsonSchema = v->SuryToJsonSchema.deriveObjectSchema
840
+ // Rooted at the mutation field, and asking for the input type names, so an
841
+ // object nested inside an argument (an order line inside `lineItems`) carries
842
+ // the name the SDL declares it under — which `x-reventless-graphql-type`, keyed
843
+ // by top-level argument, has no place to put.
844
+ let jsonSchema =
845
+ v->SuryToJsonSchema.deriveObjectSchema(~inputNames=apiExposed, ~typeName=mutationField)
767
846
  let annotatedSchema = if apiExposed {
768
847
  GraphQL_FragmentGenerator.mutationArgTypes(~fieldName=mutationField, v)->Option.mapOr(
769
848
  jsonSchema,
@@ -785,6 +864,7 @@ let toCommandDef = (
785
864
  references,
786
865
  allowedStates,
787
866
  targetState,
867
+ allowedStatesSource: ?allowedStatesSource,
788
868
  apiExposed: Some(apiExposed),
789
869
  requiredAccess,
790
870
  // Resolved from this constructor's own properties, not the union's: two
@@ -817,6 +897,7 @@ let extractCommandDefs = (
817
897
  ~mutationFieldFor: string => string,
818
898
  ~commandAuthorization: unknown => Reventless.Authorization.permission,
819
899
  ~commandTransition: unknown => Reventless.Transition.t<string>,
900
+ ~derivedEdgeFor: string => option<Reventless.Plugin.derivedEdge>=_ => None,
820
901
  commandSchema: S.t<unknown>,
821
902
  ): array<Reventless.Plugin.commandDef> =>
822
903
  switch commandSchema {
@@ -828,6 +909,7 @@ let extractCommandDefs = (
828
909
  ~parentSchema=commandSchema,
829
910
  ~commandAuthorization,
830
911
  ~commandTransition,
912
+ ~derivedEdgeFor,
831
913
  v,
832
914
  )
833
915
  )
@@ -839,6 +921,7 @@ let extractCommandDefs = (
839
921
  ~parentSchema=commandSchema,
840
922
  ~commandAuthorization,
841
923
  ~commandTransition,
924
+ ~derivedEdgeFor,
842
925
  commandSchema,
843
926
  )->Option.mapOr([], def => [def])
844
927
  }
@@ -911,8 +994,18 @@ let make = (
911
994
  // Component name → chapter, captured from each component's source folder by the
912
995
  // plugin generator. Keyed by `Spec.name`; no entry renders flat.
913
996
  ~componentChapters: dict<string>=Dict.make(),
997
+ // What the plugin's own scenarios say about each command's lifecycle edge,
998
+ // harvested by `check:lifecycle` and committed as `src/LifecycleModel.res`.
999
+ ~lifecycleModel: array<Reventless.Plugin.derivedEdge>=[],
914
1000
  ): Reventless.Plugin.pluginStructure => {
915
1001
  let chapterOf = (compName: string): option<string> => componentChapters->Dict.get(compName)
1002
+ // The harvest compares its own reading of the corpus with the DECLARATION, so
1003
+ // it needs a structure the model has not already been folded into. Reading a
1004
+ // derived value back as though it were the claim would confirm every edge and
1005
+ // hide exactly the disagreements the check exists to find.
1006
+ let model = declaredTransitionsOnly ? [] : lifecycleModel
1007
+ let derivedEdgeFor = (~component: string, command: string) =>
1008
+ model->Array.find(e => e.component == component && e.command == command)
916
1009
  // Payload-less variants dropped: the graph must not claim an edge a DCB lookup
917
1010
  // cannot WHERE-clause on.
918
1011
  let eventVariantNames = schema => Reventless.DcbTag.extractVariantNames(schema)
@@ -1391,6 +1484,7 @@ let make = (
1391
1484
  ),
1392
1485
  ~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
1393
1486
  ~commandTransition=SCS.Spec.commandTransition->Obj.magic,
1487
+ ~derivedEdgeFor=derivedEdgeFor(~component=SCS.Spec.name, ...),
1394
1488
  SCS.Spec.commandSchema->S.castToUnknown,
1395
1489
  ),
1396
1490
  producedEventTypes: produced,
@@ -1416,6 +1510,7 @@ let make = (
1416
1510
  ~mutationFieldFor=variantName => Api_Naming.aggregateMutationField(~plugin=name, ~aggregate=A.Spec.name, ~command=variantName),
1417
1511
  ~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
1418
1512
  ~commandTransition=A.Spec.commandTransition->Obj.magic,
1513
+ ~derivedEdgeFor=derivedEdgeFor(~component=A.Spec.name, ...),
1419
1514
  A.Spec.commandSchema->S.castToUnknown,
1420
1515
  ),
1421
1516
  producedEventTypes: produced,
@@ -29,6 +29,8 @@ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/
29
29
 
30
30
  let log = Logger$ReventlessCore.fromEnv();
31
31
 
32
+ let declaredTransitionsOnly = Stdlib_Option.isSome(process.env.REVENTLESS_DECLARED_TRANSITIONS_ONLY);
33
+
32
34
  function isLabelShape(_t) {
33
35
  while (true) {
34
36
  let t = _t;
@@ -342,18 +344,20 @@ function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView)
342
344
  });
343
345
  if (known.length === 0) {
344
346
  unvalidated.contents = unvalidated.contents + 1 | 0;
345
- } else {
346
- declared.filter(state => !known.includes(state)).forEach(state => {
347
- failures.push(w.name + `.` + cmd.name + ` declares state "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
348
- });
347
+ return;
349
348
  }
349
+ let match = cmd.allowedStatesSource;
350
+ let hint = match === "derived" ? " This edge came from the component's own scenarios, so the committed lifecycle model is stale." : "";
351
+ declared.filter(state => !known.includes(state)).forEach(state => {
352
+ failures.push(w.name + `.` + cmd.name + ` declares state "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.` + hint));
353
+ });
350
354
  });
351
355
  });
352
356
  if (unvalidated.contents > 0) {
353
357
  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.`);
354
358
  }
355
359
  if (failures.length !== 0) {
356
- return Stdlib_JsError.throwWithMessage(pluginName + `: a declared transition names states that do not exist.\n` + failures.join("\n"));
360
+ return Stdlib_JsError.throwWithMessage(pluginName + `: a transition names states that do not exist.\n` + failures.join("\n"));
357
361
  }
358
362
  }
359
363
 
@@ -449,14 +453,14 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
449
453
  }
450
454
 
451
455
  function extractReferences(properties) {
452
- return Stdlib_Array.filterMap(Object.entries(properties), param => {
453
- let fieldName = param[0];
454
- return Stdlib_Option.map(Reference$Reventless.getFieldTarget(param[1]), target => ({
455
- fieldName: fieldName,
456
+ return Object.entries(properties).flatMap(param => Reference$Reventless.collectFieldTargets(param[0], param[1]).map(param => {
457
+ let target = param[1];
458
+ return {
459
+ fieldName: param[0],
456
460
  entity: target.entity,
457
461
  plugin: target.plugin
458
- }));
459
- });
462
+ };
463
+ }));
460
464
  }
461
465
 
462
466
  function toEventDef(v) {
@@ -464,7 +468,7 @@ function toEventDef(v) {
464
468
  let references = extractReferences(properties);
465
469
  return {
466
470
  name: variantName,
467
- schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
471
+ schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, v)),
468
472
  references: references
469
473
  };
470
474
  };
@@ -592,7 +596,7 @@ function annotateArgTypes(schema, argTypes) {
592
596
  return schema;
593
597
  }
594
598
 
595
- function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, v) {
599
+ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
596
600
  let mkDef = (variantName, properties) => {
597
601
  let match = commandLevelAndId(isAggregate, variantName, properties);
598
602
  let references = extractReferences(properties);
@@ -600,8 +604,36 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
600
604
  TAG: variantName
601
605
  }) : variantName;
602
606
  let declared = commandTransition(syntheticCommand);
603
- let allowedStates = Transition$Reventless.allowedStates(declared);
604
- let targetState = Transition$Reventless.targetState(declared);
607
+ let derived = derivedEdgeFor(variantName);
608
+ let unrestricted = Transition$Reventless.isUnrestricted(declared);
609
+ let match$1 = unrestricted ? undefined : Stdlib_Option.flatMap(derived, d => {
610
+ if (d.allowedStates.length !== 0) {
611
+ return d.allowedStates;
612
+ }
613
+ });
614
+ let match$2 = Transition$Reventless.allowedStates(declared);
615
+ let match$3 = match$1 !== undefined ? [
616
+ match$1,
617
+ "derived"
618
+ ] : (
619
+ match$2 !== undefined ? [
620
+ match$2,
621
+ "declared"
622
+ ] : [
623
+ undefined,
624
+ unrestricted ? "unrestricted" : undefined
625
+ ]
626
+ );
627
+ let observed = Stdlib_Option.flatMap(derived, d => {
628
+ let match = d.targets;
629
+ if (match.length !== 1) {
630
+ return;
631
+ } else {
632
+ return match[0];
633
+ }
634
+ });
635
+ let targetState = observed !== undefined ? observed : Transition$Reventless.targetState(declared);
636
+ let level = Stdlib_Option.getOr(Stdlib_Option.flatMap(derived, d => d.level), match[0]);
605
637
  let apiExposed = false;
606
638
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(parentSchema)) {
607
639
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
@@ -609,17 +641,18 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
609
641
  }
610
642
  let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
611
643
  let mutationField = apiExposed ? mutationFieldFor(variantName) : "";
612
- let jsonSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v);
644
+ let jsonSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(apiExposed, mutationField, v);
613
645
  let annotatedSchema = apiExposed ? Stdlib_Option.mapOr(GraphQL_FragmentGenerator$ReventlessCore.mutationArgTypes(mutationField, v), jsonSchema, __x => annotateArgTypes(jsonSchema, __x)) : jsonSchema;
614
646
  return {
615
647
  name: variantName,
616
648
  schema: JSON.stringify(annotatedSchema),
617
- level: match[0],
649
+ level: level,
618
650
  aggregateIdField: match[1],
619
651
  mutationField: mutationField,
620
652
  references: references,
621
- allowedStates: allowedStates,
653
+ allowedStates: match$3[0],
622
654
  targetState: targetState,
655
+ allowedStatesSource: match$3[1],
623
656
  apiExposed: apiExposed,
624
657
  requiredAccess: requiredAccess,
625
658
  ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
@@ -649,11 +682,12 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
649
682
  }
650
683
  }
651
684
 
652
- function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandTransition, commandSchema) {
685
+ function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
686
+ let derivedEdgeFor = derivedEdgeForOpt !== undefined ? derivedEdgeForOpt : param => {};
653
687
  if (commandSchema.type === "anyOf") {
654
- return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, v));
688
+ return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
655
689
  } else {
656
- return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, commandSchema), [], def => [def]);
690
+ return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
657
691
  }
658
692
  }
659
693
 
@@ -675,7 +709,7 @@ function queryableDefFromSpec(plugin, name, stateSchema, authorization, visibili
675
709
  return {
676
710
  name: name,
677
711
  queryField: qf.listFieldName,
678
- schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(stateSchema)),
712
+ schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, stateSchema)),
679
713
  consumedEventTypes: consumedEventTypes,
680
714
  linkedWriteSide: linkedWriteSide,
681
715
  labelField: label.field,
@@ -695,7 +729,7 @@ function queryableDefFromSpec(plugin, name, stateSchema, authorization, visibili
695
729
  };
696
730
  }
697
731
 
698
- function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt) {
732
+ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt, lifecycleModelOpt) {
699
733
  let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
700
734
  let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
701
735
  let stateViewSlices = stateViewSlicesOpt !== undefined ? stateViewSlicesOpt : [];
@@ -706,6 +740,15 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
706
740
  let extensions = extensionsOpt !== undefined ? extensionsOpt : [];
707
741
  let extensionPoints = extensionPointsOpt !== undefined ? extensionPointsOpt : [];
708
742
  let componentChapters = componentChaptersOpt !== undefined ? componentChaptersOpt : ({});
743
+ let lifecycleModel = lifecycleModelOpt !== undefined ? lifecycleModelOpt : [];
744
+ let model = declaredTransitionsOnly ? [] : lifecycleModel;
745
+ let derivedEdgeFor = (component, command) => model.find(e => {
746
+ if (e.component === component) {
747
+ return e.command === command;
748
+ } else {
749
+ return false;
750
+ }
751
+ });
709
752
  let qualify = (prefix, names) => names.map(n => prefix + "." + n);
710
753
  let scsProduced = stateChangeSlices.map(SCS => [
711
754
  SCS.Spec.name,
@@ -975,7 +1018,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
975
1018
  return {
976
1019
  name: R.Spec.name,
977
1020
  queryField: qf.listFieldName,
978
- schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(stateSchema)),
1021
+ schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, stateSchema)),
979
1022
  consumedEventTypes: consumed,
980
1023
  linkedWriteSide: linkedWriteSideFor(consumed),
981
1024
  labelField: label.field,
@@ -1007,7 +1050,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1007
1050
  return {
1008
1051
  name: SVS.Spec.name,
1009
1052
  queryField: qf.listFieldName,
1010
- schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(stateSchema)),
1053
+ schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, stateSchema)),
1011
1054
  consumedEventTypes: consumed,
1012
1055
  linkedWriteSide: linkedWriteSideFor(consumed),
1013
1056
  labelField: label.field,
@@ -1033,7 +1076,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1033
1076
  let consumed = match$1[1];
1034
1077
  return {
1035
1078
  name: SCS.Spec.name,
1036
- commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, SCS.Spec.commandSchema),
1079
+ commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
1037
1080
  producedEventTypes: produced,
1038
1081
  consumedEventTypes: consumed,
1039
1082
  linkedViews: linkedSvsFor(produced),
@@ -1048,7 +1091,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1048
1091
  let produced = match[1];
1049
1092
  return {
1050
1093
  name: A.Spec.name,
1051
- commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, A.Spec.commandSchema),
1094
+ commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
1052
1095
  producedEventTypes: produced,
1053
1096
  consumedEventTypes: [],
1054
1097
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
@@ -1342,6 +1385,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1342
1385
 
1343
1386
  export {
1344
1387
  log,
1388
+ declaredTransitionsOnly,
1345
1389
  isLabelShape,
1346
1390
  isLifecycleShape,
1347
1391
  conventionalLabelNames,
@@ -31,7 +31,7 @@ let merge: (t<'a, 'b>, t<'a, 'b>) => t<'a, 'b> = (v1, v2) => {
31
31
  }
32
32
 
33
33
  let defaultErrorHandler = err => {
34
- EffectLogger.logError(~comp=__MODULE__, err->JSON.stringifyAny->Option.getOr("unknown"))->Effect.runSync
34
+ EffectLogger.logError(~comp=__MODULE__, err->Reventless.Util_Sury.exnMessage)->Effect.runSync
35
35
  Failure("Couldn't resolve promise.")
36
36
  }
37
37
 
@@ -1,8 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
3
  import * as Effect from "effect/Effect";
5
4
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
5
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
6
6
  import * as EffectLogger$ReventlessCore from "./EffectLogger.res.mjs";
7
7
 
8
8
  function merge(v1, v2) {
@@ -105,7 +105,7 @@ function merge(v1, v2) {
105
105
  }
106
106
 
107
107
  function defaultErrorHandler(err) {
108
- Effect.runSync(EffectLogger$ReventlessCore.logError("Validation-ReventlessCore", undefined, Stdlib_Option.getOr(JSON.stringify(err), "unknown")));
108
+ Effect.runSync(EffectLogger$ReventlessCore.logError("Validation-ReventlessCore", undefined, Util_Sury$Reventless.exnMessage(err, undefined)));
109
109
  return {
110
110
  TAG: "Failure",
111
111
  _0: "Couldn't resolve promise."
@@ -6,10 +6,49 @@ open JestGlobals
6
6
  let stubResource: ReventlessInfra.Adapter.resource = %raw(`{}`)
7
7
 
8
8
  describe("Monitoring", () => {
9
- testSync("Noop default: notify is silent and does not throw", () => {
10
- // No backend registered — the default Noop backend swallows the call.
9
+ testSync("no backend registered: notify is silent and does not throw", () => {
11
10
  Monitoring.notify(~kind=CommandHandler, ~name="before", ~component=stubResource)
12
11
  expect(true)->toBe(true)
12
+ Monitoring.reset()
13
+ })
14
+
15
+ // The case the dead-letter sink is: a module that provisions at import time announces
16
+ // itself before any statement of the deploy program has run, so it is *always* earlier
17
+ // than the `use` call. Dropping those announcements made that one unit unmonitorable no
18
+ // matter what a backend did.
19
+ testSync("an announcement made before any backend is registered reaches the first one", () => {
20
+ let recorded: array<(Monitoring.unitKind, string, option<string>)> = []
21
+ module Late: Monitoring.Backend = {
22
+ let onProvisioned = (~kind, ~name, ~component as _, ~plugin, ~platform as _, ~logLocator as _) =>
23
+ recorded->Array.push((kind, name, plugin))
24
+ }
25
+
26
+ // Provisioned at import time, inside a construct scope that is long gone by the time
27
+ // anyone registers — so the owner must be captured now, not at delivery.
28
+ let prev = ResourceAttribution.enter(~platform="online-shop", ~plugin="Ordering")
29
+ Monitoring.notify(~kind=DeadLetterSink, ~name="DeadLetterQueue", ~component=stubResource)
30
+ ResourceAttribution.restore(prev)
31
+ Monitoring.notify(~kind=Scheduler, ~name="Heartbeat", ~component=stubResource)
32
+
33
+ expect(recorded)->toEqual([])
34
+
35
+ Monitoring.use(module(Late: Monitoring.Backend))
36
+
37
+ expect(recorded)->toEqual([
38
+ (Monitoring.DeadLetterSink, "DeadLetterQueue", Some("Ordering")),
39
+ (Monitoring.Scheduler, "Heartbeat", None),
40
+ ])
41
+
42
+ // Drained: a second registration does not receive them again.
43
+ let second: array<string> = []
44
+ module Other: Monitoring.Backend = {
45
+ let onProvisioned = (~kind as _, ~name, ~component as _, ~plugin as _, ~platform as _, ~logLocator as _) =>
46
+ second->Array.push(name)
47
+ }
48
+ Monitoring.use(module(Other: Monitoring.Backend))
49
+ expect(second)->toEqual([])
50
+
51
+ Monitoring.reset()
13
52
  })
14
53
 
15
54
  testSync("use registers a backend that receives every notify, role and name", () => {
@@ -32,8 +71,8 @@ describe("Monitoring", () => {
32
71
  (Monitoring.Other("Counter"), "ProductCounter"),
33
72
  ])
34
73
 
35
- // Restore the default so registry state doesn't leak to other cases.
36
- Monitoring.use(module(Monitoring.Noop))
74
+ // Restore the initial state so registry and buffer don't leak to other cases.
75
+ Monitoring.reset()
37
76
  })
38
77
 
39
78
  testSync("notify forwards the static name and component resource to the backend", () => {
@@ -50,7 +89,7 @@ describe("Monitoring", () => {
50
89
  | None => expect("no notify received")->toBe("a notify")
51
90
  }
52
91
 
53
- Monitoring.use(module(Monitoring.Noop))
92
+ Monitoring.reset()
54
93
  })
55
94
 
56
95
  testSync("notify delivers the ambient plugin/platform inside a construct scope, None outside", () => {
@@ -78,7 +117,7 @@ describe("Monitoring", () => {
78
117
  (None, None),
79
118
  ])
80
119
 
81
- Monitoring.use(module(Monitoring.Noop))
120
+ Monitoring.reset()
82
121
  })
83
122
 
84
123
  // Unlike the owner, the locator is not ambient — the seam only forwards what the provisioning
@@ -105,6 +144,6 @@ describe("Monitoring", () => {
105
144
 
106
145
  expect(seen)->toEqual([true, false])
107
146
 
108
- Monitoring.use(module(Monitoring.Noop))
147
+ Monitoring.reset()
109
148
  })
110
149
  })
@@ -7,9 +7,51 @@ import * as ResourceAttribution$ReventlessCore from "../../src/ResourceAttributi
7
7
  let stubResource = {};
8
8
 
9
9
  globalThis.describe("Monitoring", () => {
10
- globalThis.test("Noop default: notify is silent and does not throw", () => {
10
+ globalThis.test("no backend registered: notify is silent and does not throw", () => {
11
11
  Monitoring$ReventlessCore.notify("CommandHandler", "before", stubResource, undefined);
12
12
  globalThis.expect(true).toBe(true);
13
+ Monitoring$ReventlessCore.reset();
14
+ });
15
+ globalThis.test("an announcement made before any backend is registered reaches the first one", () => {
16
+ let recorded = [];
17
+ let onProvisioned = (kind, name, param, plugin, param$1, param$2) => {
18
+ recorded.push([
19
+ kind,
20
+ name,
21
+ plugin
22
+ ]);
23
+ };
24
+ let Late = {
25
+ onProvisioned: onProvisioned
26
+ };
27
+ let prev = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
28
+ Monitoring$ReventlessCore.notify("DeadLetterSink", "DeadLetterQueue", stubResource, undefined);
29
+ ResourceAttribution$ReventlessCore.restore(prev);
30
+ Monitoring$ReventlessCore.notify("Scheduler", "Heartbeat", stubResource, undefined);
31
+ globalThis.expect(recorded).toEqual([]);
32
+ Monitoring$ReventlessCore.use(Late);
33
+ globalThis.expect(recorded).toEqual([
34
+ [
35
+ "DeadLetterSink",
36
+ "DeadLetterQueue",
37
+ "Ordering"
38
+ ],
39
+ [
40
+ "Scheduler",
41
+ "Heartbeat",
42
+ undefined
43
+ ]
44
+ ]);
45
+ let second = [];
46
+ let onProvisioned$1 = (param, name, param$1, param$2, param$3, param$4) => {
47
+ second.push(name);
48
+ };
49
+ let Other = {
50
+ onProvisioned: onProvisioned$1
51
+ };
52
+ Monitoring$ReventlessCore.use(Other);
53
+ globalThis.expect(second).toEqual([]);
54
+ Monitoring$ReventlessCore.reset();
13
55
  });
14
56
  globalThis.test("use registers a backend that receives every notify, role and name", () => {
15
57
  let recorded = [];
@@ -51,7 +93,7 @@ globalThis.describe("Monitoring", () => {
51
93
  "ProductCounter"
52
94
  ]
53
95
  ]);
54
- Monitoring$ReventlessCore.use(Monitoring$ReventlessCore.Noop);
96
+ Monitoring$ReventlessCore.reset();
55
97
  });
56
98
  globalThis.test("notify forwards the static name and component resource to the backend", () => {
57
99
  let seen = {
@@ -74,7 +116,7 @@ globalThis.describe("Monitoring", () => {
74
116
  } else {
75
117
  globalThis.expect("no notify received").toBe("a notify");
76
118
  }
77
- Monitoring$ReventlessCore.use(Monitoring$ReventlessCore.Noop);
119
+ Monitoring$ReventlessCore.reset();
78
120
  });
79
121
  globalThis.test("notify delivers the ambient plugin/platform inside a construct scope, None outside", () => {
80
122
  let seen = [];
@@ -107,7 +149,7 @@ globalThis.describe("Monitoring", () => {
107
149
  undefined
108
150
  ]
109
151
  ]);
110
- Monitoring$ReventlessCore.use(Monitoring$ReventlessCore.Noop);
152
+ Monitoring$ReventlessCore.reset();
111
153
  });
112
154
  globalThis.test("notify forwards the log locator when given one, None when omitted", () => {
113
155
  let stubLocator = {};
@@ -125,7 +167,7 @@ globalThis.describe("Monitoring", () => {
125
167
  true,
126
168
  false
127
169
  ]);
128
- Monitoring$ReventlessCore.use(Monitoring$ReventlessCore.Noop);
170
+ Monitoring$ReventlessCore.reset();
129
171
  });
130
172
  });
131
173