@reventlessdev/reventless-core 3.0.0-alpha.251 → 3.0.0-alpha.253

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 (73) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +8 -8
  3. package/src/admin/Platform_Admin_Structure.res +4 -1
  4. package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
  5. package/src/admin/Platform_BakedManifest.res.mjs +3 -1
  6. package/src/admin/Platform_PluginStructuresApi.res +43 -7
  7. package/src/admin/Platform_PluginStructuresApi.res.mjs +47 -1
  8. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +8 -0
  9. package/src/components/Api/ApiNoApiHelpers.res +37 -3
  10. package/src/components/Api/ApiNoApiHelpers.res.mjs +29 -2
  11. package/src/components/AutomationSlice/AutomationSlice_Callback.res +4 -4
  12. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +3 -3
  13. package/src/plugin/api/PluginBaseFragment.res +3 -3
  14. package/src/plugin/component/Plugin_Structure.res +158 -25
  15. package/src/plugin/component/Plugin_Structure.res.mjs +82 -18
  16. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +3 -1
  17. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +3 -1
  18. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +3 -1
  19. package/src/plugin/lifecycle/PluginBehavior.res +11 -11
  20. package/src/plugin/lifecycle/PluginBehavior.res.mjs +11 -11
  21. package/src/plugin/lifecycle/PluginSpec.res +39 -16
  22. package/src/plugin/lifecycle/PluginSpec.res.mjs +54 -56
  23. package/tests/admin/AdminApiSchemaDriftTest.res +27 -1
  24. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +22 -2
  25. package/tests/admin/Platform_Admin_StructureTest.res +8 -3
  26. package/tests/admin/Platform_Admin_StructureTest.res.mjs +8 -2
  27. package/tests/admin/Platform_BakedManifestTest.res +2 -0
  28. package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
  29. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +4 -0
  30. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -7
  31. package/tests/admin/Platform_PluginStructuresApiTest.res +16 -0
  32. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +17 -3
  33. package/tests/aggregate/AggregateCacheTest.res.mjs +15 -3
  34. package/tests/aggregate/AggregateConflictTest.res.mjs +15 -3
  35. package/tests/aggregate/AggregateFixtures.res.mjs +15 -3
  36. package/tests/aggregate/AggregateSnapshotTest.res.mjs +21 -5
  37. package/tests/api/ApiNoApiTest.res +46 -0
  38. package/tests/api/ApiNoApiTest.res.mjs +49 -1
  39. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +27 -3
  40. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +12 -2
  41. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +2 -0
  42. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +12 -2
  43. package/tests/dcb/DcbFixtures.res.mjs +8 -0
  44. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +18 -0
  45. package/tests/eventmapper/EventMapperFixtures.res.mjs +15 -3
  46. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +9 -1
  47. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -1
  48. package/tests/message/MessageTest.res +2 -0
  49. package/tests/message/MessageTest.res.mjs +3 -1
  50. package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -0
  51. package/tests/plugin/PluginStructureTest.res +191 -12
  52. package/tests/plugin/PluginStructureTest.res.mjs +246 -6
  53. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +8 -0
  54. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -0
  55. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +17 -7
  56. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +12 -11
  57. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
  58. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
  59. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
  60. package/tests/plugin/StateChangeSlice/PsShipOrder.res +17 -13
  61. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +17 -11
  62. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +54 -0
  63. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +104 -0
  64. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
  65. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
  66. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
  67. package/tests/plugin/StateViewSlice/PsShipmentsView.res +1 -1
  68. package/tests/plugin/pluginDefinitionRequiredScalars.txt +18 -0
  69. package/tests/util/CommandPublisherTest.res.mjs +9 -1
  70. package/src/components/Api/ApiAllowedStatesHelpers.res +0 -18
  71. package/src/components/Api/ApiAllowedStatesHelpers.res.mjs +0 -16
  72. package/src/components/Api/ApiTargetStateHelpers.res +0 -21
  73. package/src/components/Api/ApiTargetStateHelpers.res.mjs +0 -16
@@ -117,7 +117,7 @@ let checkRetiredValue = (~entityName: string, stateSchema: S.t<unknown>): retire
117
117
  ->Option.map(f => ` (that is "${f}")`)
118
118
  ->Option.getOr(
119
119
  " (it declares none)",
120
- )}. A retirement state no command's @transition can name loses the command filtering the state form exists for.`,
120
+ )}. A retirement state no command's declared edge can name loses the command filtering the state form exists for.`,
121
121
  )
122
122
  }
123
123
  let declared = switch stateSchema {
@@ -179,8 +179,9 @@ let reportRetiredStates = (
179
179
  }
180
180
  }
181
181
 
182
- // The states a record's lifecycle field can hold — the field `@transition` is
183
- // written in terms of. `None`: no lifecycle; `Some([])`: one with no cases.
182
+ // The states a record's lifecycle field can hold — the field a command's
183
+ // declared edge is written in terms of. `None`: no lifecycle; `Some([])`: one
184
+ // with no cases.
184
185
  let lifecycleStatesFromStateSchema = (
185
186
  ~entityName: string,
186
187
  stateSchema: S.t<unknown>,
@@ -373,12 +374,17 @@ let reportTranslationTables = (
373
374
  }
374
375
  }
375
376
 
376
- // `@transition` names states belonging to another component's enum, which the PPX
377
- // only ever sees as names — so a misspelling compiles clean and produces a command
378
- // legal in a state no row is in. Both sides are in hand here. A state the linked
379
- // views do not declare raises (this runs at assembly, never in a Lambda, so that is
380
- // a failed deploy); no resolvable view only warns. Checked against the UNION of the
381
- // linked views: a slice feeding two is not claiming which one.
377
+ // A command's declared edge names states belonging to another component's enum,
378
+ // and nothing forces a spec to pick the one its linked view declares. Both sides
379
+ // are in hand here. A state the linked views do not declare raises (this runs at assembly,
380
+ // never in a Lambda, so that is a failed deploy); no resolvable view only warns.
381
+ // Checked against the UNION of the linked views: a slice feeding two is not
382
+ // claiming which one.
383
+ //
384
+ // The compiler has already resolved the switch's constructors, so what survives
385
+ // to here is a component naming the wrong lifecycle enum rather than misspelling
386
+ // a state — the half of the check `lifecycleState` cannot make, since nothing
387
+ // forces a spec to pick its linked view's enum.
382
388
  let checkDeclaredTransitions = (
383
389
  ~pluginName: string,
384
390
  ~writables: array<Reventless.Plugin.writableDef>,
@@ -412,7 +418,7 @@ let checkDeclaredTransitions = (
412
418
  ->Array.forEach(state =>
413
419
  failures
414
420
  ->Array.push(
415
- `${w.name}.${cmd.name}: @transition names "${state}", which none of its ` ++
421
+ `${w.name}.${cmd.name} declares state "${state}", which none of its ` ++
416
422
  `linked views declare — ${w.linkedViews->Array.join(
417
423
  ", ",
418
424
  )} know ${known->Array.join(", ")}.`,
@@ -430,14 +436,14 @@ let checkDeclaredTransitions = (
430
436
  if unvalidated.contents > 0 {
431
437
  log.warn(
432
438
  ~comp="Plugin_Structure",
433
- `${pluginName}: ${unvalidated.contents->Int.toString} command(s) declare a @transition ` ++
439
+ `${pluginName}: ${unvalidated.contents->Int.toString} command(s) declare a transition ` ++
434
440
  `but no linked view declares a lifecycle to check it against.`,
435
441
  )
436
442
  }
437
443
 
438
444
  if Array.length(failures) > 0 {
439
445
  JsError.throwWithMessage(
440
- `${pluginName}: @transition names states that do not exist.\n` ++
446
+ `${pluginName}: a declared transition names states that do not exist.\n` ++
441
447
  failures->Array.join("\n"),
442
448
  )
443
449
  }
@@ -707,6 +713,17 @@ let toCommandDef = (
707
713
  // one aggregate carries commands with very different audiences, and a
708
714
  // component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
709
715
  ~commandAuthorization: unknown => Reventless.Authorization.permission,
716
+ // The spec's `command => Transition.t<_>`, evaluated per variant the same way
717
+ // `commandAuthorization` is, and the only declaration of an edge there is —
718
+ // the PPX injects `Unrestricted` for a spec that writes no switch.
719
+ //
720
+ // Read at `t<string>` because this is the erasure boundary: the spec declares
721
+ // its edges over the linked view's own lifecycle enum, whose arms are
722
+ // payload-less and therefore ARE their own names at run time. The one cast
723
+ // that says so is the same `->Obj.magic` every spec member arrives through
724
+ // (see the call sites below), so `Transition` itself asserts nothing about
725
+ // representation and stays parameterised all the way down.
726
+ ~commandTransition: unknown => Reventless.Transition.t<string>,
710
727
  v: S.t<unknown>,
711
728
  ): option<Reventless.Plugin.commandDef> => {
712
729
  // Build a commandDef for one variant. `properties` is the variant's field dict —
@@ -715,12 +732,22 @@ let toCommandDef = (
715
732
  let mkDef = (~variantName, ~properties) => {
716
733
  let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
717
734
  let references = extractReferences(properties)
718
- // Per-variant, but attached by the PPX to the *parent* schema as one dict.
719
- let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
720
- // The `@transition` target (the command's *to* status), read the same way
721
- // as allowedStates. None ⇒ AutoUI's board resolver falls back to its
722
- // name-stem heuristic.
723
- let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
735
+ // Evaluated against a synthetic value per constructor, the same shape the
736
+ // resolver builds at call time: a payload-bearing variant compiles to
737
+ // `{TAG, ...}`, a payload-less one to a bare string.
738
+ let syntheticCommand: unknown =
739
+ Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
740
+ ? {"TAG": variantName}->Obj.magic
741
+ : variantName->Obj.magic
742
+ // The spec's own switch, which is exhaustive — so it also speaks for a
743
+ // constructor the host did not declare but spliced from a trait.
744
+ //
745
+ // An edge is ONE declaration, so the two fields are read off it together.
746
+ // `targetState: None` ⇒ AutoUI's board resolver falls back to its name-stem
747
+ // heuristic.
748
+ let declared = commandTransition(syntheticCommand)
749
+ let allowedStates = Reventless.Transition.allowedStates(declared)
750
+ let targetState = Reventless.Transition.targetState(declared)
724
751
  // API-exposed iff the whole command isn't @noApi and this variant
725
752
  // isn't in its @noApi-variants set — mirrors the API-generation filter
726
753
  // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
@@ -730,13 +757,6 @@ let toCommandDef = (
730
757
  | Some(excluded) => !(excluded->Set.has(variantName))
731
758
  | None => true
732
759
  }
733
- // Evaluated against a synthetic value per constructor, the same shape the
734
- // resolver builds at call time: a payload-bearing variant compiles to
735
- // `{TAG, ...}`, a payload-less one to a bare string.
736
- let syntheticCommand: unknown =
737
- Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
738
- ? {"TAG": variantName}->Obj.magic
739
- : variantName->Obj.magic
740
760
  let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
741
761
  // See the note on the record's `mutationField` for why a non-exposed
742
762
  // variant gets the empty sentinel. It has no callable field, and the
@@ -796,6 +816,7 @@ let extractCommandDefs = (
796
816
  ~isAggregate,
797
817
  ~mutationFieldFor: string => string,
798
818
  ~commandAuthorization: unknown => Reventless.Authorization.permission,
819
+ ~commandTransition: unknown => Reventless.Transition.t<string>,
799
820
  commandSchema: S.t<unknown>,
800
821
  ): array<Reventless.Plugin.commandDef> =>
801
822
  switch commandSchema {
@@ -806,6 +827,7 @@ let extractCommandDefs = (
806
827
  ~mutationFieldFor,
807
828
  ~parentSchema=commandSchema,
808
829
  ~commandAuthorization,
830
+ ~commandTransition,
809
831
  v,
810
832
  )
811
833
  )
@@ -816,6 +838,7 @@ let extractCommandDefs = (
816
838
  ~mutationFieldFor,
817
839
  ~parentSchema=commandSchema,
818
840
  ~commandAuthorization,
841
+ ~commandTransition,
819
842
  commandSchema,
820
843
  )->Option.mapOr([], def => [def])
821
844
  }
@@ -1061,6 +1084,49 @@ let make = (
1061
1084
  }
1062
1085
  }
1063
1086
 
1087
+ // ── One command name, one handler ──────────────────────────────────────────
1088
+ //
1089
+ // A DCB plugin routes a command by its bare type name: the runtime builds
1090
+ // `handlersByType[typeName]` across every StateChangeSlice in the plugin and
1091
+ // dispatches on the incoming command's TAG alone. Two slices declaring one name
1092
+ // therefore have one handler between them — the second registration overwrites
1093
+ // the first, and the plugin deploys.
1094
+ //
1095
+ // Refused here rather than logged there. The runtime notices only when it goes
1096
+ // to stamp owner fields, by which point the deploy has succeeded and the losing
1097
+ // slice is silently unreachable; a name is a compile-time fact and this is the
1098
+ // last place that can see all of them at once.
1099
+ //
1100
+ // Aggregates are exempt and are not checked: their commands are addressed per
1101
+ // component channel, so two aggregates sharing a command name is ordinary.
1102
+ let dcbCommandOwners: dict<array<string>> = Dict.make()
1103
+ stateChangeSlices->Array.forEach((module(SCS: ReventlessInfra.StateChangeSlice.T)) =>
1104
+ Reventless.DcbTag.extractAllVariantNames(
1105
+ SCS.Spec.commandSchema->S.castToUnknown,
1106
+ )->Array.forEach(cmd => {
1107
+ let owners = dcbCommandOwners->Dict.get(cmd)->Option.getOr([])
1108
+ dcbCommandOwners->Dict.set(cmd, Array.concat(owners, [SCS.Spec.name]))
1109
+ })
1110
+ )
1111
+ let clashes =
1112
+ dcbCommandOwners
1113
+ ->Dict.toArray
1114
+ ->Array.filter(((_, owners)) => owners->Array.length > 1)
1115
+ ->Array.toSorted((((a, _)), ((b, _))) => String.compare(a, b))
1116
+ if clashes->Array.length > 0 {
1117
+ JsError.throwWithMessage(
1118
+ `${name}: ` ++
1119
+ clashes
1120
+ ->Array.map((((cmd, owners))) =>
1121
+ `${owners->Array.join(" and ")} both declare the command "${cmd}"`
1122
+ )
1123
+ ->Array.join("; ") ++
1124
+ `.\n A DCB plugin routes a command by its bare name, so one of these would never run. ` ++
1125
+ `Qualify them — the shipped traits' emitters prefix every name with the host they are ` ++
1126
+ `grafted onto, for exactly this reason.`,
1127
+ )
1128
+ }
1129
+
1064
1130
  // One list feeds both the store collection and the lint below, so a field is
1065
1131
  // read exactly once by either.
1066
1132
  let storeDeclarationSites: array<(string, S.t<unknown>)> =
@@ -1118,6 +1184,69 @@ let make = (
1118
1184
  )
1119
1185
  }
1120
1186
 
1187
+ // Capabilities a slice declares its `translate` reaches for. Sorted and
1188
+ // deduplicated for the same reason the store declarations are: the manifest is
1189
+ // committed, so it must not churn on a re-build. Only outbound translation
1190
+ // slices are handed `Capabilities.t`, so only they can declare.
1191
+ let requiredCapabilities =
1192
+ outboundTranslationSlices
1193
+ ->Array.flatMap((module(OTS: ReventlessInfra.OutboundTranslationSlice.T)) =>
1194
+ OTS.Spec.capabilityNeeds->Array.map(need => (
1195
+ {
1196
+ Reventless.Plugin.capability: Reventless.CapabilityNeed.toString(need),
1197
+ component: OTS.Spec.name,
1198
+ }: Reventless.Plugin.requiredCapabilityDeclaration
1199
+ ))
1200
+ )
1201
+ ->Array.toSorted((a, b) =>
1202
+ a.capability == b.capability
1203
+ ? String.compare(a.component, b.component)
1204
+ : String.compare(a.capability, b.capability)
1205
+ )
1206
+ ->Array.reduce([], (acc, d) =>
1207
+ switch acc->Array.last {
1208
+ | Some(prev) if prev == d => acc
1209
+ | _ => acc->Array.concat([d])
1210
+ }
1211
+ )
1212
+
1213
+ // The traits grafted into this plugin, one entry per declaring component. The
1214
+ // component name is added here rather than declared: a trait cannot know which
1215
+ // component a host grafted it onto, and a host writing it down would be the one
1216
+ // hand-typed string this whole mechanism exists to avoid. Sorted and
1217
+ // deduplicated for the same reason the two declarations above are.
1218
+ let traitDeclarations = {
1219
+ let entry = (~component, decl: Reventless.Trait.t) => (
1220
+ {
1221
+ Reventless.Plugin.trait: decl.trait,
1222
+ version: decl.version,
1223
+ posture: Reventless.Trait.postureToString(decl.posture),
1224
+ component,
1225
+ }: Reventless.Plugin.traitDeclaration
1226
+ )
1227
+ [
1228
+ aggregates->Array.flatMap((module(A: ReventlessInfra.Aggregate.T with type api = api)) =>
1229
+ A.Spec.traits->Array.map(entry(~component=A.Spec.name, ...))
1230
+ ),
1231
+ stateChangeSlices->Array.flatMap((module(SCS: ReventlessInfra.StateChangeSlice.T)) =>
1232
+ SCS.Spec.traits->Array.map(entry(~component=SCS.Spec.name, ...))
1233
+ ),
1234
+ outboundTranslationSlices->Array.flatMap((
1235
+ module(OTS: ReventlessInfra.OutboundTranslationSlice.T),
1236
+ ) => OTS.Spec.traits->Array.map(entry(~component=OTS.Spec.name, ...))),
1237
+ ]
1238
+ ->Array.flat
1239
+ ->Array.toSorted((a, b) =>
1240
+ a.trait == b.trait ? String.compare(a.component, b.component) : String.compare(a.trait, b.trait)
1241
+ )
1242
+ ->Array.reduce([], (acc, d) =>
1243
+ switch acc->Array.last {
1244
+ | Some(prev) if prev == d => acc
1245
+ | _ => acc->Array.concat([d])
1246
+ }
1247
+ )
1248
+ }
1249
+
1121
1250
  // Heuristic-only matches — a field *named* like a stored-object ref with no
1122
1251
  // `@storageRef` — warn and provision nothing. Declaration outranks inference;
1123
1252
  // the warning names the annotation that would settle it.
@@ -1252,6 +1381,7 @@ let make = (
1252
1381
  ~variant=variantName,
1253
1382
  ),
1254
1383
  ~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
1384
+ ~commandTransition=SCS.Spec.commandTransition->Obj.magic,
1255
1385
  SCS.Spec.commandSchema->S.castToUnknown,
1256
1386
  ),
1257
1387
  producedEventTypes: produced,
@@ -1276,6 +1406,7 @@ let make = (
1276
1406
  ~isAggregate=true,
1277
1407
  ~mutationFieldFor=variantName => Api_Naming.aggregateMutationField(~plugin=name, ~aggregate=A.Spec.name, ~command=variantName),
1278
1408
  ~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
1409
+ ~commandTransition=A.Spec.commandTransition->Obj.magic,
1279
1410
  A.Spec.commandSchema->S.castToUnknown,
1280
1411
  ),
1281
1412
  producedEventTypes: produced,
@@ -1747,5 +1878,7 @@ let make = (
1747
1878
  extensionPoints: Some(extensionPointDefs),
1748
1879
  requiredStores: Some(requiredStores),
1749
1880
  requiredStoreDeclarations: Some(requiredStoreDeclarations),
1881
+ traitDeclarations: Some(traitDeclarations),
1882
+ requiredCapabilities: Some(requiredCapabilities),
1750
1883
  }
1751
1884
  }
@@ -9,21 +9,22 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
9
  import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
10
10
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
11
11
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
12
+ import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trait.res.mjs";
12
13
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
13
14
  import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
14
15
  import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
15
16
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
16
17
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
17
18
  import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
19
+ import * as Transition$Reventless from "@reventlessdev/reventless-spec/src/types/Transition.res.mjs";
18
20
  import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
19
21
  import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.mjs";
22
+ import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
20
23
  import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.mjs";
21
24
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
22
25
  import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
23
26
  import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
24
27
  import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
25
- import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
26
- import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
27
28
  import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/GraphQL_FragmentGenerator.res.mjs";
28
29
 
29
30
  let log = Logger$ReventlessCore.fromEnv();
@@ -119,7 +120,7 @@ function checkRetiredValue(entityName, stateSchema) {
119
120
  let named = values.join(", ");
120
121
  let lifecycle = lifecycleFieldFromStateSchema(entityName, stateSchema);
121
122
  if (Primitive_object.notequal(lifecycle, field)) {
122
- log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + named + `) is on "` + field + `", which is not this record's lifecycle field` + Stdlib_Option.getOr(Stdlib_Option.map(lifecycle, f => ` (that is "` + f + `")`), " (it declares none)") + `. A retirement state no command's @transition can name loses the command filtering the state form exists for.`);
123
+ log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + named + `) is on "` + field + `", which is not this record's lifecycle field` + Stdlib_Option.getOr(Stdlib_Option.map(lifecycle, f => ` (that is "` + f + `")`), " (it declares none)") + `. A retirement state no command's declared edge can name loses the command filtering the state form exists for.`);
123
124
  }
124
125
  let declared;
125
126
  declared = stateSchema.type === "object" ? Stdlib_Option.getOr(Stdlib_Option.map(stateSchema.properties[field], schema => {
@@ -343,16 +344,16 @@ function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView)
343
344
  unvalidated.contents = unvalidated.contents + 1 | 0;
344
345
  } else {
345
346
  declared.filter(state => !known.includes(state)).forEach(state => {
346
- failures.push(w.name + `.` + cmd.name + `: @transition names "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
347
+ failures.push(w.name + `.` + cmd.name + ` declares state "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
347
348
  });
348
349
  }
349
350
  });
350
351
  });
351
352
  if (unvalidated.contents > 0) {
352
- 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.`);
353
+ 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.`);
353
354
  }
354
355
  if (failures.length !== 0) {
355
- return Stdlib_JsError.throwWithMessage(pluginName + `: @transition names states that do not exist.\n` + failures.join("\n"));
356
+ return Stdlib_JsError.throwWithMessage(pluginName + `: a declared transition names states that do not exist.\n` + failures.join("\n"));
356
357
  }
357
358
  }
358
359
 
@@ -591,20 +592,21 @@ function annotateArgTypes(schema, argTypes) {
591
592
  return schema;
592
593
  }
593
594
 
594
- function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, v) {
595
+ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, v) {
595
596
  let mkDef = (variantName, properties) => {
596
597
  let match = commandLevelAndId(isAggregate, variantName, properties);
597
598
  let references = extractReferences(properties);
598
- let allowedStates = ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName);
599
- let targetState = ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName);
599
+ let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
600
+ TAG: variantName
601
+ }) : variantName;
602
+ let declared = commandTransition(syntheticCommand);
603
+ let allowedStates = Transition$Reventless.allowedStates(declared);
604
+ let targetState = Transition$Reventless.targetState(declared);
600
605
  let apiExposed = false;
601
606
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(parentSchema)) {
602
607
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
603
608
  apiExposed = excluded !== undefined ? !excluded.has(variantName) : true;
604
609
  }
605
- let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
606
- TAG: variantName
607
- }) : variantName;
608
610
  let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
609
611
  let mutationField = apiExposed ? mutationFieldFor(variantName) : "";
610
612
  let jsonSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v);
@@ -647,11 +649,11 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
647
649
  }
648
650
  }
649
651
 
650
- function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandSchema) {
652
+ function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandTransition, commandSchema) {
651
653
  if (commandSchema.type === "anyOf") {
652
- return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, v));
654
+ return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, v));
653
655
  } else {
654
- return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandSchema), [], def => [def]);
656
+ return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, commandSchema), [], def => [def]);
655
657
  }
656
658
  }
657
659
 
@@ -791,6 +793,17 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
791
793
  return Primitive_string.compare(a.field, b.field);
792
794
  }
793
795
  };
796
+ let dcbCommandOwners = {};
797
+ stateChangeSlices.forEach(SCS => {
798
+ DcbTag$Reventless.extractAllVariantNames(SCS.Spec.commandSchema).forEach(cmd => {
799
+ let owners = Stdlib_Option.getOr(dcbCommandOwners[cmd], []);
800
+ dcbCommandOwners[cmd] = owners.concat([SCS.Spec.name]);
801
+ });
802
+ });
803
+ let clashes = Object.entries(dcbCommandOwners).filter(param => param[1].length > 1).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
804
+ if (clashes.length !== 0) {
805
+ Stdlib_JsError.throwWithMessage(name + `: ` + clashes.map(param => param[1].join(" and ") + ` both declare the command "` + param[0] + `"`).join("; ") + `.\n A DCB plugin routes a command by its bare name, so one of these would never run. Qualify them — the shipped traits' emitters prefix every name with the host they are grafted onto, for exactly this reason.`);
806
+ }
794
807
  let storeDeclarationSites = [
795
808
  aggregates.flatMap(A => [
796
809
  [
@@ -874,6 +887,55 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
874
887
  if (found.length !== 0) {
875
888
  Stdlib_JsError.throwWithMessage(name + `: two declared object stores look like one store misspelled.\n` + found.map(Capability_Inference$ReventlessCore.collisionMessage).join("\n"));
876
889
  }
890
+ let requiredCapabilities = Stdlib_Array.reduce(outboundTranslationSlices.flatMap(OTS => OTS.Spec.capabilityNeeds.map(need => ({
891
+ capability: CapabilityNeed$Reventless.toString(need),
892
+ component: OTS.Spec.name
893
+ }))).toSorted((a, b) => {
894
+ if (a.capability === b.capability) {
895
+ return Primitive_string.compare(a.component, b.component);
896
+ } else {
897
+ return Primitive_string.compare(a.capability, b.capability);
898
+ }
899
+ }), [], (acc, d) => {
900
+ let prev = Stdlib_Array.last(acc);
901
+ if (prev !== undefined) {
902
+ if (Primitive_object.equal(prev, d)) {
903
+ return acc;
904
+ } else {
905
+ return acc.concat([d]);
906
+ }
907
+ } else {
908
+ return acc.concat([d]);
909
+ }
910
+ });
911
+ let entry = (component, decl) => ({
912
+ trait: decl.trait,
913
+ version: decl.version,
914
+ posture: Trait$Reventless.postureToString(decl.posture),
915
+ component: component
916
+ });
917
+ let traitDeclarations = Stdlib_Array.reduce([
918
+ aggregates.flatMap(A => A.Spec.traits.map(extra => entry(A.Spec.name, extra))),
919
+ stateChangeSlices.flatMap(SCS => SCS.Spec.traits.map(extra => entry(SCS.Spec.name, extra))),
920
+ outboundTranslationSlices.flatMap(OTS => OTS.Spec.traits.map(extra => entry(OTS.Spec.name, extra)))
921
+ ].flat().toSorted((a, b) => {
922
+ if (a.trait === b.trait) {
923
+ return Primitive_string.compare(a.component, b.component);
924
+ } else {
925
+ return Primitive_string.compare(a.trait, b.trait);
926
+ }
927
+ }), [], (acc, d) => {
928
+ let prev = Stdlib_Array.last(acc);
929
+ if (prev !== undefined) {
930
+ if (Primitive_object.equal(prev, d)) {
931
+ return acc;
932
+ } else {
933
+ return acc.concat([d]);
934
+ }
935
+ } else {
936
+ return acc.concat([d]);
937
+ }
938
+ });
877
939
  storeDeclarationSites.forEach(param => {
878
940
  Capability_Inference$ReventlessCore.scanSchema(param[0], param[1]).forEach(w => log.warn("Plugin_Structure", undefined, Capability_Inference$ReventlessCore.message(w)));
879
941
  });
@@ -968,7 +1030,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
968
1030
  let consumed = match$1[1];
969
1031
  return {
970
1032
  name: SCS.Spec.name,
971
- commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandSchema),
1033
+ 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),
972
1034
  producedEventTypes: produced,
973
1035
  consumedEventTypes: consumed,
974
1036
  linkedViews: linkedSvsFor(produced),
@@ -983,7 +1045,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
983
1045
  let produced = match[1];
984
1046
  return {
985
1047
  name: A.Spec.name,
986
- commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandSchema),
1048
+ commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, A.Spec.commandSchema),
987
1049
  producedEventTypes: produced,
988
1050
  consumedEventTypes: [],
989
1051
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
@@ -1268,7 +1330,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1268
1330
  extensions: extensionDefs,
1269
1331
  extensionPoints: extensionPointDefs,
1270
1332
  requiredStores: requiredStores,
1271
- requiredStoreDeclarations: requiredStoreDeclarations
1333
+ requiredStoreDeclarations: requiredStoreDeclarations,
1334
+ requiredCapabilities: requiredCapabilities,
1335
+ traitDeclarations: traitDeclarations
1272
1336
  };
1273
1337
  }
1274
1338
 
@@ -75,7 +75,9 @@ function Make(Spec) {
75
75
  errorSchema: ExtensionMapping$ReventlessInfra.NoDelegate.errorSchema,
76
76
  commandSchema: ExtensionMapping$ReventlessInfra.NoDelegate.commandSchema,
77
77
  moduleUrl: ExtensionMapping$ReventlessInfra.NoDelegate.moduleUrl,
78
- commandAuthorization: ExtensionMapping$ReventlessInfra.NoDelegate.commandAuthorization
78
+ commandAuthorization: ExtensionMapping$ReventlessInfra.NoDelegate.commandAuthorization,
79
+ commandTransition: ExtensionMapping$ReventlessInfra.NoDelegate.commandTransition,
80
+ traits: ExtensionMapping$ReventlessInfra.NoDelegate.traits
79
81
  },
80
82
  moduleUrl: PluginExtensionPointSpec$ReventlessInfra.moduleUrl,
81
83
  delegateModuleUrl: delegateModuleUrl,
@@ -405,7 +405,9 @@ function Make(Spec) {
405
405
  errorSchema: PluginSpec$ReventlessCore.errorSchema,
406
406
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
407
407
  moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
408
- commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
408
+ commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
409
+ commandTransition: PluginSpec$ReventlessCore.commandTransition,
410
+ traits: PluginSpec$ReventlessCore.traits
409
411
  },
410
412
  moduleUrl: PluginExtensionPointSpec$ReventlessInfra.moduleUrl,
411
413
  mapIncomingCommand: mapIncomingCommand,
@@ -72,7 +72,9 @@ let Mapping = ExtensionPointMapping$ReventlessInfra.Make({
72
72
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
73
73
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
74
74
  moduleUrl: UiFragmentRegistry$ReventlessCore.moduleUrl,
75
- commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization
75
+ commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
76
+ commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
77
+ traits: UiFragmentRegistry$ReventlessCore.traits
76
78
  },
77
79
  moduleUrl: moduleUrl,
78
80
  mapIncomingCommand: mapIncomingCommand,
@@ -159,19 +159,19 @@ let decide = (state, command) =>
159
159
  Ok(Array.concat([VersionDisconnected(definition)], promoteEvents(state, v)))
160
160
  | _ => Ok([]) // not connected / unknown → tolerate stray disconnect
161
161
  }
162
- | Activate(v) =>
163
- switch state.known->Dict.get(v) {
162
+ | Activate({version}) =>
163
+ switch state.known->Dict.get(version) {
164
164
  | Some({status: Inactive | Retired, definition}) =>
165
165
  let base = [VersionActivated(definition)]
166
- let supersede = if currentAfterConnect(state.known, v) == v {
166
+ let supersede = if currentAfterConnect(state.known, version) == version {
167
167
  switch state.current {
168
- | Some(c) if c != v =>
168
+ | Some(c) if c != version =>
169
169
  switch state.known->Dict.get(c) {
170
170
  | Some({definition: defC}) => [
171
171
  VersionSuperseded({
172
172
  supersededVersion: c,
173
173
  supersededDefinition: defC,
174
- newVersion: v,
174
+ newVersion: version,
175
175
  newDefinition: definition,
176
176
  }),
177
177
  ]
@@ -187,18 +187,18 @@ let decide = (state, command) =>
187
187
  | Some({status: Disconnected}) => Error(IsDisconnected)
188
188
  | None => Error(UnknownVersion)
189
189
  }
190
- | Deactivate(v) =>
191
- switch state.known->Dict.get(v) {
190
+ | Deactivate({version}) =>
191
+ switch state.known->Dict.get(version) {
192
192
  | Some({status: Connected | Disconnected, definition}) =>
193
- Ok(Array.concat([VersionDeactivated(definition)], promoteEvents(state, v)))
193
+ Ok(Array.concat([VersionDeactivated(definition)], promoteEvents(state, version)))
194
194
  | Some({status: Inactive | Retired}) => Ok([]) // idempotent / archived no-op
195
195
  | None => Error(UnknownVersion)
196
196
  }
197
- | Retire(v) =>
198
- switch state.known->Dict.get(v) {
197
+ | Retire({version}) =>
198
+ switch state.known->Dict.get(version) {
199
199
  | Some({status: Retired}) => Ok([]) // idempotent
200
200
  | Some({definition}) =>
201
- Ok(Array.concat([VersionRetired(definition)], promoteEvents(state, v)))
201
+ Ok(Array.concat([VersionRetired(definition)], promoteEvents(state, version)))
202
202
  | None => Error(UnknownVersion)
203
203
  }
204
204
  | ReportIncompatibility(def) => Ok([IncompatiblePluginDetected(def)])
@@ -232,8 +232,8 @@ function decide(state, command) {
232
232
  };
233
233
  }
234
234
  case "Activate" :
235
- let v$4 = command._0;
236
- let match$4 = state.known[v$4];
235
+ let version = command.version;
236
+ let match$4 = state.known[version];
237
237
  if (match$4 === undefined) {
238
238
  return {
239
239
  TAG: "Error",
@@ -261,17 +261,17 @@ function decide(state, command) {
261
261
  _0: definition
262
262
  }];
263
263
  let supersede;
264
- if (currentAfterConnect(state.known, v$4) === v$4) {
264
+ if (currentAfterConnect(state.known, version) === version) {
265
265
  let c = state.current;
266
266
  if (c !== undefined) {
267
- if (c !== v$4) {
267
+ if (c !== version) {
268
268
  let match$5 = state.known[c];
269
269
  supersede = match$5 !== undefined ? [{
270
270
  TAG: "VersionSuperseded",
271
271
  _0: {
272
272
  supersededVersion: c,
273
273
  supersededDefinition: match$5.definition,
274
- newVersion: v$4,
274
+ newVersion: version,
275
275
  newDefinition: definition
276
276
  }
277
277
  }] : [];
@@ -290,8 +290,8 @@ function decide(state, command) {
290
290
  };
291
291
  break;
292
292
  case "Deactivate" :
293
- let v$5 = command._0;
294
- let match$6 = state.known[v$5];
293
+ let version$1 = command.version;
294
+ let match$6 = state.known[version$1];
295
295
  if (match$6 === undefined) {
296
296
  return {
297
297
  TAG: "Error",
@@ -314,12 +314,12 @@ function decide(state, command) {
314
314
  _0: [{
315
315
  TAG: "VersionDeactivated",
316
316
  _0: match$6.definition
317
- }].concat(promoteEvents(state, v$5))
317
+ }].concat(promoteEvents(state, version$1))
318
318
  };
319
319
  break;
320
320
  case "Retire" :
321
- let v$6 = command._0;
322
- let match$7 = state.known[v$6];
321
+ let version$2 = command.version;
322
+ let match$7 = state.known[version$2];
323
323
  if (match$7 !== undefined) {
324
324
  if (match$7.status === "Retired") {
325
325
  return {
@@ -332,7 +332,7 @@ function decide(state, command) {
332
332
  _0: [{
333
333
  TAG: "VersionRetired",
334
334
  _0: match$7.definition
335
- }].concat(promoteEvents(state, v$6))
335
+ }].concat(promoteEvents(state, version$2))
336
336
  };
337
337
  }
338
338
  } else {