@reventlessdev/reventless-core 3.0.0-alpha.250 → 3.0.0-alpha.252

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 (70) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +7 -7
  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_ComponentDefinitionsApi.res +31 -1
  7. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +29 -1
  8. package/src/admin/Platform_PluginStructuresApi.res +80 -8
  9. package/src/admin/Platform_PluginStructuresApi.res.mjs +76 -2
  10. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +8 -0
  11. package/src/components/Api/ApiNoApiHelpers.res +37 -3
  12. package/src/components/Api/ApiNoApiHelpers.res.mjs +29 -2
  13. package/src/components/AutomationSlice/AutomationSlice_Callback.res +4 -4
  14. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +3 -3
  15. package/src/plugin/api/PluginBaseFragment.res +3 -3
  16. package/src/plugin/component/Plugin_Structure.res +787 -384
  17. package/src/plugin/component/Plugin_Structure.res.mjs +430 -20
  18. package/src/plugin/connect/PluginConnectExtension_Mapping.res +3 -6
  19. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +14 -2
  20. package/src/plugin/connect/PluginExtensionPoint_Plugin.res +0 -1
  21. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +63 -3
  22. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +22 -3
  23. package/src/plugin/lifecycle/PluginBehavior.res +11 -11
  24. package/src/plugin/lifecycle/PluginBehavior.res.mjs +11 -11
  25. package/src/plugin/lifecycle/PluginSpec.res +11 -3
  26. package/src/plugin/lifecycle/PluginSpec.res.mjs +11 -3
  27. package/tests/admin/AdminApiSchemaDriftTest.res +486 -0
  28. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +619 -0
  29. package/tests/admin/Platform_Admin_StructureTest.res +8 -3
  30. package/tests/admin/Platform_Admin_StructureTest.res.mjs +8 -2
  31. package/tests/admin/Platform_BakedManifestTest.res +2 -0
  32. package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
  33. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
  34. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +30 -8
  35. package/tests/admin/Platform_PluginStructuresApiTest.res +28 -1
  36. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -5
  37. package/tests/aggregate/AggregateCacheTest.res.mjs +15 -3
  38. package/tests/aggregate/AggregateConflictTest.res.mjs +15 -3
  39. package/tests/aggregate/AggregateFixtures.res.mjs +15 -3
  40. package/tests/aggregate/AggregateSnapshotTest.res.mjs +21 -5
  41. package/tests/api/ApiNoApiTest.res +46 -0
  42. package/tests/api/ApiNoApiTest.res.mjs +49 -1
  43. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +27 -3
  44. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +12 -2
  45. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +2 -0
  46. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +12 -2
  47. package/tests/dcb/DcbFixtures.res.mjs +8 -0
  48. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +18 -0
  49. package/tests/eventmapper/EventMapperFixtures.res.mjs +15 -3
  50. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +9 -1
  51. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -1
  52. package/tests/message/MessageTest.res +2 -0
  53. package/tests/message/MessageTest.res.mjs +3 -1
  54. package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -0
  55. package/tests/plugin/PluginStructureTest.res +294 -1
  56. package/tests/plugin/PluginStructureTest.res.mjs +373 -3
  57. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +8 -0
  58. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -0
  59. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +8 -0
  60. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
  61. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
  62. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
  63. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +8 -0
  64. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +59 -0
  65. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +115 -0
  66. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
  67. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
  68. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
  69. package/tests/plugin/pluginDefinitionRequiredScalars.txt +26 -0
  70. package/tests/util/CommandPublisherTest.res.mjs +9 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,46 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.252 (2026-09-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** [@no](https://github.com/no)Api was lost when a command variant was spliced into another type ([00403fb](https://github.com/ReventlessDev/reventless-core/commit/00403fb4b8873a68a69558829e598339a5012833))
11
+ * **automation:** a mapping is handed the envelope's id ([9ee482c](https://github.com/ReventlessDev/reventless-core/commit/9ee482c5ad0aae09efd1259eeb7f39b781867b91))
12
+ * **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
13
+ ### Features
14
+
15
+ * **plugin:** refuse two DCB slices that declare one command name ([8c6ec39](https://github.com/ReventlessDev/reventless-core/commit/8c6ec39cb3c6768446f46d0fc6dc04c256e2e9d9))
16
+ * **plugin:** the admin lifecycle commands name their argument ([ea552ec](https://github.com/ReventlessDev/reventless-core/commit/ea552ec64e5db0b3dc465c0d9a80cac89627f727))
17
+ * **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
18
+ * **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
19
+
20
+
21
+ # 3.0.0-alpha.251 (2026-08-27)
22
+
23
+ * feat(spec)!: reflect the command direction across a port, both halves ([f2fe258](https://github.com/ReventlessDev/reventless-core/commit/f2fe258d195b74f4a61488edee305665341020ea))
24
+ * feat(spec)!: drop the plugin protocol's reconnect event, which nothing published ([7549db6](https://github.com/ReventlessDev/reventless-core/commit/7549db6aac6ad3f94dbb8d72dac4b4e783756ad1))
25
+ * feat(spec)!: read the port's translation table off the mapping's arms ([956348a](https://github.com/ReventlessDev/reventless-core/commit/956348a9fcb256cfff9db51809bdc27d73360e6c))
26
+
27
+ ### BREAKING CHANGES
28
+
29
+ * `ExtensionPointMapping.Mapping` gains `acceptedCommands` and
30
+ `ExtensionMapping.Mapping` gains `issuedCommands`; both are derived, so a
31
+ mapping the ppx can read needs no source change, and one it cannot names itself
32
+ at compile time. Definitions persisted before the two new def fields must be
33
+ re-emitted before a consumer can read them as present — and until then they
34
+ decode as None, which means unknown, not "issues nothing".
35
+ * `PluginExtensionPointSpec.event` no longer declares
36
+ `PluginReconnected`. An extension matching on it was matching an event it could
37
+ never receive; drop the arm.
38
+ * `ExtensionPointMapping.Mapping` requires `publishedEvents` and
39
+ `ExtensionMapping.Mapping` requires `handledEvents`; the ppx injects both for
40
+ mappings it can read, so app code must build against the matching ppx. Plugin
41
+ definitions persisted before the new fields must be re-emitted before a consumer
42
+ reads them as present.
43
+
44
+
45
+
6
46
  # 3.0.0-alpha.250 (2026-08-23)
7
47
 
8
48
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.250",
3
+ "version": "3.0.0-alpha.252",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,22 +28,22 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0-rc.2",
30
30
  "uuid": "^13.0.0",
31
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
31
  "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.8",
32
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
33
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
34
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
35
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
36
35
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
36
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.8",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.33",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.123",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.151"
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.153",
40
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.34",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.125"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-rc.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.73"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.75"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -24,7 +24,7 @@ let commandSchema = PluginSpec.commandSchema->S.castToUnknown
24
24
  // Every variant of `PluginSpec.command`, walked by the same `extractCommandDefs`
25
25
  // an ordinary aggregate goes through. The hand-written alternative described the
26
26
  // arguments with a local `{id}` schema while the generated SDL declared
27
- // `Platform_Plugin_Activate(_0: String!, id: ID!)`, so AutoUI built a mutation
27
+ // `Platform_Plugin_Activate(id: ID!, version: String!)`, so AutoUI built a mutation
28
28
  // missing a required argument and every admin row action was rejected before it
29
29
  // reached the aggregate. Deriving from `commandSchema` — the same schema the SDL
30
30
  // is generated from — cannot reproduce that class of fault.
@@ -38,6 +38,7 @@ let pluginCommands: array<commandDef> = Plugin_Structure.extractCommandDefs(
38
38
  ~mutationFieldFor=variantName =>
39
39
  Api_Naming.adminField(~name=PluginSpec.name ++ "_" ++ variantName),
40
40
  ~commandAuthorization=PluginSpec.commandAuthorization->Obj.magic,
41
+ ~commandTransition=PluginSpec.commandTransition->Obj.magic,
41
42
  commandSchema,
42
43
  )
43
44
 
@@ -76,6 +77,8 @@ let structure: pluginStructure = {
76
77
  extensionPoints: None,
77
78
  requiredStores: None,
78
79
  requiredStoreDeclarations: None,
80
+ requiredCapabilities: None,
81
+ traitDeclarations: None,
79
82
  }
80
83
 
81
84
  /** The definition the platform's own EventCollector ships as its
@@ -7,7 +7,7 @@ import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/Plugin
7
7
 
8
8
  let pluginId = "Platform";
9
9
 
10
- let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandSchema);
10
+ let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, PluginSpec$ReventlessCore.commandSchema);
11
11
 
12
12
  let pluginAggregate_producedEventTypes = [];
13
13
 
@@ -60,7 +60,9 @@ let structure = {
60
60
  extensions: structure_extensions,
61
61
  extensionPoints: undefined,
62
62
  requiredStores: undefined,
63
- requiredStoreDeclarations: undefined
63
+ requiredStoreDeclarations: undefined,
64
+ requiredCapabilities: undefined,
65
+ traitDeclarations: undefined
64
66
  };
65
67
 
66
68
  let internalPluginDefinition = {
@@ -134,7 +134,9 @@ function curateStructure(pluginId, def, sel) {
134
134
  extensions: def.extensions,
135
135
  extensionPoints: def.extensionPoints,
136
136
  requiredStores: def.requiredStores,
137
- requiredStoreDeclarations: def.requiredStoreDeclarations
137
+ requiredStoreDeclarations: def.requiredStoreDeclarations,
138
+ requiredCapabilities: def.requiredCapabilities,
139
+ traitDeclarations: def.traitDeclarations
138
140
  };
139
141
  });
140
142
  }
@@ -33,7 +33,11 @@ let sdlTypes: array<string> = [
33
33
  `type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
34
34
  `type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
35
35
  `type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
36
- `type Platform_ExtensionDef {\n name: String!\n delegateNames: [String!]!\n eventTypes: [String!]!\n commandTypes: [String!]!\n}`,
36
+ // The subscriber's half of the port's translation table — see `handledEventDef`.
37
+ `type Platform_HandledEventDef {\n name: String!\n toCommandTypes: [String!]!\n}`,
38
+ // Its command-direction mirror — see `issuedCommandDef`.
39
+ `type Platform_IssuedCommandDef {\n name: String!\n fromEventTypes: [String!]!\n}`,
40
+ `type Platform_ExtensionDef {\n name: String!\n delegateNames: [String!]!\n eventTypes: [String!]!\n commandTypes: [String!]!\n handledEvents: [Platform_HandledEventDef!]\n issuedCommands: [Platform_IssuedCommandDef!]\n}`,
37
41
  // `internalQueryables` is the complement of the `readModels` / `stateViewSlices`
38
42
  // filter, not an addition to either: it carries exactly the components the
39
43
  // filter removed. A consumer enumerating surfaces reads the two filtered lists
@@ -208,6 +212,32 @@ let encodeExtensionDef = (e: extensionDef): JSON.t =>
208
212
  ("delegateNames", encodeStrings(e.delegateNames)),
209
213
  ("eventTypes", encodeStrings(e.eventTypes)),
210
214
  ("commandTypes", encodeStrings(e.commandTypes)),
215
+ (
216
+ "handledEvents",
217
+ e.handledEvents->Option.mapOr(JSON.Encode.null, handled =>
218
+ handled
219
+ ->Array.map(h =>
220
+ Dict.fromArray([
221
+ ("name", JSON.Encode.string(h.name)),
222
+ ("toCommandTypes", encodeStrings(h.toCommandTypes)),
223
+ ])->JSON.Encode.object
224
+ )
225
+ ->JSON.Encode.array
226
+ ),
227
+ ),
228
+ (
229
+ "issuedCommands",
230
+ e.issuedCommands->Option.mapOr(JSON.Encode.null, issued =>
231
+ issued
232
+ ->Array.map(i =>
233
+ Dict.fromArray([
234
+ ("name", JSON.Encode.string(i.name)),
235
+ ("fromEventTypes", encodeStrings(i.fromEventTypes)),
236
+ ])->JSON.Encode.object
237
+ )
238
+ ->JSON.Encode.array
239
+ ),
240
+ ),
211
241
  ])->JSON.Encode.object
212
242
 
213
243
  // `derived` is curation, not structure: it says which of the pages a shell
@@ -14,7 +14,9 @@ let sdlTypes = [
14
14
  `type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
15
15
  `type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
16
16
  `type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
17
- `type Platform_ExtensionDef {\n name: String!\n delegateNames: [String!]!\n eventTypes: [String!]!\n commandTypes: [String!]!\n}`,
17
+ `type Platform_HandledEventDef {\n name: String!\n toCommandTypes: [String!]!\n}`,
18
+ `type Platform_IssuedCommandDef {\n name: String!\n fromEventTypes: [String!]!\n}`,
19
+ `type Platform_ExtensionDef {\n name: String!\n delegateNames: [String!]!\n eventTypes: [String!]!\n commandTypes: [String!]!\n handledEvents: [Platform_HandledEventDef!]\n issuedCommands: [Platform_IssuedCommandDef!]\n}`,
18
20
  `type Platform_ComponentDefinitionEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n internalQueryables: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n}`
19
21
  ];
20
22
 
@@ -347,6 +349,32 @@ function encodeExtensionDef(e) {
347
349
  [
348
350
  "commandTypes",
349
351
  e.commandTypes.map(prim => prim)
352
+ ],
353
+ [
354
+ "handledEvents",
355
+ Stdlib_Option.mapOr(e.handledEvents, null, handled => handled.map(h => Object.fromEntries([
356
+ [
357
+ "name",
358
+ h.name
359
+ ],
360
+ [
361
+ "toCommandTypes",
362
+ h.toCommandTypes.map(prim => prim)
363
+ ]
364
+ ])))
365
+ ],
366
+ [
367
+ "issuedCommands",
368
+ Stdlib_Option.mapOr(e.issuedCommands, null, issued => issued.map(i => Object.fromEntries([
369
+ [
370
+ "name",
371
+ i.name
372
+ ],
373
+ [
374
+ "fromEventTypes",
375
+ i.fromEventTypes.map(prim => prim)
376
+ ]
377
+ ])))
350
378
  ]
351
379
  ]);
352
380
  }
@@ -28,21 +28,43 @@
28
28
  open Reventless.Plugin
29
29
 
30
30
  let sdlTypes: array<string> = [
31
+ // The port's translation table — see `publishedEventDef`.
32
+ `type Platform_PublishedEventDef {\n name: String!\n fromEventTypes: [String!]!\n}`,
33
+ // Its command-direction mirror — see `acceptedCommandDef`.
34
+ `type Platform_AcceptedCommandDef {\n name: String!\n toCommandTypes: [String!]!\n}`,
31
35
  // Producer side of an extension point. `commandTypes` is null for a
32
36
  // notification-only (`command = unit`) extension point — see `extensionPointDef`.
33
- `type Platform_ExtensionPointDef {\n name: String!\n delegateNames: [String!]!\n sourceEventTypes: [String!]!\n commandTypes: [String!]\n}`,
37
+ `type Platform_ExtensionPointDef {\n name: String!\n delegateNames: [String!]!\n sourceEventTypes: [String!]!\n commandTypes: [String!]\n publishedEvents: [Platform_PublishedEventDef!]\n acceptedCommands: [Platform_AcceptedCommandDef!]\n}`,
34
38
  `type Platform_RequiredStoreDeclaration {\n store: String!\n component: String!\n field: String!\n annotation: String\n}`,
35
- // `extensionPoints` / `requiredStores` / `requiredStoreDeclarations` are nullable
36
- // lists, not `[T!]!`: all three are optional on `pluginStructure` so that plugin
37
- // definitions persisted before the field existed still decode. A structure written
38
- // by an older deploy sends null, and null is the honest answer — an empty list
39
- // would claim the plugin has no extension points when the truth is that the
40
- // deployment cannot say.
41
- `type Platform_PluginStructureEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n extensionPoints: [Platform_ExtensionPointDef!]\n requiredStores: [String!]\n requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]\n}`,
39
+ // The capability a component declares its translate reaches for. No `field`:
40
+ // unlike a store, this need is not expressible as an annotation on one.
41
+ `type Platform_RequiredCapabilityDeclaration {\n capability: String!\n component: String!\n}`,
42
+ // One graft's provenance. `posture` is a String rather than an enum for the
43
+ // reason the capability above is: a plugin built against a newer framework,
44
+ // naming a posture this schema has never heard of, still decodes.
45
+ //
46
+ // It records ORIGIN, never behaviour — a grafted file is the host's to edit
47
+ // afterwards — so a reader must not paint a declared graft as a verified one.
48
+ `type Platform_TraitDeclaration {\n trait: String!\n version: String!\n posture: String!\n component: String!\n}`,
49
+ // `extensionPoints` / `requiredStores` / `requiredStoreDeclarations` /
50
+ // `requiredCapabilities` / `traitDeclarations` are nullable lists, not `[T!]!`: all five are optional on
51
+ // `pluginStructure` so that plugin definitions persisted before the field existed
52
+ // still decode. A structure written by an older deploy sends null, and null is the
53
+ // honest answer — an empty list would claim the plugin has no extension points when
54
+ // the truth is that the deployment cannot say.
55
+ `type Platform_PluginStructureEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n extensionPoints: [Platform_ExtensionPointDef!]\n requiredStores: [String!]\n requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]\n requiredCapabilities: [Platform_RequiredCapabilityDeclaration!]\n traitDeclarations: [Platform_TraitDeclaration!]\n}`,
42
56
  ]
43
57
 
44
58
  let sdlQueryField: string = ` Platform_PluginStructures: [Platform_PluginStructureEntry!]!`
45
59
 
60
+ let encodeTraitDeclaration = (d: traitDeclaration): JSON.t =>
61
+ Dict.fromArray([
62
+ ("trait", JSON.Encode.string(d.trait)),
63
+ ("version", JSON.Encode.string(d.version)),
64
+ ("posture", JSON.Encode.string(d.posture)),
65
+ ("component", JSON.Encode.string(d.component)),
66
+ ])->JSON.Encode.object
67
+
46
68
  let encodeExtensionPointDef = (e: extensionPointDef): JSON.t =>
47
69
  Dict.fromArray([
48
70
  ("name", JSON.Encode.string(e.name)),
@@ -52,6 +74,38 @@ let encodeExtensionPointDef = (e: extensionPointDef): JSON.t =>
52
74
  "commandTypes",
53
75
  e.commandTypes->Option.mapOr(JSON.Encode.null, Platform_ComponentDefinitionsApi.encodeStrings),
54
76
  ),
77
+ (
78
+ "publishedEvents",
79
+ e.publishedEvents->Option.mapOr(JSON.Encode.null, published =>
80
+ published
81
+ ->Array.map(p =>
82
+ Dict.fromArray([
83
+ ("name", JSON.Encode.string(p.name)),
84
+ (
85
+ "fromEventTypes",
86
+ Platform_ComponentDefinitionsApi.encodeStrings(p.fromEventTypes),
87
+ ),
88
+ ])->JSON.Encode.object
89
+ )
90
+ ->JSON.Encode.array
91
+ ),
92
+ ),
93
+ (
94
+ "acceptedCommands",
95
+ e.acceptedCommands->Option.mapOr(JSON.Encode.null, accepted =>
96
+ accepted
97
+ ->Array.map(a =>
98
+ Dict.fromArray([
99
+ ("name", JSON.Encode.string(a.name)),
100
+ (
101
+ "toCommandTypes",
102
+ Platform_ComponentDefinitionsApi.encodeStrings(a.toCommandTypes),
103
+ ),
104
+ ])->JSON.Encode.object
105
+ )
106
+ ->JSON.Encode.array
107
+ ),
108
+ ),
55
109
  ])->JSON.Encode.object
56
110
 
57
111
  let encodeRequiredStoreDeclaration = (d: requiredStoreDeclaration): JSON.t =>
@@ -62,6 +116,12 @@ let encodeRequiredStoreDeclaration = (d: requiredStoreDeclaration): JSON.t =>
62
116
  ("annotation", d.annotation->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
63
117
  ])->JSON.Encode.object
64
118
 
119
+ let encodeRequiredCapabilityDeclaration = (d: requiredCapabilityDeclaration): JSON.t =>
120
+ Dict.fromArray([
121
+ ("capability", JSON.Encode.string(d.capability)),
122
+ ("component", JSON.Encode.string(d.component)),
123
+ ])->JSON.Encode.object
124
+
65
125
  // Deliberately NOT filtered by `isPublicQueryable`: that filter is the AutoUI
66
126
  // contract, and applying it here would silently drop Internal components from the
67
127
  // graph a developer tool draws.
@@ -129,4 +189,16 @@ let encodePluginStructureEntry = (~pluginId: string, def: pluginStructure): JSON
129
189
  ds->Array.map(encodeRequiredStoreDeclaration)->JSON.Encode.array
130
190
  ),
131
191
  ),
192
+ (
193
+ "requiredCapabilities",
194
+ def.requiredCapabilities->Option.mapOr(JSON.Encode.null, ds =>
195
+ ds->Array.map(encodeRequiredCapabilityDeclaration)->JSON.Encode.array
196
+ ),
197
+ ),
198
+ (
199
+ "traitDeclarations",
200
+ def.traitDeclarations->Option.mapOr(JSON.Encode.null, ds =>
201
+ ds->Array.map(encodeTraitDeclaration)->JSON.Encode.array
202
+ ),
203
+ ),
132
204
  ])->JSON.Encode.object
@@ -5,11 +5,36 @@ import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
5
5
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "./Platform_ComponentDefinitionsApi.res.mjs";
6
6
 
7
7
  let sdlTypes = [
8
- `type Platform_ExtensionPointDef {\n name: String!\n delegateNames: [String!]!\n sourceEventTypes: [String!]!\n commandTypes: [String!]\n}`,
8
+ `type Platform_PublishedEventDef {\n name: String!\n fromEventTypes: [String!]!\n}`,
9
+ `type Platform_AcceptedCommandDef {\n name: String!\n toCommandTypes: [String!]!\n}`,
10
+ `type Platform_ExtensionPointDef {\n name: String!\n delegateNames: [String!]!\n sourceEventTypes: [String!]!\n commandTypes: [String!]\n publishedEvents: [Platform_PublishedEventDef!]\n acceptedCommands: [Platform_AcceptedCommandDef!]\n}`,
9
11
  `type Platform_RequiredStoreDeclaration {\n store: String!\n component: String!\n field: String!\n annotation: String\n}`,
10
- `type Platform_PluginStructureEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n extensionPoints: [Platform_ExtensionPointDef!]\n requiredStores: [String!]\n requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]\n}`
12
+ `type Platform_RequiredCapabilityDeclaration {\n capability: String!\n component: String!\n}`,
13
+ `type Platform_TraitDeclaration {\n trait: String!\n version: String!\n posture: String!\n component: String!\n}`,
14
+ `type Platform_PluginStructureEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n extensionPoints: [Platform_ExtensionPointDef!]\n requiredStores: [String!]\n requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]\n requiredCapabilities: [Platform_RequiredCapabilityDeclaration!]\n traitDeclarations: [Platform_TraitDeclaration!]\n}`
11
15
  ];
12
16
 
17
+ function encodeTraitDeclaration(d) {
18
+ return Object.fromEntries([
19
+ [
20
+ "trait",
21
+ d.trait
22
+ ],
23
+ [
24
+ "version",
25
+ d.version
26
+ ],
27
+ [
28
+ "posture",
29
+ d.posture
30
+ ],
31
+ [
32
+ "component",
33
+ d.component
34
+ ]
35
+ ]);
36
+ }
37
+
13
38
  function encodeExtensionPointDef(e) {
14
39
  return Object.fromEntries([
15
40
  [
@@ -27,6 +52,32 @@ function encodeExtensionPointDef(e) {
27
52
  [
28
53
  "commandTypes",
29
54
  Stdlib_Option.mapOr(e.commandTypes, null, Platform_ComponentDefinitionsApi$ReventlessCore.encodeStrings)
55
+ ],
56
+ [
57
+ "publishedEvents",
58
+ Stdlib_Option.mapOr(e.publishedEvents, null, published => published.map(p => Object.fromEntries([
59
+ [
60
+ "name",
61
+ p.name
62
+ ],
63
+ [
64
+ "fromEventTypes",
65
+ Platform_ComponentDefinitionsApi$ReventlessCore.encodeStrings(p.fromEventTypes)
66
+ ]
67
+ ])))
68
+ ],
69
+ [
70
+ "acceptedCommands",
71
+ Stdlib_Option.mapOr(e.acceptedCommands, null, accepted => accepted.map(a => Object.fromEntries([
72
+ [
73
+ "name",
74
+ a.name
75
+ ],
76
+ [
77
+ "toCommandTypes",
78
+ Platform_ComponentDefinitionsApi$ReventlessCore.encodeStrings(a.toCommandTypes)
79
+ ]
80
+ ])))
30
81
  ]
31
82
  ]);
32
83
  }
@@ -52,6 +103,19 @@ function encodeRequiredStoreDeclaration(d) {
52
103
  ]);
53
104
  }
54
105
 
106
+ function encodeRequiredCapabilityDeclaration(d) {
107
+ return Object.fromEntries([
108
+ [
109
+ "capability",
110
+ d.capability
111
+ ],
112
+ [
113
+ "component",
114
+ d.component
115
+ ]
116
+ ]);
117
+ }
118
+
55
119
  function encodePluginStructureEntry(pluginId, def) {
56
120
  return Object.fromEntries([
57
121
  [
@@ -101,6 +165,14 @@ function encodePluginStructureEntry(pluginId, def) {
101
165
  [
102
166
  "requiredStoreDeclarations",
103
167
  Stdlib_Option.mapOr(def.requiredStoreDeclarations, null, ds => ds.map(encodeRequiredStoreDeclaration))
168
+ ],
169
+ [
170
+ "requiredCapabilities",
171
+ Stdlib_Option.mapOr(def.requiredCapabilities, null, ds => ds.map(encodeRequiredCapabilityDeclaration))
172
+ ],
173
+ [
174
+ "traitDeclarations",
175
+ Stdlib_Option.mapOr(def.traitDeclarations, null, ds => ds.map(encodeTraitDeclaration))
104
176
  ]
105
177
  ]);
106
178
  }
@@ -110,8 +182,10 @@ let sdlQueryField = ` Platform_PluginStructures: [Platform_PluginStructureEntry
110
182
  export {
111
183
  sdlTypes,
112
184
  sdlQueryField,
185
+ encodeTraitDeclaration,
113
186
  encodeExtensionPointDef,
114
187
  encodeRequiredStoreDeclaration,
188
+ encodeRequiredCapabilityDeclaration,
115
189
  encodePluginStructureEntry,
116
190
  }
117
191
  /* No side effect */
@@ -46,6 +46,12 @@ function commandAuthorization(param) {
46
46
  return "AllowAuthenticated";
47
47
  }
48
48
 
49
+ function commandTransition(param) {
50
+ return "Unrestricted";
51
+ }
52
+
53
+ let traits = [];
54
+
49
55
  let name = "UiFragmentRegistry";
50
56
 
51
57
  let Id;
@@ -66,5 +72,7 @@ export {
66
72
  moduleUrl,
67
73
  commandAuthorization,
68
74
  readConsistency,
75
+ commandTransition,
76
+ traits,
69
77
  }
70
78
  /* commandSchema Not a pure module */
@@ -3,11 +3,45 @@ open ReventlessInfra.Api
3
3
  let isNoApi = (commandSchema: S.t<unknown>): bool =>
4
4
  commandSchema->S.Metadata.get(~id=noApiId)->Option.getOr(false)
5
5
 
6
- let getExcludedVariants = (commandSchema: S.t<unknown>): option<Set.t<string>> =>
7
- commandSchema->S.Metadata.get(~id=noApiVariantsId)
6
+ // Variants excluded by a mark on the member itself. This is the half that
7
+ // survives a spread: splicing copies members, not the metadata of the union they
8
+ // came from, so a command a trait marked internal is only still recognisable here.
9
+ let excludedByMembers = (commandSchema: S.t<unknown>): array<string> =>
10
+ switch commandSchema {
11
+ | AnyOf({anyOf}) =>
12
+ anyOf->Array.filterMap(member =>
13
+ member->S.Metadata.get(~id=noApiVariantId)->Option.getOr(false)
14
+ ? variantNameOf(member)
15
+ : None
16
+ )
17
+ | _ => []
18
+ }
19
+
20
+ /**
21
+ Every variant excluded from the API, from both records of the fact.
22
+
23
+ The union's own set covers variants the schema's author declared; the members'
24
+ marks cover variants spliced in from another type, whose author's declaration is
25
+ in a file this one never mentions. A reader that consulted only the first
26
+ published someone else's internal commands.
27
+
28
+ `None` still means "nothing is excluded", so a caller's existing branch is
29
+ unchanged.
30
+ */
31
+ let getExcludedVariants = (commandSchema: S.t<unknown>): option<Set.t<string>> => {
32
+ let fromMembers = excludedByMembers(commandSchema)
33
+ switch (commandSchema->S.Metadata.get(~id=noApiVariantsId), fromMembers) {
34
+ | (None, []) => None
35
+ | (None, members) => Some(Set.fromArray(members))
36
+ | (Some(declared), []) => Some(declared)
37
+ | (Some(declared), members) =>
38
+ let all = declared->Set.toArray->Array.concat(members)
39
+ Some(Set.fromArray(all))
40
+ }
41
+ }
8
42
 
9
43
  let filterNoApiVariants = (fieldNames: array<string>, commandSchema: S.t<unknown>): array<string> =>
10
- switch commandSchema->S.Metadata.get(~id=noApiVariantsId) {
44
+ switch getExcludedVariants(commandSchema) {
11
45
  | None => fieldNames
12
46
  | Some(excluded) => fieldNames->Array.filter(name => !(excluded->Set.has(name)))
13
47
  }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
6
7
 
@@ -8,12 +9,37 @@ function isNoApi(commandSchema) {
8
9
  return Stdlib_Option.getOr(Sury.$Metadata_get(commandSchema, Api$ReventlessInfra.noApiId), false);
9
10
  }
10
11
 
12
+ function excludedByMembers(commandSchema) {
13
+ if (commandSchema.type === "anyOf") {
14
+ return Stdlib_Array.filterMap(commandSchema.anyOf, member => {
15
+ if (Stdlib_Option.getOr(Sury.$Metadata_get(member, Api$ReventlessInfra.noApiVariantId), false)) {
16
+ return Api$ReventlessInfra.variantNameOf(member);
17
+ }
18
+ });
19
+ } else {
20
+ return [];
21
+ }
22
+ }
23
+
11
24
  function getExcludedVariants(commandSchema) {
12
- return Sury.$Metadata_get(commandSchema, Api$ReventlessInfra.noApiVariantsId);
25
+ let fromMembers = excludedByMembers(commandSchema);
26
+ let match = Sury.$Metadata_get(commandSchema, Api$ReventlessInfra.noApiVariantsId);
27
+ if (match === undefined) {
28
+ if (fromMembers.length !== 0) {
29
+ return new Set(fromMembers);
30
+ } else {
31
+ return;
32
+ }
33
+ }
34
+ if (fromMembers.length === 0) {
35
+ return match;
36
+ }
37
+ let all = Array.from(match).concat(fromMembers);
38
+ return new Set(all);
13
39
  }
14
40
 
15
41
  function filterNoApiVariants(fieldNames, commandSchema) {
16
- let excluded = Sury.$Metadata_get(commandSchema, Api$ReventlessInfra.noApiVariantsId);
42
+ let excluded = getExcludedVariants(commandSchema);
17
43
  if (excluded !== undefined) {
18
44
  return fieldNames.filter(name => !excluded.has(name));
19
45
  } else {
@@ -23,6 +49,7 @@ function filterNoApiVariants(fieldNames, commandSchema) {
23
49
 
24
50
  export {
25
51
  isNoApi,
52
+ excludedByMembers,
26
53
  getExcludedVariants,
27
54
  filterNoApiVariants,
28
55
  }
@@ -169,19 +169,19 @@ module Make = (
169
169
  // Per-source erased dispatch — pre-compile decoders once at module init.
170
170
  type dispatch = {
171
171
  sourceName: string,
172
- handle: (JSON.t, Reventless.AutomationSlice.context) => unit,
172
+ handle: (JSON.t, ~sourceId: string, Reventless.AutomationSlice.context) => unit,
173
173
  }
174
174
 
175
175
  let dispatches: array<dispatch> = Automation.mappings->Array.map((
176
176
  module(M: Automation.Mapping),
177
177
  ) => {
178
178
  let decoder = Reventless.DcbDecode.makeDecoder(M.sourceEventSchema)
179
- let handle = (json: JSON.t, ctx: Reventless.AutomationSlice.context) => {
179
+ let handle = (json: JSON.t, ~sourceId: string, ctx: Reventless.AutomationSlice.context) => {
180
180
  let (eventType, dataDict) = json->Message.splitMessage
181
181
  switch decoder.decode(~eventType, ~data=dataDict) {
182
182
  | Some(event) =>
183
183
  // Collect — append new items keyed by ID; first writer wins (idempotent).
184
- M.collect(event, ctx)->Array.forEach(((id, item)) => {
184
+ M.collect(event, ~sourceId, ctx)->Array.forEach(((id, item)) => {
185
185
  switch todoItems->Dict.get(id) {
186
186
  | Some(_) => () // Already exists — skip
187
187
  | None =>
@@ -225,7 +225,7 @@ module Make = (
225
225
  // Multiple mappings can share a sourceName; all matching dispatches run.
226
226
  dispatches->Array.forEach(d => {
227
227
  if d.sourceName == sourceName {
228
- d.handle(eventPayload, ctx)
228
+ d.handle(eventPayload, ~sourceId=context.id, ctx)
229
229
  }
230
230
  })
231
231
  })
@@ -57,14 +57,14 @@ function Make(Spec) {
57
57
  };
58
58
  let dispatches = Automation.mappings.map(M => {
59
59
  let decoder = DcbDecode$Reventless.makeDecoder(M.sourceEventSchema);
60
- let handle = (json, ctx) => {
60
+ let handle = (json, sourceId, ctx) => {
61
61
  let match = Message$ReventlessCore.splitMessage(json);
62
62
  let event = decoder.decode(match[0], match[1]);
63
63
  if (event === undefined) {
64
64
  return;
65
65
  }
66
66
  let event$1 = Primitive_option.valFromOption(event);
67
- M.collect(event$1, ctx).forEach(param => {
67
+ M.collect(event$1, sourceId, ctx).forEach(param => {
68
68
  let id = param[0];
69
69
  let match = todoItems[id];
70
70
  if (match !== undefined) {
@@ -105,7 +105,7 @@ function Make(Spec) {
105
105
  let eventPayload = dict !== undefined ? Stdlib_Option.getOr(dict["event"], json) : json;
106
106
  dispatches.forEach(d => {
107
107
  if (d.sourceName === sourceName) {
108
- return d.handle(eventPayload, ctx);
108
+ return d.handle(eventPayload, context.id, ctx);
109
109
  }
110
110
  });
111
111
  });