@reventlessdev/reventless-core 3.0.0-alpha.255 → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/package.json +9 -9
  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 +8 -3
  21. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
  22. package/src/plugin/component/Plugin.res +1 -0
  23. package/src/plugin/component/Plugin_Builder.res +2 -0
  24. package/src/plugin/component/Plugin_Builder.res.mjs +3 -2
  25. package/src/plugin/component/Plugin_Structure.res +105 -10
  26. package/src/plugin/component/Plugin_Structure.res.mjs +71 -27
  27. package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
  28. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +1 -1
  29. package/src/util/Validation.res +1 -1
  30. package/src/util/Validation.res.mjs +2 -2
  31. package/tests/adapter/MonitoringTest.res +46 -7
  32. package/tests/adapter/MonitoringTest.res.mjs +47 -5
  33. package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
  34. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -0
  35. package/tests/admin/Platform_Admin_StructureTest.res.mjs +2 -14
  36. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +1 -0
  37. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +2 -0
  38. package/tests/admin/Platform_PluginStructuresApiTest.res +8 -14
  39. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +3 -3
  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/fixtures/plugin-lifecycle/2026-07-22-versionconnected-no-requiredstores.json +1 -1
  55. package/tests/fixtures/plugin-lifecycle/2026-07-22-versionsuperseded-nested-definitions.json +1 -1
  56. package/tests/fixtures/plugin-lifecycle/2026-07-28-versionconnected-requiredstores-only.json +1 -1
  57. package/tests/fixtures/plugin-lifecycle/2026-07-30-versionconnected-declarations-without-annotation.json +1 -1
  58. package/tests/fixtures/plugin-lifecycle/2026-07-30-versiondisconnected-declarations-without-annotation.json +1 -1
  59. package/tests/fixtures/plugin-lifecycle/README.md +12 -0
  60. package/tests/logger/LogFormatTest.res +1 -1
  61. package/tests/logger/LogFormatTest.res.mjs +1 -1
  62. package/tests/plugin/ManifestVisibilityTest.res.mjs +1 -1
  63. package/tests/plugin/PluginDefinitionRequiredScalarsTest.res +1 -1
  64. package/tests/plugin/PluginDefinitionScalars.res +13 -8
  65. package/tests/plugin/PluginDefinitionScalars.res.mjs +13 -5
  66. package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
  67. package/tests/plugin/PluginStructureTest.res +184 -0
  68. package/tests/plugin/PluginStructureTest.res.mjs +142 -17
  69. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +1 -1
  70. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +1 -1
  71. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +1 -1
  72. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +1 -1
  73. package/tests/plugin/StateChangeSlice/PsReserveStock.res +14 -1
  74. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +9 -2
  75. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +1 -1
  76. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +1 -1
  77. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +1 -1
  78. package/tests/plugin/pluginDefinitionRequiredScalars.txt +13 -14
  79. package/tests/util/CommandPublisherTest.res.mjs +1 -1
@@ -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,
@@ -71,7 +71,7 @@ type state = {
71
71
  // Admin's manageSubscriptions uses this to wire cross-plugin SNS subscriptions
72
72
  // from this plugin's DCB topic → peer EventCollectors (and vice-versa). None
73
73
  // for pure-aggregate plugins or for plugins persisted before Phase 4.
74
- dcbEventLog: @s.matches(Reventless.Plugin.dcbEventLogOptionSchema) option<Reventless.Plugin.dcbEventLogDefinition>,
74
+ dcbEventLog: option<Reventless.Plugin.dcbEventLogDefinition>,
75
75
  // Business role of the plugin (from pluginDefinition.kind). Lets the admin Plugins
76
76
  // view segregate PlatformInfrastructure / Commercial / Marketplace from Domain.
77
77
  // @scan opts the field into server-side equality filtering so the connection gains
@@ -26,7 +26,7 @@ let stateSchema = Sury.$schema(s => ({
26
26
  apiSchemaFragment: s.m(Sury.$option(Sury.json)),
27
27
  apiTarget: s.m(Sury.$option(Sury.string)),
28
28
  structure: s.m(Sury.$option(Sury.json)),
29
- dcbEventLog: s.m(Plugin$Reventless.dcbEventLogOptionSchema),
29
+ dcbEventLog: s.m(Sury.$option(Plugin$Reventless.dcbEventLogDefinitionSchema)),
30
30
  kind: s.m(Sury.$option(Plugin$Reventless.pluginKindSchema)),
31
31
  otherConnectedVersions: s.m(Sury.array(Plugin$Reventless.versionSchema))
32
32
  }));
@@ -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
 
@@ -71,6 +71,7 @@ let cmd: commandDef = {
71
71
  references: [fieldRef],
72
72
  allowedStates: Some(["Draft"]),
73
73
  targetState: Some("Active"),
74
+ allowedStatesSource: "declared",
74
75
  apiExposed: Some(true),
75
76
  requiredAccess: Some(["catalog.write"]),
76
77
  ownerField: Some("ownerId"),
@@ -61,6 +61,8 @@ let cmd_allowedStates = ["Draft"];
61
61
 
62
62
  let cmd_targetState = "Active";
63
63
 
64
+ let cmd_allowedStatesSource = "declared";
65
+
64
66
  let cmd_apiExposed = true;
65
67
 
66
68
  let cmd_requiredAccess = ["catalog.write"];
@@ -76,6 +78,7 @@ let cmd = {
76
78
  references: cmd_references,
77
79
  allowedStates: cmd_allowedStates,
78
80
  targetState: cmd_targetState,
81
+ allowedStatesSource: cmd_allowedStatesSource,
79
82
  apiExposed: cmd_apiExposed,
80
83
  requiredAccess: cmd_requiredAccess,
81
84
  ownerField: cmd_ownerField
@@ -63,21 +63,9 @@ globalThis.describe("the admin structure's declared transitions are checked", ()
63
63
  globalThis.expect(raises([Platform_Admin_Structure$ReventlessCore.pluginAggregate])).toEqual(false);
64
64
  });
65
65
  globalThis.test("and a state the view does not declare is caught", () => {
66
- let init = commandNamed("Activate");
66
+ let newrecord = {...commandNamed("Activate")};
67
67
  let bogus_name = Platform_Admin_Structure$ReventlessCore.pluginAggregate.name;
68
- let bogus_commands = [{
69
- name: init.name,
70
- schema: init.schema,
71
- level: init.level,
72
- aggregateIdField: init.aggregateIdField,
73
- mutationField: init.mutationField,
74
- references: init.references,
75
- allowedStates: init.allowedStates,
76
- targetState: "Conected",
77
- apiExposed: init.apiExposed,
78
- requiredAccess: init.requiredAccess,
79
- ownerField: init.ownerField
80
- }];
68
+ let bogus_commands = [(newrecord.targetState = "Conected", newrecord)];
81
69
  let bogus_producedEventTypes = Platform_Admin_Structure$ReventlessCore.pluginAggregate.producedEventTypes;
82
70
  let bogus_consumedEventTypes = Platform_Admin_Structure$ReventlessCore.pluginAggregate.consumedEventTypes;
83
71
  let bogus_linkedViews = Platform_Admin_Structure$ReventlessCore.pluginAggregate.linkedViews;
@@ -355,6 +355,7 @@ describe("allowedStates + lifecycleField populated", () => {
355
355
  references: [],
356
356
  allowedStates: Some(["Inactive"]),
357
357
  targetState: Some("Active"),
358
+ allowedStatesSource: "declared",
358
359
  apiExposed: Some(true),
359
360
  requiredAccess: None,
360
361
  ownerField: None,
@@ -410,6 +410,7 @@ globalThis.describe("allowedStates + lifecycleField populated", () => {
410
410
  let cmdWithStates_references = [];
411
411
  let cmdWithStates_allowedStates = ["Inactive"];
412
412
  let cmdWithStates_targetState = "Active";
413
+ let cmdWithStates_allowedStatesSource = "declared";
413
414
  let cmdWithStates_apiExposed = true;
414
415
  let cmdWithStates = {
415
416
  name: "Activate",
@@ -420,6 +421,7 @@ globalThis.describe("allowedStates + lifecycleField populated", () => {
420
421
  references: cmdWithStates_references,
421
422
  allowedStates: cmdWithStates_allowedStates,
422
423
  targetState: cmdWithStates_targetState,
424
+ allowedStatesSource: cmdWithStates_allowedStatesSource,
423
425
  apiExposed: cmdWithStates_apiExposed,
424
426
  requiredAccess: undefined,
425
427
  ownerField: undefined
@@ -187,29 +187,23 @@ describe("absent optional collections", () => {
187
187
  // The case the frozen corpus cannot reach: every fixture in it carries
188
188
  // `"outboundTranslationSlices":[]`, so no stored payload has an outbound slice
189
189
  // to be missing a field. A structure written before `consumedSources` existed
190
- // has no such key, and the schema alone refuses it — `S.nullAsOption` is
191
- // `T | null`, and the variant that also accepts `undefined` fails sury's
192
- // jsonable validation inside a union payload, which is where a structure
193
- // travels. `parseJsonTolerant` is what makes the absence survivable, and it is
194
- // the path the Plugin aggregate replays through.
190
+ // has no such key. `parseJsonTolerant` is what makes the absence survivable,
191
+ // and it is the path the Plugin aggregate replays through.
195
192
  testSync("an outbound slice stored before consumedSources existed still replays", () =>
196
193
  expect(
197
- (
198
- `{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[],` ++
199
- `"targetName":null,"externalSystem":null,"chapter":null}`
200
- )
194
+ `{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[]}`
201
195
  ->JSON.parseOrThrow
202
196
  ->Reventless.Message.parseJsonTolerant(outboundTranslationSliceDefSchema)
203
197
  ->(slice => slice.consumedSources),
204
198
  )->toEqual(None)
205
199
  )
206
200
 
207
- testSync("an explicit null decodes as None", () =>
201
+ // Absent needs no healer: an omitted key IS the optional encoding, so strict
202
+ // decode takes it. `parseJsonTolerant` above is for the fields a stale payload
203
+ // is missing that are NOT optional.
204
+ testSync("an omitted optional decodes as None on the strict path", () =>
208
205
  expect(
209
- (
210
- `{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[],` ++
211
- `"targetName":null,"externalSystem":null,"chapter":null,"consumedSources":null}`
212
- )
206
+ `{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[]}`
213
207
  ->JSON.parseOrThrow
214
208
  ->Reventless.Util_Sury.fromJson(outboundTranslationSliceDefSchema)
215
209
  ->(slice => slice.consumedSources),
@@ -241,10 +241,10 @@ globalThis.describe("absent optional collections", () => {
241
241
  globalThis.expect(bareJson.includes("\"requiredStores\":null")).toEqual(true);
242
242
  });
243
243
  globalThis.test("an outbound slice stored before consumedSources existed still replays", () => {
244
- globalThis.expect(Message$Reventless.parseJsonTolerant(JSON.parse(`{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[],"targetName":null,"externalSystem":null,"chapter":null}`), Plugin$Reventless.outboundTranslationSliceDefSchema).consumedSources).toEqual(undefined);
244
+ globalThis.expect(Message$Reventless.parseJsonTolerant(JSON.parse(`{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[]}`), Plugin$Reventless.outboundTranslationSliceDefSchema).consumedSources).toEqual(undefined);
245
245
  });
246
- globalThis.test("an explicit null decodes as None", () => {
247
- globalThis.expect(Util_Sury$Reventless.fromJson(JSON.parse(`{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[],"targetName":null,"externalSystem":null,"chapter":null,"consumedSources":null}`), Plugin$Reventless.outboundTranslationSliceDefSchema).consumedSources).toEqual(undefined);
246
+ globalThis.test("an omitted optional decodes as None on the strict path", () => {
247
+ globalThis.expect(Util_Sury$Reventless.fromJson(JSON.parse(`{"name":"ToShipper","consumedEventTypes":[],"inboundCommandTypes":[]}`), Plugin$Reventless.outboundTranslationSliceDefSchema).consumedSources).toEqual(undefined);
248
248
  });
249
249
  });
250
250
 
@@ -38,7 +38,7 @@ function commandAuthorization(param) {
38
38
  }
39
39
 
40
40
  function commandTransition(param) {
41
- return "Unrestricted";
41
+ return "Undeclared";
42
42
  }
43
43
 
44
44
  let traits = [];
@@ -27,7 +27,7 @@ function commandAuthorization(param) {
27
27
  }
28
28
 
29
29
  function commandTransition(param) {
30
- return "Unrestricted";
30
+ return "Undeclared";
31
31
  }
32
32
 
33
33
  let traits = [];
@@ -41,7 +41,7 @@ function commandAuthorization(param) {
41
41
  }
42
42
 
43
43
  function commandTransition(param) {
44
- return "Unrestricted";
44
+ return "Undeclared";
45
45
  }
46
46
 
47
47
  let traits = [];
@@ -33,7 +33,7 @@ function commandAuthorization(param) {
33
33
  }
34
34
 
35
35
  function commandTransition(param) {
36
- return "Unrestricted";
36
+ return "Undeclared";
37
37
  }
38
38
 
39
39
  let traits = [];
@@ -357,11 +357,13 @@ describe("GraphQL_FragmentGenerator.mutationArgTypes", () => {
357
357
  expect((typeOf("tip"), typeOf("total")))->toEqual((Some("MoneyInput"), Some("MoneyInput!")))
358
358
  )
359
359
 
360
- // Both of these differ from what the JSON-Schema type alone suggests —
361
- // `orderId` is a plain string there and `itemCount` an integer — which is the
362
- // narrower reason a client cannot derive even the scalars itself.
360
+ // `orderId` differs from what the JSON-Schema type alone suggests — a plain
361
+ // string there, an `ID!` here — which is the narrower reason a client cannot
362
+ // derive even the scalars itself. `itemCount` is an `Int!` on both sides: sury
363
+ // carries the whole-number format and the walk now keeps it, so a quantity is
364
+ // no longer a field a client could legally send `2.5` in.
363
365
  testSync("reports the scalar the server chose, not the JSON-Schema one", () =>
364
- expect((typeOf("orderId"), typeOf("itemCount")))->toEqual((Some("ID!"), Some("Float!")))
366
+ expect((typeOf("orderId"), typeOf("itemCount")))->toEqual((Some("ID!"), Some("Int!")))
365
367
  )
366
368
 
367
369
  // The property that makes publishing worth more than re-deriving downstream: