@reventlessdev/reventless-core 3.0.0-alpha.180 → 3.0.0-alpha.181

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 (115) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +7 -7
  3. package/src/Message.res +2 -1
  4. package/src/Message.res.mjs +5 -1
  5. package/src/admin/Platform_Admin_Structure.res +2 -0
  6. package/src/admin/Platform_Admin_Structure.res.mjs +2 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  9. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res +1 -1
  10. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs +2 -1
  11. package/src/components/Api/ApiTargetStateHelpers.res +20 -0
  12. package/src/components/Api/ApiTargetStateHelpers.res.mjs +16 -0
  13. package/src/components/Api/GraphQL_FragmentGenerator.res +1 -0
  14. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +1 -0
  15. package/src/components/Api/SchemaType.res +3 -0
  16. package/src/components/Api/SchemaType.res.mjs +10 -2
  17. package/src/components/Api/SuryToJsonSchema.res +16 -0
  18. package/src/components/Api/SuryToJsonSchema.res.mjs +28 -0
  19. package/src/components/Dcb/Dcb_Builder.res +4 -5
  20. package/src/components/Dcb/Dcb_Builder.res.mjs +3 -5
  21. package/src/components/DcbEventLog/DcbEventLog_Operations.res +6 -1
  22. package/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs +2 -1
  23. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +25 -0
  24. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res.mjs +31 -0
  25. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +8 -8
  26. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +26 -6
  27. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +44 -37
  28. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +17 -25
  29. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +27 -2
  30. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -0
  31. package/src/components/StateViewSlice/StateViewSlice_Builder.res +20 -6
  32. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +23 -5
  33. package/src/components/StateViewSlice/StateViewSlice_Callback.res +1 -1
  34. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +5 -1
  35. package/src/plugin/component/Plugin_Helpers.res +4 -1
  36. package/src/plugin/component/Plugin_Structure.res +15 -1
  37. package/src/plugin/component/Plugin_Structure.res.mjs +4 -1
  38. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +2 -0
  39. package/tests/IdentityTest.res.mjs +174 -0
  40. package/tests/ResourceAttributionTest.res.mjs +75 -0
  41. package/tests/adapter/MonitoringTest.res.mjs +116 -0
  42. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
  43. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +365 -0
  44. package/tests/admin/Platform_UIFragmentsApiTest.res.mjs +101 -0
  45. package/tests/aggregate/AggregateCacheTest.res.mjs +433 -0
  46. package/tests/aggregate/AggregateCallbackTest.res.mjs +143 -0
  47. package/tests/aggregate/AggregateCausationTest.res.mjs +47 -0
  48. package/tests/aggregate/AggregateConflictTest.res.mjs +386 -0
  49. package/tests/aggregate/AggregateFixtures.res.mjs +308 -0
  50. package/tests/aggregate/AggregateRejectionTest.res.mjs +166 -0
  51. package/tests/aggregate/AggregateSnapshotTest.res.mjs +645 -0
  52. package/tests/api/ApiNoApiTest.res.mjs +90 -0
  53. package/tests/api/GraphQL_StitcherTest.res.mjs +112 -0
  54. package/tests/api/SimpleTest.res.mjs +10 -0
  55. package/tests/api/SuryToJsonSchemaTest.res +68 -0
  56. package/tests/api/SuryToJsonSchemaTest.res.mjs +843 -0
  57. package/tests/commandgenerator/CommandGeneratorCallbackTest.res.mjs +160 -0
  58. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +197 -0
  59. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +152 -0
  60. package/tests/commandtopic/CommandTopicCallbackTest.res.mjs +106 -0
  61. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +109 -0
  62. package/tests/counter/CounterCallbackTest.res.mjs +140 -0
  63. package/tests/counter/CounterFixtures.res.mjs +67 -0
  64. package/tests/dcb/DcbDecodeTest.res.mjs +194 -0
  65. package/tests/dcb/DcbEventLogOperationsTest.res.mjs +268 -0
  66. package/tests/dcb/DcbFixtures.res.mjs +534 -0
  67. package/tests/dcb/DcbScopeInferenceTest.res.mjs +480 -0
  68. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +540 -0
  69. package/tests/dcb/DcbTagTest.res.mjs +867 -0
  70. package/tests/dcb/DcbValidationTest.res.mjs +476 -0
  71. package/tests/eventlog/EventLogFixtures.res.mjs +196 -0
  72. package/tests/eventlog/EventLogOperationsTest.res.mjs +192 -0
  73. package/tests/eventlog/EventLogRetryTest.res.mjs +195 -0
  74. package/tests/eventmapper/EventMapperCallbackTest.res.mjs +141 -0
  75. package/tests/eventmapper/EventMapperFixtures.res.mjs +440 -0
  76. package/tests/eventmapper/EventMapperTest.res.mjs +83 -0
  77. package/tests/eventtopic/EventTopicFixtures.res.mjs +65 -0
  78. package/tests/eventtopic/EventTopicOperationsTest.res.mjs +71 -0
  79. package/tests/extensionpoint/ExtensionPointCallbackTest.res.mjs +94 -0
  80. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +205 -0
  81. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +304 -0
  82. package/tests/extensionpoint/PluginDisconnectScheduleTest.res.mjs +92 -0
  83. package/tests/ftp/FTPTest.res.mjs +88 -0
  84. package/tests/logger/LogFormatTest.res.mjs +329 -0
  85. package/tests/logger/LoggerLazyTest.res.mjs +40 -0
  86. package/tests/message/MessageTest.res.mjs +218 -0
  87. package/tests/message/MetaEnvelopeTest.res.mjs +188 -0
  88. package/tests/plugin/HeartbeatDisconnectGraceTest.res.mjs +91 -0
  89. package/tests/plugin/ManifestVisibilityTest.res.mjs +215 -0
  90. package/tests/plugin/PluginSpecExperiment.res.mjs +31 -0
  91. package/tests/plugin/PluginStructureTest.res +55 -0
  92. package/tests/plugin/PluginStructureTest.res.mjs +467 -0
  93. package/tests/plugin/PluginVersionTest.res.mjs +30 -0
  94. package/tests/plugin/SchemaWalkerTest.res.mjs +160 -0
  95. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +62 -0
  96. package/tests/plugin/StateChangeSlice/PsShipOrder.res +4 -1
  97. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +77 -0
  98. package/tests/plugin/StateViewSlice/PsAnnotatedView.res +5 -4
  99. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +110 -0
  100. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res +1 -1
  101. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res.mjs +70 -0
  102. package/tests/plugin/StateViewSlice/PsCustomersView.res +1 -1
  103. package/tests/plugin/StateViewSlice/PsCustomersView.res.mjs +70 -0
  104. package/tests/plugin/StateViewSlice/PsOrdersView.res +1 -1
  105. package/tests/plugin/StateViewSlice/PsOrdersView.res.mjs +82 -0
  106. package/tests/poc/StructuralSubtypingPoc.res.mjs +61 -0
  107. package/tests/projection/ProjectionOptimizeTest.res.mjs +75 -0
  108. package/tests/querydb/QueryDbFixtures.res.mjs +135 -0
  109. package/tests/querydb/QueryDbOperationsTest.res.mjs +168 -0
  110. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res.mjs +77 -0
  111. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +139 -0
  112. package/tests/util/CommandPublisherTest.res.mjs +78 -0
  113. package/tests/util/CsvStreamTest.res.mjs +51 -0
  114. package/tests/util/LruTest.res.mjs +105 -0
  115. package/tests/util/MetricsTest.res.mjs +53 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
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.181 (2026-07-26)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **auto-ui:** don't hand a [@no](https://github.com/no)Api command variant a sibling's mutation field ([2640465](https://github.com/ReventlessDev/reventless-core/commit/264046509c5f5fd7f00f5d04ab977bbc2008098c)), closes [#2](https://github.com/ReventlessDev/reventless-core/issues/2)
11
+ * **core:** decode the event envelope in OutboundTranslationSlice ([66008a4](https://github.com/ReventlessDev/reventless-core/commit/66008a4dff411c85470f60aac5424e7b3eda6f01))
12
+ * **graphql:** give InboundTranslationSlice mutations a resolvable result type ([381b545](https://github.com/ReventlessDev/reventless-core/commit/381b5458c16797404cb8ed95fa853fb1e1ca4199))
13
+ ### Features
14
+
15
+ * **auto-ui:** declare command target state via [@target](https://github.com/target)State ([5fc0374](https://github.com/ReventlessDev/reventless-core/commit/5fc03741a8816c57085b86a4ad7d595e3b690193)), closes [#5](https://github.com/ReventlessDev/reventless-core/issues/5)
16
+ * **auto-ui:** declare field semantics + dashboard metrics ([@semantic](https://github.com/semantic), [@metric](https://github.com/metric)) ([d74ff77](https://github.com/ReventlessDev/reventless-core/commit/d74ff7721e18e8638a82931a370a549b304dac94)), closes [#4](https://github.com/ReventlessDev/reventless-core/issues/4)
17
+ * **dcb:** emit decision-model cache hit/miss + delta metrics ([967fb7f](https://github.com/ReventlessDev/reventless-core/commit/967fb7fb42f00d22b9dd6f6261e2f5308a6e9516))
18
+
19
+
6
20
  # 3.0.0-alpha.180 (2026-07-24)
7
21
 
8
22
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.180",
3
+ "version": "3.0.0-alpha.181",
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-alpha.4",
30
30
  "uuid": "^13.0.0",
31
- "@reventlessdev/rescript-effect": "0.1.0-alpha.30",
32
- "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
33
31
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
34
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
33
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
35
34
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
35
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
37
37
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
38
+ "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.105",
38
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.104",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.80",
41
- "@reventlessdev/rescript-uuid": "1.1.0-alpha.17"
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.81"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-alpha.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.54"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.56"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
package/src/Message.res CHANGED
@@ -257,10 +257,11 @@ let decomposeMeta = meta =>
257
257
  ->Option.getOrThrow
258
258
  ->Dict.toArray
259
259
 
260
- let composeEventJson' = (id, meta, eventJson) =>
260
+ let composeEventJson' = (id, meta, ~recordedAt, eventJson) =>
261
261
  [
262
262
  ("id", id->JSON.Encode.string),
263
263
  ("meta", meta->S.reverseConvertToJsonOrThrow(metaSchema)),
264
+ ("recordedAt", recordedAt->JSON.Encode.string),
264
265
  ("event", eventJson),
265
266
  ]
266
267
  ->Dict.fromArray
@@ -203,7 +203,7 @@ function decomposeMeta(meta) {
203
203
  return Object.entries(Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(S.reverseConvertToJsonOrThrow(meta, Message$Reventless.metaSchema)), undefined));
204
204
  }
205
205
 
206
- function composeEventJson$p(id, meta, eventJson) {
206
+ function composeEventJson$p(id, meta, recordedAt, eventJson) {
207
207
  return Object.fromEntries([
208
208
  [
209
209
  "id",
@@ -213,6 +213,10 @@ function composeEventJson$p(id, meta, eventJson) {
213
213
  "meta",
214
214
  S.reverseConvertToJsonOrThrow(meta, Message$Reventless.metaSchema)
215
215
  ],
216
+ [
217
+ "recordedAt",
218
+ recordedAt
219
+ ],
216
220
  [
217
221
  "event",
218
222
  eventJson
@@ -74,6 +74,7 @@ let activateCommand: commandDef = {
74
74
  mutationField: Api_Naming.adminField(~name="Plugin_Activate"),
75
75
  references: [],
76
76
  allowedStates: Some(["Inactive"]),
77
+ targetState: None,
77
78
  apiExposed: Some(true),
78
79
  }
79
80
 
@@ -85,6 +86,7 @@ let deactivateCommand: commandDef = {
85
86
  mutationField: Api_Naming.adminField(~name="Plugin_Deactivate"),
86
87
  references: [],
87
88
  allowedStates: Some(["Connected", "Disconnected"]),
89
+ targetState: None,
88
90
  apiExposed: Some(true),
89
91
  }
90
92
 
@@ -61,6 +61,7 @@ let activateCommand = {
61
61
  mutationField: activateCommand_mutationField,
62
62
  references: activateCommand_references,
63
63
  allowedStates: activateCommand_allowedStates,
64
+ targetState: undefined,
64
65
  apiExposed: activateCommand_apiExposed
65
66
  };
66
67
 
@@ -87,6 +88,7 @@ let deactivateCommand = {
87
88
  mutationField: deactivateCommand_mutationField,
88
89
  references: deactivateCommand_references,
89
90
  allowedStates: deactivateCommand_allowedStates,
91
+ targetState: undefined,
90
92
  apiExposed: deactivateCommand_apiExposed
91
93
  };
92
94
 
@@ -8,7 +8,7 @@ open Reventless.Plugin
8
8
 
9
9
  let sdlTypes: array<string> = [
10
10
  `type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
11
- `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n apiExposed: Boolean\n}`,
11
+ `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
12
12
  `type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
13
13
  `type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n}`,
14
14
  `type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n statusField: String\n}`,
@@ -54,6 +54,7 @@ let encodeCommandDef = (c: commandDef): JSON.t =>
54
54
  "allowedStates",
55
55
  c.allowedStates->Option.mapOr(JSON.Encode.null, encodeStrings),
56
56
  ),
57
+ ("targetState", c.targetState->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
57
58
  ("apiExposed", c.apiExposed->Option.mapOr(JSON.Encode.null, JSON.Encode.bool)),
58
59
  ])->JSON.Encode.object
59
60
 
@@ -6,7 +6,7 @@ import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
6
6
 
7
7
  let sdlTypes = [
8
8
  `type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
9
- `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n apiExposed: Boolean\n}`,
9
+ `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
10
10
  `type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
11
11
  `type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n}`,
12
12
  `type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n statusField: String\n}`,
@@ -71,6 +71,10 @@ function encodeCommandDef(c) {
71
71
  "allowedStates",
72
72
  Stdlib_Option.mapOr(c.allowedStates, null, encodeStrings)
73
73
  ],
74
+ [
75
+ "targetState",
76
+ Stdlib_Option.mapOr(c.targetState, null, prim => prim)
77
+ ],
74
78
  [
75
79
  "apiExposed",
76
80
  Stdlib_Option.mapOr(c.apiExposed, null, prim => prim)
@@ -3,7 +3,7 @@
3
3
  // No `open Reventless.Plugin`: it would shadow the state labels (panels/pages/...) brought in
4
4
  // by the projection's spec open. `manifest.<field>` resolves type-directed from the event's
5
5
  // uiFragmentManifest payload.
6
- let project = event =>
6
+ let project = ({event}) =>
7
7
  switch event {
8
8
  | UiFragmentRegistered({pluginId, manifest, at}) => [
9
9
  Set(
@@ -1,7 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
 
4
- function project(event) {
4
+ function project(param) {
5
+ let event = param.event;
5
6
  switch (event.TAG) {
6
7
  case "UiFragmentRegistered" :
7
8
  let at = event.at;
@@ -0,0 +1,20 @@
1
+ // Read per-variant `targetState` metadata that the reventless-ppx
2
+ // `@targetState("…")` attribute attaches to a command schema. Consumed by
3
+ // `Plugin_Structure.toCommandDef` when building the `commandDef` records that
4
+ // land in `Platform_ComponentDefinitions`, so AutoUI's board drag resolver can
5
+ // move a row by a *declared* transition instead of a name-stem guess. Mirrors
6
+ // `ApiAllowedStatesHelpers.getAllowedStates`.
7
+
8
+ open ReventlessInfra.Api
9
+
10
+ /** Returns the declared target state for a given command-variant name, or None
11
+ when the variant has no `@targetState` annotation (back-compat default —
12
+ the resolver falls back to its name-stem heuristic). */
13
+ let getTargetState = (
14
+ commandSchema: S.t<unknown>,
15
+ ~variantName: string,
16
+ ): option<string> =>
17
+ switch commandSchema->S.Metadata.get(~id=targetStateId) {
18
+ | None => None
19
+ | Some(entries) => entries->Dict.get(variantName)
20
+ }
@@ -0,0 +1,16 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
5
+
6
+ function getTargetState(commandSchema, variantName) {
7
+ let entries = S.Metadata.get(commandSchema, Api$ReventlessInfra.targetStateId);
8
+ if (entries !== undefined) {
9
+ return entries[variantName];
10
+ }
11
+ }
12
+
13
+ export {
14
+ getTargetState,
15
+ }
16
+ /* S Not a pure module */
@@ -43,6 +43,7 @@ let rec fromSchemaType = (
43
43
  | ScalarBoolean => `Boolean${bang}`
44
44
  | ScalarBigInt => `String${bang}`
45
45
  | EntityId => `ID${bang}`
46
+ | DateTime => `String${bang}`
46
47
  | Nullable(inner) => fromSchemaType(~required=false, ~asInput, inner, collectedTypes, seenTypes)
47
48
  | ArrayOf(item) =>
48
49
  let itemType = fromSchemaType(~required=true, ~asInput, item, collectedTypes, seenTypes)
@@ -41,6 +41,7 @@ function fromSchemaType(_required, _asInputOpt, _st, collectedTypes, seenTypes)
41
41
  return `ID` + bang;
42
42
  case "ScalarString" :
43
43
  case "ScalarBigInt" :
44
+ case "DateTime" :
44
45
  case "Unknown" :
45
46
  return `String` + bang;
46
47
  }
@@ -4,6 +4,7 @@ type rec schemaType =
4
4
  | ScalarBoolean
5
5
  | ScalarBigInt
6
6
  | EntityId
7
+ | DateTime
7
8
  | Nullable(schemaType)
8
9
  | ArrayOf(schemaType)
9
10
  | ObjectRef(string, dict<schemaType>)
@@ -11,6 +12,7 @@ type rec schemaType =
11
12
  | Unknown
12
13
 
13
14
  let isTagged = Reventless.DcbTag.isTagged
15
+ let isDateTime = Reventless.DateTime.isDateTime
14
16
 
15
17
  let isIdFieldName = (name: string): bool => {
16
18
  let lower = String.toLowerCase(name)
@@ -30,6 +32,7 @@ let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown
30
32
  } else {
31
33
  switch schema {
32
34
  | String({const: ?Some(_)}) => ScalarString
35
+ | String(_) if isDateTime(schema) => DateTime
33
36
  | String(_) => isIdFieldName(fieldName) ? EntityId : ScalarString
34
37
  | Number(_) => ScalarNumber
35
38
  | Boolean(_) => ScalarBoolean
@@ -3,6 +3,7 @@
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
6
+ import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
6
7
  import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
7
8
 
8
9
  function isIdFieldName(name) {
@@ -31,10 +32,14 @@ function fromSury(parentName, fieldName, schema) {
31
32
  }
32
33
  switch (schema.type) {
33
34
  case "string" :
34
- if (schema.const !== undefined || !isIdFieldName(fieldName)) {
35
+ if (schema.const !== undefined) {
35
36
  return "ScalarString";
36
- } else {
37
+ } else if (DateTime$Reventless.isDateTime(schema)) {
38
+ return "DateTime";
39
+ } else if (isIdFieldName(fieldName)) {
37
40
  return "EntityId";
41
+ } else {
42
+ return "ScalarString";
38
43
  }
39
44
  case "number" :
40
45
  return "ScalarNumber";
@@ -133,8 +138,11 @@ function fromSuryObject(typeName, schema) {
133
138
 
134
139
  let isTagged = DcbTag$Reventless.isTagged;
135
140
 
141
+ let isDateTime = DateTime$Reventless.isDateTime;
142
+
136
143
  export {
137
144
  isTagged,
145
+ isDateTime,
138
146
  isIdFieldName,
139
147
  isIdsFieldName,
140
148
  fromSury,
@@ -68,6 +68,21 @@ let mergeAnnotations = (
68
68
  if spec.scanSort->Array.includes(fieldName) {
69
69
  obj->Dict.set("x-reventless-scanSort", JSON.Encode.bool(true))
70
70
  }
71
+ switch spec.semantic->Array.find(((field, _)) => field === fieldName) {
72
+ | Some((_, semanticId)) =>
73
+ obj->Dict.set("x-reventless-semantic", JSON.Encode.string(semanticId))
74
+ | None => ()
75
+ }
76
+ switch spec.metric->Array.find(((field, _)) => field === fieldName) {
77
+ | Some((_, m)) =>
78
+ // `x-reventless-metric: {aggregate, label}`. Omit an empty label so the
79
+ // UI derives one from the field name.
80
+ let entries = [("aggregate", JSON.Encode.string(m.aggregate))]
81
+ let entries =
82
+ m.label === "" ? entries : Array.concat(entries, [("label", JSON.Encode.string(m.label))])
83
+ obj->Dict.set("x-reventless-metric", JSON.Encode.object(Dict.fromArray(entries)))
84
+ | None => ()
85
+ }
71
86
  JSON.Encode.object(obj)
72
87
  }
73
88
 
@@ -80,6 +95,7 @@ let rec fromSchemaType = (st: SchemaType.schemaType): JSON.t =>
80
95
  | ScalarBoolean => jsonObject([("type", str("boolean"))])
81
96
  | ScalarBigInt => jsonObject([("type", str("integer"))])
82
97
  | EntityId => jsonObject([("type", str("string")), ("format", str("uuid"))])
98
+ | DateTime => jsonObject([("type", str("string")), ("format", str("date-time"))])
83
99
  | Nullable(inner) =>
84
100
  let innerSchema = fromSchemaType(inner)
85
101
  jsonObject([("oneOf", JSON.Encode.array([innerSchema, jsonObject([("type", str("null"))])]))])
@@ -62,6 +62,23 @@ function mergeAnnotations(fieldSchema, fieldName, spec) {
62
62
  if (spec.scanSort.includes(fieldName)) {
63
63
  obj["x-reventless-scanSort"] = true;
64
64
  }
65
+ let match$3 = spec.semantic.find(param => param[0] === fieldName);
66
+ if (match$3 !== undefined) {
67
+ obj["x-reventless-semantic"] = match$3[1];
68
+ }
69
+ let match$4 = spec.metric.find(param => param[0] === fieldName);
70
+ if (match$4 !== undefined) {
71
+ let m = match$4[1];
72
+ let entries = [[
73
+ "aggregate",
74
+ m.aggregate
75
+ ]];
76
+ let entries$1 = m.label === "" ? entries : entries.concat([[
77
+ "label",
78
+ m.label
79
+ ]]);
80
+ obj["x-reventless-metric"] = Object.fromEntries(entries$1);
81
+ }
65
82
  return obj;
66
83
  }
67
84
 
@@ -94,6 +111,17 @@ function fromSchemaType(st) {
94
111
  "uuid"
95
112
  ]
96
113
  ]);
114
+ case "DateTime" :
115
+ return Object.fromEntries([
116
+ [
117
+ "type",
118
+ "string"
119
+ ],
120
+ [
121
+ "format",
122
+ "date-time"
123
+ ]
124
+ ]);
97
125
  case "ScalarString" :
98
126
  case "Unknown" :
99
127
  return Object.fromEntries([[
@@ -718,11 +718,10 @@ module Make = (
718
718
  | Some(receiveFn) =>
719
719
  Effect.promise(async () => {
720
720
  let result = await receiveFn(args)
721
- let response = switch result {
722
- | Ok(targetIds) =>
723
- targetIds->Array.map(JSON.Encode.string)->JSON.Encode.array
724
- | Error(msg) => msg->JSON.Encode.string
725
- }
721
+ let response =
722
+ result
723
+ ->InboundTranslationSlice.receiveResultToOutcome
724
+ ->CommandTopic.commandOutcomeToJson
726
725
  response->Obj.magic
727
726
  })
728
727
  | None => baseHandler(event, ctx)
@@ -15,6 +15,7 @@ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
15
15
  import * as Component$ReventlessCore from "../Component.res.mjs";
16
16
  import * as DcbValidation$Reventless from "@reventlessdev/reventless-spec/src/components/DcbValidation.res.mjs";
17
17
  import * as Api_Naming$ReventlessCore from "../Api/Api_Naming.res.mjs";
18
+ import * as CommandTopic$ReventlessCore from "../CommandTopic/CommandTopic.res.mjs";
18
19
  import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
19
20
  import * as RuntimeHints$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/RuntimeHints.res.mjs";
20
21
  import * as Plugin_Helpers$ReventlessCore from "../../plugin/component/Plugin_Helpers.res.mjs";
@@ -22,6 +23,7 @@ import * as ApiNoApiHelpers$ReventlessCore from "../Api/ApiNoApiHelpers.res.mjs"
22
23
  import * as Plugin_Structure$ReventlessCore from "../../plugin/component/Plugin_Structure.res.mjs";
23
24
  import * as DcbEventLog_Builder$ReventlessCore from "../DcbEventLog/DcbEventLog_Builder.res.mjs";
24
25
  import * as CommandTopic_Builder$ReventlessCore from "../CommandTopic/CommandTopic_Builder.res.mjs";
26
+ import * as InboundTranslationSlice$ReventlessCore from "../InboundTranslationSlice/InboundTranslationSlice.res.mjs";
25
27
  import * as AutomationSlice_Callback$ReventlessCore from "../AutomationSlice/AutomationSlice_Callback.res.mjs";
26
28
  import * as CommandGenerator_Callback$ReventlessCore from "../CommandGenerator/CommandGenerator_Callback.res.mjs";
27
29
  import * as InboundTranslationSlice_Callback$ReventlessCore from "../InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
@@ -382,11 +384,7 @@ function Make(DcbEventLogStorage) {
382
384
  if (receiveFn !== undefined) {
383
385
  return Effect.promise(async () => {
384
386
  let result = await receiveFn(args);
385
- if (result.TAG === "Ok") {
386
- return result._0.map(prim => prim);
387
- } else {
388
- return result._0;
389
- }
387
+ return CommandTopic$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice$ReventlessCore.receiveResultToOutcome(result));
390
388
  });
391
389
  } else {
392
390
  return baseHandler(event, ctx);
@@ -68,11 +68,16 @@ module Make = (Ops: Ops): T => {
68
68
  }
69
69
  }
70
70
 
71
+ // Storage stamped its own `recordedAt` at append but does not return it, so
72
+ // the topic envelope carries a publish-time stamp instead (a few ms later).
73
+ // StateViewSlice projections read this as `consumed.recordedAt`; the stored
74
+ // column remains authoritative for reads. Producer time is `meta.time`.
75
+ let recordedAt = Message.nowAsISOString()
71
76
  let _ = await Array.zip(rawEvents, finalRawEventsJson)
72
77
  ->Array.map(async ((rawEvent, eventJson)) => {
73
78
  let entityId =
74
79
  rawEvent.tags->Array.get(0)->Option.map(t => t.value)->Option.getOr(name)
75
- let eventJson' = Message.composeEventJson'(entityId, rawEvent.meta, eventJson)
80
+ let eventJson' = Message.composeEventJson'(entityId, rawEvent.meta, ~recordedAt, eventJson)
76
81
  try await Ops.publishJson(serviceName, rawEvent.meta, eventJson') catch {
77
82
  | JsExn(err) =>
78
83
  let errMsg = err->JsExn.message->Option.getOr("unknown")
@@ -41,10 +41,11 @@ function Make(Ops) {
41
41
  } else {
42
42
  finalRawEventsJson = rawEventsJson;
43
43
  }
44
+ let recordedAt = Message$ReventlessCore.nowAsISOString();
44
45
  await Promise.all(Stdlib_Array.zip(rawEvents, finalRawEventsJson).map(async param => {
45
46
  let rawEvent = param[0];
46
47
  let entityId = Stdlib_Option.getOr(Stdlib_Option.map(rawEvent.tags[0], t => t.value), name);
47
- let eventJson$p = Message$ReventlessCore.composeEventJson$p(entityId, rawEvent.meta, param[1]);
48
+ let eventJson$p = Message$ReventlessCore.composeEventJson$p(entityId, rawEvent.meta, recordedAt, param[1]);
48
49
  try {
49
50
  return await Ops.publishJson(serviceName, rawEvent.meta, eventJson$p);
50
51
  } catch (raw_err) {
@@ -3,8 +3,33 @@ let componentType = ComponentType.InboundTranslationSlice
3
3
  type t = ReventlessInfra.InboundTranslationSlice.t
4
4
  type outputs = ReventlessInfra.InboundTranslationSlice.outputs
5
5
  type operations = ReventlessInfra.InboundTranslationSlice.operations
6
+ type acceptedResult = ReventlessInfra.InboundTranslationSlice.acceptedResult
7
+ type rejectedResult = ReventlessInfra.InboundTranslationSlice.rejectedResult
8
+ type receiveResult = ReventlessInfra.InboundTranslationSlice.receiveResult
6
9
  type component = Component.t<t, outputs, operations>
7
10
 
11
+ /**
12
+ Map a `receive` outcome onto the `CommandResult` union the slice's mutation field
13
+ declares, so both surfaces feed `CommandTopic.commandOutcomeToJson` rather than
14
+ hand-rolling a second `__typename` writer.
15
+
16
+ A translation can fan out across several targets; the mutation response reports
17
+ the first target as `entityId` and the fan-out count as `eventCount`. The
18
+ per-target detail stays queryable through the slice's audit read model. When the
19
+ translation legitimately produced no command, `entityId` is omitted (encoding as
20
+ `null`) rather than reporting a target that does not exist.
21
+ */
22
+ let receiveResultToOutcome = (result: receiveResult): CommandTopic.commandOutcome =>
23
+ switch result {
24
+ | Ok({requestId, targetIds, commandCount}) =>
25
+ switch targetIds->Array.get(0) {
26
+ | Some(entityId) => Accepted({msgId: requestId, entityId, eventCount: commandCount})
27
+ | None => Accepted({msgId: requestId, eventCount: commandCount})
28
+ }
29
+ | Error({requestId, error}) =>
30
+ Rejected({msgId: requestId, errorCode: "TranslationFailed", errorDetail: Some(error)})
31
+ }
32
+
8
33
  let toResolvedOutputs = (
9
34
  outputs: outputs,
10
35
  ): Pulumi.Output.t<ReventlessInterop.InboundTranslationSlice.resolvedOutputs> =>
@@ -3,6 +3,36 @@
3
3
  import * as Pulumi from "@pulumi/pulumi";
4
4
  import * as Adapter$ReventlessCore from "../../adapter/Adapter.res.mjs";
5
5
 
6
+ function receiveResultToOutcome(result) {
7
+ if (result.TAG === "Ok") {
8
+ let match = result._0;
9
+ let commandCount = match.commandCount;
10
+ let requestId = match.requestId;
11
+ let entityId = match.targetIds[0];
12
+ if (entityId !== undefined) {
13
+ return {
14
+ TAG: "Accepted",
15
+ msgId: requestId,
16
+ entityId: entityId,
17
+ eventCount: commandCount
18
+ };
19
+ } else {
20
+ return {
21
+ TAG: "Accepted",
22
+ msgId: requestId,
23
+ eventCount: commandCount
24
+ };
25
+ }
26
+ }
27
+ let match$1 = result._0;
28
+ return {
29
+ TAG: "Rejected",
30
+ msgId: match$1.requestId,
31
+ errorCode: "TranslationFailed",
32
+ errorDetail: match$1.error
33
+ };
34
+ }
35
+
6
36
  function toResolvedOutputs(outputs) {
7
37
  return Pulumi.all([
8
38
  Adapter$ReventlessCore.resourcesToInterop(outputs.resources),
@@ -19,6 +49,7 @@ let componentType = "InboundTranslationSlice";
19
49
 
20
50
  export {
21
51
  componentType,
52
+ receiveResultToOutcome,
22
53
  toResolvedOutputs,
23
54
  }
24
55
  /* @pulumi/pulumi Not a pure module */
@@ -29,7 +29,7 @@ module type T = {
29
29
  let receive: (
30
30
  ReventlessInfra.CommandTopic.publishJsons,
31
31
  JSON.t,
32
- ) => promise<result<array<string>, string>>
32
+ ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>
33
33
  }
34
34
 
35
35
  module Make = (
@@ -53,7 +53,7 @@ module Make = (
53
53
  let receive = async (
54
54
  publishJsons: ReventlessInfra.CommandTopic.publishJsons,
55
55
  inputJson: JSON.t,
56
- ) => {
56
+ ): ReventlessInfra.InboundTranslationSlice.receiveResult => {
57
57
  let requestId = Uuid.v4()
58
58
 
59
59
  // Parse the external input
@@ -78,7 +78,7 @@ module Make = (
78
78
  receivedAt: now(),
79
79
  },
80
80
  )
81
- Error(msg)
81
+ Error({requestId, error: msg})
82
82
 
83
83
  | Ok(input) =>
84
84
  switch Translation.translate(input) {
@@ -94,7 +94,7 @@ module Make = (
94
94
  receivedAt: now(),
95
95
  },
96
96
  )
97
- Ok([])
97
+ Ok({requestId, targetIds: [], commandCount: 0})
98
98
  } else {
99
99
  // Encode all commands; abort on first encoding failure
100
100
  let msgs = ref([])
@@ -134,7 +134,7 @@ module Make = (
134
134
  receivedAt: now(),
135
135
  },
136
136
  )
137
- Error(msg)
137
+ Error({requestId, error: msg})
138
138
  | None =>
139
139
  try {
140
140
  await publishJsons(msgs.contents)
@@ -152,7 +152,7 @@ module Make = (
152
152
  receivedAt: now(),
153
153
  },
154
154
  )
155
- Ok(targetIds)
155
+ Ok({requestId, targetIds, commandCount: pairs->Array.length})
156
156
  } catch {
157
157
  | exn =>
158
158
  let msg =
@@ -169,7 +169,7 @@ module Make = (
169
169
  receivedAt: now(),
170
170
  },
171
171
  )
172
- Error(msg)
172
+ Error({requestId, error: msg})
173
173
  }
174
174
  }
175
175
  }
@@ -184,7 +184,7 @@ module Make = (
184
184
  receivedAt: now(),
185
185
  },
186
186
  )
187
- Error(msg)
187
+ Error({requestId, error: msg})
188
188
  }
189
189
  }
190
190
  }