@reventlessdev/reventless-core 3.0.0-alpha.260 → 3.0.0-alpha.262

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 (208) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +10 -10
  3. package/src/Behavior.res +1 -4
  4. package/src/Message.res +17 -14
  5. package/src/Projection.res +51 -25
  6. package/src/RequestContext.res +8 -9
  7. package/src/ResourceAttribution.res +28 -14
  8. package/src/adapter/Adapter.res +48 -4
  9. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
  10. package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
  11. package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
  12. package/src/adapter/Monitoring/Monitoring.res +16 -17
  13. package/src/adapter/QueryInterception/QueryInterception.res +0 -1
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
  15. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
  16. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
  17. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
  18. package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
  19. package/src/adapter/Secrets/Secrets_Node_Backend.res +60 -0
  20. package/src/adapter/Secrets/Secrets_Node_Backend.res.mjs +69 -0
  21. package/src/admin/Platform_Admin.res +17 -6
  22. package/src/admin/Platform_AdminApi.res +14 -9
  23. package/src/admin/Platform_Admin_Callback.res +1 -1
  24. package/src/admin/Platform_BakedManifest.res +34 -34
  25. package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
  26. package/src/admin/Platform_PluginStructuresApi.res +15 -12
  27. package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
  28. package/src/components/Aggregate/Aggregate.res +13 -12
  29. package/src/components/Aggregate/Aggregate_Builder.res +22 -21
  30. package/src/components/Aggregate/Aggregate_Callback.res +91 -86
  31. package/src/components/Api/ApiNoApiHelpers.res +1 -3
  32. package/src/components/Api/Api_Ids.res +0 -1
  33. package/src/components/Api/Api_Naming.res +41 -18
  34. package/src/components/Api/GraphQL_FragmentGenerator.res +38 -51
  35. package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
  36. package/src/components/Api/GraphQL_Stitcher.res +9 -10
  37. package/src/components/Api/MCP_SchemaGenerator.res +8 -12
  38. package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
  39. package/src/components/Api/QueryDbListQuery.res +13 -6
  40. package/src/components/Api/SchemaType.res +28 -23
  41. package/src/components/Api/SuryToJsonSchema.res +9 -10
  42. package/src/components/AutomationSlice/AutomationSlice.res +4 -4
  43. package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
  44. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
  45. package/src/components/Builder_Helpers.res +12 -23
  46. package/src/components/CommandGenerator/CommandGenerator.res +6 -1
  47. package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
  48. package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
  49. package/src/components/CommandTopic/CommandTopic.res +3 -3
  50. package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
  51. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
  52. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
  53. package/src/components/Counter/Counter_Builder.res +1 -6
  54. package/src/components/Counter/Counter_Callback.res +3 -3
  55. package/src/components/Dcb/Dcb_Builder.res +970 -956
  56. package/src/components/DcbEventLog/DcbEventLog.res +4 -4
  57. package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
  58. package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
  59. package/src/components/EventCollector/EventCollector_Builder.res +2 -1
  60. package/src/components/EventLog/EventLog.res +5 -1
  61. package/src/components/EventLog/EventLog_Operations.res +12 -8
  62. package/src/components/EventMapper/EventMapper.res +8 -10
  63. package/src/components/EventMapper/EventMapper_Builder.res +4 -1
  64. package/src/components/EventMapper/EventMapper_Callback.res +46 -31
  65. package/src/components/EventTopic/EventTopic.res +3 -3
  66. package/src/components/EventTopic/EventTopic_Builder.res +1 -1
  67. package/src/components/Extension/Extension.res +4 -1
  68. package/src/components/Extension/Extension_Operations.res +11 -3
  69. package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
  70. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
  71. package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
  72. package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
  73. package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
  74. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
  75. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
  76. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
  77. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
  78. package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
  79. package/src/components/QueryDb/QueryDb_Operations.res +10 -10
  80. package/src/components/ReadModel/ReadModel.res +3 -3
  81. package/src/components/ReadModel/ReadModel_Builder.res +1 -2
  82. package/src/components/ReadModel/ReadModel_Callback.res +2 -1
  83. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
  84. package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
  85. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
  86. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
  87. package/src/components/StateTopic.res +1 -3
  88. package/src/components/StateViewSlice/StateViewSlice.res +4 -4
  89. package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
  90. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  91. package/src/components/Task/Task.res +3 -3
  92. package/src/components/Task/Task_Adapter.res +4 -1
  93. package/src/components/Task/Task_Builder.res +24 -21
  94. package/src/plugin/api/PluginBaseFragment.res +9 -3
  95. package/src/plugin/component/Capability_Inference.res +8 -10
  96. package/src/plugin/component/Plugin.res +6 -6
  97. package/src/plugin/component/Plugin_Builder.res +54 -50
  98. package/src/plugin/component/Plugin_Callback.res +10 -6
  99. package/src/plugin/component/Plugin_Helpers.res +264 -230
  100. package/src/plugin/component/Plugin_Structure.res +370 -324
  101. package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
  102. package/src/plugin/component/SchemaWalker.res +11 -12
  103. package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
  104. package/src/plugin/lifecycle/PluginBehavior.res +3 -4
  105. package/src/plugin/lifecycle/PluginSpec.res +1 -2
  106. package/src/plugin/lifecycle/PluginsProjection.res +16 -5
  107. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  108. package/src/util/CommandPublisher.res +9 -2
  109. package/src/util/CsvStream.res +3 -3
  110. package/src/util/Interstack.res +7 -15
  111. package/src/util/Interstack.resi +3 -3
  112. package/src/util/LogFormat.res +5 -9
  113. package/src/util/Logger.res +1 -2
  114. package/src/util/Lru.res +2 -1
  115. package/src/util/Lru.resi +2 -3
  116. package/src/util/ScheduleOps.res +24 -27
  117. package/src/util/StorePrefixCollision.res +6 -9
  118. package/src/util/Util_Promise.res +4 -1
  119. package/src/util/Util_Promise.res.mjs +1 -1
  120. package/tests/IdentityTest.res +166 -113
  121. package/tests/ResourceAttributionTest.res +28 -25
  122. package/tests/Secrets_Node_BackendTest.res +141 -0
  123. package/tests/Secrets_Node_BackendTest.res.mjs +152 -0
  124. package/tests/StorePrefixCollisionTest.res +2 -1
  125. package/tests/TaskBucketNamingTest.res +3 -2
  126. package/tests/adapter/EventLogProvisioningTest.res +51 -48
  127. package/tests/adapter/MonitoringTest.res +81 -38
  128. package/tests/adapter/QueryInterceptionTest.res +2 -2
  129. package/tests/adapter/RefusalVocabularyTest.res +31 -23
  130. package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
  131. package/tests/adapter/RuntimeExtensionTest.res +5 -3
  132. package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
  133. package/tests/admin/Platform_Admin_StructureTest.res +22 -14
  134. package/tests/admin/Platform_Admin_StructureTest.res.mjs +4 -3
  135. package/tests/admin/Platform_BakedManifestTest.res +31 -35
  136. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
  137. package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
  138. package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
  139. package/tests/aggregate/AggregateCacheTest.res +10 -12
  140. package/tests/aggregate/AggregateCallbackTest.res +75 -46
  141. package/tests/aggregate/AggregateCausationTest.res +19 -20
  142. package/tests/aggregate/AggregateConflictTest.res +41 -19
  143. package/tests/aggregate/AggregateFixtures.res +15 -5
  144. package/tests/aggregate/AggregateRejectionTest.res +97 -80
  145. package/tests/aggregate/AggregateSnapshotTest.res +44 -16
  146. package/tests/api/Api_IdsTest.res +10 -4
  147. package/tests/api/BrandedMarkerCompositionTest.res +48 -36
  148. package/tests/api/GeolocationSemanticTest.res +3 -3
  149. package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
  150. package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
  151. package/tests/api/StorageRefFieldsTest.res +30 -18
  152. package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
  153. package/tests/api/TaggedUnionSchemaTest.res +17 -24
  154. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
  155. package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
  156. package/tests/commandgenerator/OwnerStampingTest.res +91 -76
  157. package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
  158. package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
  159. package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
  160. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
  161. package/tests/counter/CounterCallbackTest.res +40 -34
  162. package/tests/counter/CounterFixtures.res +4 -4
  163. package/tests/dcb/DcbDecodeTest.res +115 -84
  164. package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
  165. package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
  166. package/tests/dcb/DcbFixtures.res +4 -5
  167. package/tests/dcb/DcbScopeInferenceTest.res +173 -114
  168. package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
  169. package/tests/dcb/DcbTagTest.res +22 -11
  170. package/tests/dcb/DcbValidationTest.res +288 -218
  171. package/tests/eventlog/EventLogFixtures.res +4 -4
  172. package/tests/eventlog/EventLogOperationsTest.res +102 -67
  173. package/tests/eventlog/EventLogRetryTest.res +104 -65
  174. package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
  175. package/tests/eventmapper/EventMapperFixtures.res +15 -23
  176. package/tests/eventmapper/EventMapperTest.res +54 -38
  177. package/tests/eventtopic/EventTopicFixtures.res +3 -4
  178. package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
  179. package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
  180. package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
  181. package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
  182. package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
  183. package/tests/logger/LogFormatTest.res +104 -82
  184. package/tests/logger/LoggerLazyTest.res +21 -12
  185. package/tests/message/MessageTest.res +96 -93
  186. package/tests/message/MetaEnvelopeTest.res +26 -30
  187. package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
  188. package/tests/plugin/ManifestVisibilityTest.res +20 -12
  189. package/tests/plugin/PluginDefinitionScalars.res +1 -2
  190. package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
  191. package/tests/plugin/PluginSpecExperiment.res +0 -1
  192. package/tests/plugin/PluginStructureTest.res +1529 -1045
  193. package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
  194. package/tests/plugin/SchemaWalkerTest.res +116 -70
  195. package/tests/plugin/StateChange/PsReserveStock.res +4 -4
  196. package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
  197. package/tests/plugin/StateView/PsCustomersView.res +4 -4
  198. package/tests/plugin/StateView/PsShipmentsView.res +3 -2
  199. package/tests/projection/LifecycleTrailTest.res +1 -4
  200. package/tests/projection/ProjectionOptimizeTest.res +13 -9
  201. package/tests/querydb/QueryDbFixtures.res +0 -1
  202. package/tests/querydb/QueryDbOperationsTest.res +99 -64
  203. package/tests/querydb/TaggedUnionStampTest.res +10 -8
  204. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
  205. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
  206. package/tests/util/CommandPublisherTest.res +0 -1
  207. package/tests/util/CsvStreamTest.res +3 -3
  208. package/tests/util/LruTest.res +84 -56
@@ -417,7 +417,11 @@ describe("DcbTag:", () => {
417
417
  DcbFixtures.compositeEventSchema,
418
418
  )
419
419
  expect(fields->Array.length)->toBe(3)->ignore
420
- expect(fields->Array.map(f => f.name))->toEqual(["environment", "platformName", "pluginName"])
420
+ expect(fields->Array.map(f => f.name))->toEqual([
421
+ "environment",
422
+ "platformName",
423
+ "pluginName",
424
+ ])
421
425
  },
422
426
  )
423
427
 
@@ -601,10 +605,12 @@ describe("DcbTag:", () => {
601
605
  | JsExn(err) =>
602
606
  threw := true
603
607
  expect(
604
- JsExn.message(err)->Option.getOr("")->String.includes(
605
- "mixes @compositePartitionTag and @partitionTag",
606
- ),
607
- )->toBe(true)->ignore
608
+ JsExn.message(err)
609
+ ->Option.getOr("")
610
+ ->String.includes("mixes @compositePartitionTag and @partitionTag"),
611
+ )
612
+ ->toBe(true)
613
+ ->ignore
608
614
  }
609
615
  expect(threw.contents)->toBe(true)
610
616
  },
@@ -623,7 +629,9 @@ describe("DcbTag:", () => {
623
629
  threw := true
624
630
  expect(
625
631
  JsExn.message(err)->Option.getOr("")->String.includes("at least 2 annotated fields"),
626
- )->toBe(true)->ignore
632
+ )
633
+ ->toBe(true)
634
+ ->ignore
627
635
  }
628
636
  expect(threw.contents)->toBe(true)
629
637
  },
@@ -636,7 +644,9 @@ describe("DcbTag:", () => {
636
644
  () =>
637
645
  expect(
638
646
  Reventless.DcbTag.isCompositePartitionMember(
639
- Reventless.DcbTag.compositePartitionMember(~position=0)->Reventless.DcbTag.toUnknownSchema,
647
+ Reventless.DcbTag.compositePartitionMember(
648
+ ~position=0,
649
+ )->Reventless.DcbTag.toUnknownSchema,
640
650
  ),
641
651
  )->toBe(true),
642
652
  )
@@ -655,9 +665,7 @@ describe("DcbTag:", () => {
655
665
  "returns false for plain S.string",
656
666
  () =>
657
667
  expect(
658
- Reventless.DcbTag.isCompositePartitionMember(
659
- S.string->Reventless.DcbTag.toUnknownSchema,
660
- ),
668
+ Reventless.DcbTag.isCompositePartitionMember(S.string->Reventless.DcbTag.toUnknownSchema),
661
669
  )->toBe(false),
662
670
  )
663
671
  })
@@ -844,7 +852,10 @@ describe("DcbTag:", () => {
844
852
  ~crossPartitionTagKeys=["studentId"],
845
853
  ),
846
854
  )->toEqual([
847
- {Reventless.DcbTag.eventTypes: ["StudentSubscribed"], tags: [{key: "courseId", value: "C1"}]},
855
+ {
856
+ Reventless.DcbTag.eventTypes: ["StudentSubscribed"],
857
+ tags: [{key: "courseId", value: "C1"}],
858
+ },
848
859
  {eventTypes: ["StudentSubscribed"], tags: [{key: "studentId", value: "S1"}]},
849
860
  ]),
850
861
  )
@@ -7,28 +7,25 @@ type producedA =
7
7
  | ItemRenamed({itemId: @s.matches(Reventless.DcbTag.string) string, newName: string})
8
8
 
9
9
  @schema
10
- type producedB =
11
- | ItemArchived({itemId: @s.matches(Reventless.DcbTag.string) string})
10
+ type producedB = ItemArchived({itemId: @s.matches(Reventless.DcbTag.string) string})
12
11
 
13
12
  // A second producer for ItemCreated with matching fields/tags
14
13
  @schema
15
14
  type producedADuplicate =
16
- | ItemCreated({itemId: @s.matches(Reventless.DcbTag.string) string, name: string})
15
+ ItemCreated({itemId: @s.matches(Reventless.DcbTag.string) string, name: string})
17
16
 
18
17
  // A second producer for ItemCreated with DIFFERENT fields (missing name)
19
18
  @schema
20
- type producedAMismatchFields =
21
- | ItemCreated({itemId: @s.matches(Reventless.DcbTag.string) string})
19
+ type producedAMismatchFields = ItemCreated({itemId: @s.matches(Reventless.DcbTag.string) string})
22
20
 
23
21
  // A second producer for ItemCreated with mismatched tag annotations
24
22
  @schema
25
- type producedAMismatchTags =
26
- | ItemCreated({itemId: string, name: string})
23
+ type producedAMismatchTags = ItemCreated({itemId: string, name: string})
27
24
 
28
25
  // A second producer for ItemCreated with mismatched field type
29
26
  @schema
30
27
  type producedAMismatchType =
31
- | ItemCreated({itemId: @s.matches(Reventless.DcbTag.string) string, name: float})
28
+ ItemCreated({itemId: @s.matches(Reventless.DcbTag.string) string, name: float})
32
29
 
33
30
  // Consumed events — full shape
34
31
  @schema
@@ -38,8 +35,7 @@ type consumedFull =
38
35
 
39
36
  // Consumed events — partial projection (only itemId)
40
37
  @schema
41
- type consumedPartial =
42
- | ItemCreated({itemId: string})
38
+ type consumedPartial = ItemCreated({itemId: string})
43
39
 
44
40
  // Consumed events — payload-less
45
41
  @schema
@@ -49,18 +45,15 @@ type consumedPayloadLess =
49
45
 
50
46
  // Consumed events — references nonexistent producer
51
47
  @schema
52
- type consumedDangling =
53
- | ItemDeleted({itemId: string})
48
+ type consumedDangling = ItemDeleted({itemId: string})
54
49
 
55
50
  // Consumed events — field not in produced shape
56
51
  @schema
57
- type consumedBadField =
58
- | ItemCreated({itemId: string, rating: float})
52
+ type consumedBadField = ItemCreated({itemId: string, rating: float})
59
53
 
60
54
  // Consumed events — type mismatch
61
55
  @schema
62
- type consumedBadType =
63
- | ItemCreated({itemId: string, name: float})
56
+ type consumedBadType = ItemCreated({itemId: string, name: float})
64
57
 
65
58
  // --- Composite-read (Issue 5) fixtures ---
66
59
 
@@ -74,8 +67,7 @@ type recordDemandCommand =
74
67
 
75
68
  // Single-tag command → partition read, never composite.
76
69
  @schema
77
- type touchDemandCommand =
78
- | TouchDemand({productId: @s.matches(Reventless.DcbTag.string) string})
70
+ type touchDemandCommand = TouchDemand({productId: @s.matches(Reventless.DcbTag.string) string})
79
71
 
80
72
  // Tagged-array command → per-element OR (single-tag) clauses, never composite.
81
73
  @schema
@@ -137,213 +129,291 @@ type flaggedPartitionScoped =
137
129
 
138
130
  describe("DcbValidation:", () => {
139
131
  describe("validateProducedAndConsumed", () => {
140
- testSync("passes when all consumed events match produced events", () => {
141
- let result = validate(
142
- ~produced=[("AddItem", producedASchema->u), ("ArchiveItem", producedBSchema->u)],
143
- ~consumed=[("ItemView", consumedFullSchema->u)],
144
- )
145
- expect(result)->toEqual(Ok())
146
- })
147
-
148
- testSync("passes with partial field projection", () => {
149
- let result = validate(
150
- ~produced=[("AddItem", producedASchema->u)],
151
- ~consumed=[("CheckExists", consumedPartialSchema->u)],
152
- )
153
- expect(result)->toEqual(Ok())
154
- })
155
-
156
- testSync("passes with payload-less consumed events", () => {
157
- let result = validate(
158
- ~produced=[("AddItem", producedASchema->u), ("ArchiveItem", producedBSchema->u)],
159
- ~consumed=[("CheckExists", consumedPayloadLessSchema->u)],
160
- )
161
- expect(result)->toEqual(Ok())
162
- })
163
-
164
- testSync("passes when multiple producers have identical shapes and tags", () => {
165
- let result = validate(
166
- ~produced=[("AddItem", producedASchema->u), ("CloneItem", producedADuplicateSchema->u)],
167
- ~consumed=[],
168
- )
169
- expect(result)->toEqual(Ok())
170
- })
171
-
172
- testSync("fails when consumed event has no producer", () => {
173
- let result = validate(
174
- ~produced=[("AddItem", producedASchema->u)],
175
- ~consumed=[("DeleteHandler", consumedDanglingSchema->u)],
176
- )
177
- switch result {
178
- | Error(errors) =>
179
- expect(errors->Array.length)->toBe(1)->ignore
180
- let err = errors->Array.getUnsafe(0)
181
- expect(err.sliceName)->toBe("DeleteHandler")->ignore
182
- expect(err.message->String.includes("ItemDeleted"))->toBe(true)->ignore
183
- expect(err.message->String.includes("no slice produces it"))->toBe(true)
184
- | Ok() => fail("Expected validation error")
185
- }
186
- })
187
-
188
- testSync("fails when consumed event references field not in produced shape", () => {
189
- let result = validate(
190
- ~produced=[("AddItem", producedASchema->u)],
191
- ~consumed=[("RatingView", consumedBadFieldSchema->u)],
192
- )
193
- switch result {
194
- | Error(errors) =>
195
- expect(errors->Array.length)->toBe(1)->ignore
196
- let err = errors->Array.getUnsafe(0)
197
- expect(err.sliceName)->toBe("RatingView")->ignore
198
- expect(err.message->String.includes("rating"))->toBe(true)->ignore
199
- expect(err.message->String.includes("no field"))->toBe(true)
200
- | Ok() => fail("Expected validation error")
201
- }
202
- })
203
-
204
- testSync("fails when consumed field type mismatches produced field type", () => {
205
- let result = validate(
206
- ~produced=[("AddItem", producedASchema->u)],
207
- ~consumed=[("BadTypeView", consumedBadTypeSchema->u)],
208
- )
209
- switch result {
210
- | Error(errors) =>
211
- expect(errors->Array.length)->toBe(1)->ignore
212
- let err = errors->Array.getUnsafe(0)
213
- expect(err.sliceName)->toBe("BadTypeView")->ignore
214
- expect(err.message->String.includes("name"))->toBe(true)->ignore
215
- expect(err.message->String.includes("float"))->toBe(true)->ignore
216
- expect(err.message->String.includes("string"))->toBe(true)
217
- | Ok() => fail("Expected validation error")
218
- }
219
- })
220
-
221
- testSync("fails when producers have different fields for same TAG", () => {
222
- let result = validate(
223
- ~produced=[("AddItem", producedASchema->u), ("ImportItem", producedAMismatchFieldsSchema->u)],
224
- ~consumed=[],
225
- )
226
- switch result {
227
- | Error(errors) =>
228
- expect(errors->Array.length)->toBeGreaterThanOrEqual(1)->ignore
229
- let err = errors->Array.getUnsafe(0)
230
- expect(err.message->String.includes("AddItem"))->toBe(true)->ignore
231
- expect(err.message->String.includes("ImportItem"))->toBe(true)->ignore
232
- expect(err.message->String.includes("name"))->toBe(true)
233
- | Ok() => fail("Expected validation error")
234
- }
235
- })
236
-
237
- testSync("fails when producers have different tag annotations for same TAG", () => {
238
- let result = validate(
239
- ~produced=[("AddItem", producedASchema->u), ("UntaggerItem", producedAMismatchTagsSchema->u)],
240
- ~consumed=[],
241
- )
242
- switch result {
243
- | Error(errors) =>
244
- expect(errors->Array.length)->toBeGreaterThanOrEqual(1)->ignore
245
- let hasTagError = errors->Array.some(err => err.message->String.includes("tag annotations"))
246
- expect(hasTagError)->toBe(true)
247
- | Ok() => fail("Expected validation error")
248
- }
249
- })
250
-
251
- testSync("fails when producers have different field types for same TAG", () => {
252
- let result = validate(
253
- ~produced=[("AddItem", producedASchema->u), ("BadTypeItem", producedAMismatchTypeSchema->u)],
254
- ~consumed=[],
255
- )
256
- switch result {
257
- | Error(errors) =>
258
- expect(errors->Array.length)->toBeGreaterThanOrEqual(1)->ignore
259
- let err = errors->Array.getUnsafe(0)
260
- expect(err.message->String.includes("name"))->toBe(true)->ignore
261
- expect(err.message->String.includes("string"))->toBe(true)->ignore
262
- expect(err.message->String.includes("float"))->toBe(true)
263
- | Ok() => fail("Expected validation error")
264
- }
265
- })
266
-
267
- testSync("passes with empty produced and consumed", () => {
268
- let result = validate(~produced=[], ~consumed=[])
269
- expect(result)->toEqual(Ok())
270
- })
271
-
272
- testSync("collects multiple errors at once", () => {
273
- let result = validate(
274
- ~produced=[("AddItem", producedASchema->u)],
275
- ~consumed=[("DeleteHandler", consumedDanglingSchema->u), ("BadTypeView", consumedBadTypeSchema->u)],
276
- )
277
- switch result {
278
- | Error(errors) => expect(errors->Array.length)->toBeGreaterThanOrEqual(2)
279
- | Ok() => fail("Expected validation errors")
280
- }
281
- })
132
+ testSync(
133
+ "passes when all consumed events match produced events",
134
+ () => {
135
+ let result = validate(
136
+ ~produced=[("AddItem", producedASchema->u), ("ArchiveItem", producedBSchema->u)],
137
+ ~consumed=[("ItemView", consumedFullSchema->u)],
138
+ )
139
+ expect(result)->toEqual(Ok())
140
+ },
141
+ )
142
+
143
+ testSync(
144
+ "passes with partial field projection",
145
+ () => {
146
+ let result = validate(
147
+ ~produced=[("AddItem", producedASchema->u)],
148
+ ~consumed=[("CheckExists", consumedPartialSchema->u)],
149
+ )
150
+ expect(result)->toEqual(Ok())
151
+ },
152
+ )
153
+
154
+ testSync(
155
+ "passes with payload-less consumed events",
156
+ () => {
157
+ let result = validate(
158
+ ~produced=[("AddItem", producedASchema->u), ("ArchiveItem", producedBSchema->u)],
159
+ ~consumed=[("CheckExists", consumedPayloadLessSchema->u)],
160
+ )
161
+ expect(result)->toEqual(Ok())
162
+ },
163
+ )
164
+
165
+ testSync(
166
+ "passes when multiple producers have identical shapes and tags",
167
+ () => {
168
+ let result = validate(
169
+ ~produced=[("AddItem", producedASchema->u), ("CloneItem", producedADuplicateSchema->u)],
170
+ ~consumed=[],
171
+ )
172
+ expect(result)->toEqual(Ok())
173
+ },
174
+ )
175
+
176
+ testSync(
177
+ "fails when consumed event has no producer",
178
+ () => {
179
+ let result = validate(
180
+ ~produced=[("AddItem", producedASchema->u)],
181
+ ~consumed=[("DeleteHandler", consumedDanglingSchema->u)],
182
+ )
183
+ switch result {
184
+ | Error(errors) =>
185
+ expect(errors->Array.length)->toBe(1)->ignore
186
+ let err = errors->Array.getUnsafe(0)
187
+ expect(err.sliceName)->toBe("DeleteHandler")->ignore
188
+ expect(err.message->String.includes("ItemDeleted"))->toBe(true)->ignore
189
+ expect(err.message->String.includes("no slice produces it"))->toBe(true)
190
+ | Ok() => fail("Expected validation error")
191
+ }
192
+ },
193
+ )
194
+
195
+ testSync(
196
+ "fails when consumed event references field not in produced shape",
197
+ () => {
198
+ let result = validate(
199
+ ~produced=[("AddItem", producedASchema->u)],
200
+ ~consumed=[("RatingView", consumedBadFieldSchema->u)],
201
+ )
202
+ switch result {
203
+ | Error(errors) =>
204
+ expect(errors->Array.length)->toBe(1)->ignore
205
+ let err = errors->Array.getUnsafe(0)
206
+ expect(err.sliceName)->toBe("RatingView")->ignore
207
+ expect(err.message->String.includes("rating"))->toBe(true)->ignore
208
+ expect(err.message->String.includes("no field"))->toBe(true)
209
+ | Ok() => fail("Expected validation error")
210
+ }
211
+ },
212
+ )
213
+
214
+ testSync(
215
+ "fails when consumed field type mismatches produced field type",
216
+ () => {
217
+ let result = validate(
218
+ ~produced=[("AddItem", producedASchema->u)],
219
+ ~consumed=[("BadTypeView", consumedBadTypeSchema->u)],
220
+ )
221
+ switch result {
222
+ | Error(errors) =>
223
+ expect(errors->Array.length)->toBe(1)->ignore
224
+ let err = errors->Array.getUnsafe(0)
225
+ expect(err.sliceName)->toBe("BadTypeView")->ignore
226
+ expect(err.message->String.includes("name"))->toBe(true)->ignore
227
+ expect(err.message->String.includes("float"))->toBe(true)->ignore
228
+ expect(err.message->String.includes("string"))->toBe(true)
229
+ | Ok() => fail("Expected validation error")
230
+ }
231
+ },
232
+ )
233
+
234
+ testSync(
235
+ "fails when producers have different fields for same TAG",
236
+ () => {
237
+ let result = validate(
238
+ ~produced=[
239
+ ("AddItem", producedASchema->u),
240
+ ("ImportItem", producedAMismatchFieldsSchema->u),
241
+ ],
242
+ ~consumed=[],
243
+ )
244
+ switch result {
245
+ | Error(errors) =>
246
+ expect(errors->Array.length)->toBeGreaterThanOrEqual(1)->ignore
247
+ let err = errors->Array.getUnsafe(0)
248
+ expect(err.message->String.includes("AddItem"))->toBe(true)->ignore
249
+ expect(err.message->String.includes("ImportItem"))->toBe(true)->ignore
250
+ expect(err.message->String.includes("name"))->toBe(true)
251
+ | Ok() => fail("Expected validation error")
252
+ }
253
+ },
254
+ )
255
+
256
+ testSync(
257
+ "fails when producers have different tag annotations for same TAG",
258
+ () => {
259
+ let result = validate(
260
+ ~produced=[
261
+ ("AddItem", producedASchema->u),
262
+ ("UntaggerItem", producedAMismatchTagsSchema->u),
263
+ ],
264
+ ~consumed=[],
265
+ )
266
+ switch result {
267
+ | Error(errors) =>
268
+ expect(errors->Array.length)->toBeGreaterThanOrEqual(1)->ignore
269
+ let hasTagError =
270
+ errors->Array.some(err => err.message->String.includes("tag annotations"))
271
+ expect(hasTagError)->toBe(true)
272
+ | Ok() => fail("Expected validation error")
273
+ }
274
+ },
275
+ )
276
+
277
+ testSync(
278
+ "fails when producers have different field types for same TAG",
279
+ () => {
280
+ let result = validate(
281
+ ~produced=[
282
+ ("AddItem", producedASchema->u),
283
+ ("BadTypeItem", producedAMismatchTypeSchema->u),
284
+ ],
285
+ ~consumed=[],
286
+ )
287
+ switch result {
288
+ | Error(errors) =>
289
+ expect(errors->Array.length)->toBeGreaterThanOrEqual(1)->ignore
290
+ let err = errors->Array.getUnsafe(0)
291
+ expect(err.message->String.includes("name"))->toBe(true)->ignore
292
+ expect(err.message->String.includes("string"))->toBe(true)->ignore
293
+ expect(err.message->String.includes("float"))->toBe(true)
294
+ | Ok() => fail("Expected validation error")
295
+ }
296
+ },
297
+ )
298
+
299
+ testSync(
300
+ "passes with empty produced and consumed",
301
+ () => {
302
+ let result = validate(~produced=[], ~consumed=[])
303
+ expect(result)->toEqual(Ok())
304
+ },
305
+ )
306
+
307
+ testSync(
308
+ "collects multiple errors at once",
309
+ () => {
310
+ let result = validate(
311
+ ~produced=[("AddItem", producedASchema->u)],
312
+ ~consumed=[
313
+ ("DeleteHandler", consumedDanglingSchema->u),
314
+ ("BadTypeView", consumedBadTypeSchema->u),
315
+ ],
316
+ )
317
+ switch result {
318
+ | Error(errors) => expect(errors->Array.length)->toBeGreaterThanOrEqual(2)
319
+ | Ok() => fail("Expected validation errors")
320
+ }
321
+ },
322
+ )
282
323
  })
283
324
 
284
325
  describe("validateCompositeReads (Issue 5)", () => {
285
- let check = slices =>
286
- Reventless.DcbValidation.validateCompositeReads(~slices, ~producedTagKeys)
287
-
288
- testSync("warns when a composite read's consumed type carries an extra tag", () => {
289
- let warnings = check([("RecordDemand", recordDemandCommandSchema->u, demandConsumedSchema->u)])
290
- // ProductDemandRevoked carries customerId beyond [productId, orderId] → missed.
291
- expect(warnings->Array.length)->toBe(1)
292
- let w = warnings->Array.getUnsafe(0)
293
- expect(w.sliceName)->toBe("RecordDemand")
294
- expect(w.message->String.includes("ProductDemandRevoked"))->toBe(true)
295
- expect(w.message->String.includes("customerId"))->toBe(true)
296
- })
297
-
298
- testSync("does not warn for an exact-match consumed type", () => {
299
- // ProductDemandRecorded carries exactly [productId, orderId] — read finds it.
300
- let warnings = check([("RecordDemand", recordDemandCommandSchema->u, demandConsumedSchema->u)])
301
- expect(warnings->Array.some(w => w.message->String.includes("ProductDemandRecorded")))->toBe(
302
- false,
303
- )
304
- })
305
-
306
- testSync("ignores single-tag commands (partition read, not composite)", () => {
307
- let warnings = check([("TouchDemand", touchDemandCommandSchema->u, demandConsumedSchema->u)])
308
- expect(warnings)->toEqual([])
309
- })
310
-
311
- testSync("ignores tagged-array commands (per-element OR, not composite)", () => {
312
- let warnings = check([("PlaceOrder", placeOrderCommandSchema->u, demandConsumedSchema->u)])
313
- expect(warnings)->toEqual([])
314
- })
315
-
316
- testSync("no warnings when nothing reads compositely", () => {
317
- expect(check([]))->toEqual([])
318
- })
326
+ let check = slices => Reventless.DcbValidation.validateCompositeReads(~slices, ~producedTagKeys)
327
+
328
+ testSync(
329
+ "warns when a composite read's consumed type carries an extra tag",
330
+ () => {
331
+ let warnings = check([
332
+ ("RecordDemand", recordDemandCommandSchema->u, demandConsumedSchema->u),
333
+ ])
334
+ // ProductDemandRevoked carries customerId beyond [productId, orderId] → missed.
335
+ expect(warnings->Array.length)->toBe(1)
336
+ let w = warnings->Array.getUnsafe(0)
337
+ expect(w.sliceName)->toBe("RecordDemand")
338
+ expect(w.message->String.includes("ProductDemandRevoked"))->toBe(true)
339
+ expect(w.message->String.includes("customerId"))->toBe(true)
340
+ },
341
+ )
342
+
343
+ testSync(
344
+ "does not warn for an exact-match consumed type",
345
+ () => {
346
+ // ProductDemandRecorded carries exactly [productId, orderId] — read finds it.
347
+ let warnings = check([
348
+ ("RecordDemand", recordDemandCommandSchema->u, demandConsumedSchema->u),
349
+ ])
350
+ expect(
351
+ warnings->Array.some(w => w.message->String.includes("ProductDemandRecorded")),
352
+ )->toBe(false)
353
+ },
354
+ )
355
+
356
+ testSync(
357
+ "ignores single-tag commands (partition read, not composite)",
358
+ () => {
359
+ let warnings = check([
360
+ ("TouchDemand", touchDemandCommandSchema->u, demandConsumedSchema->u),
361
+ ])
362
+ expect(warnings)->toEqual([])
363
+ },
364
+ )
365
+
366
+ testSync(
367
+ "ignores tagged-array commands (per-element OR, not composite)",
368
+ () => {
369
+ let warnings = check([("PlaceOrder", placeOrderCommandSchema->u, demandConsumedSchema->u)])
370
+ expect(warnings)->toEqual([])
371
+ },
372
+ )
373
+
374
+ testSync(
375
+ "no warnings when nothing reads compositely",
376
+ () => {
377
+ expect(check([]))->toEqual([])
378
+ },
379
+ )
319
380
  })
320
381
 
321
382
  describe("validateCrossPartitionScope (Issue 13)", () => {
322
383
  let check = producers => Reventless.DcbValidation.validateCrossPartitionScope(~producers)
323
384
 
324
- testSync("no warning when every carrier of a key agrees on cross-partition scope", () => {
325
- let warnings = check([
326
- ("Subscribe", subscribedCrossPartitionSchema->u),
327
- ("Unsubscribe", unsubscribedCrossPartitionSchema->u),
328
- ])
329
- expect(warnings)->toEqual([])
330
- })
331
-
332
- testSync("warns when one producer leaves a cross-partition key partition-scoped", () => {
333
- let warnings = check([
334
- ("Subscribe", subscribedCrossPartitionSchema->u),
335
- ("Flag", flaggedPartitionScopedSchema->u),
336
- ])
337
- // 'Flag' carries studentId but partition-scoped, while 'Subscribe' declares it
338
- // @crossPartition → scope mismatch.
339
- expect(warnings->Array.length)->toBe(1)
340
- let w = warnings->Array.getUnsafe(0)
341
- expect(w.sliceName)->toBe("Flag")
342
- expect(w.message->String.includes("studentId"))->toBe(true)
343
- })
344
-
345
- testSync("no warning when no key is cross-partition anywhere", () => {
346
- expect(check([("Flag", flaggedPartitionScopedSchema->u)]))->toEqual([])
347
- })
385
+ testSync(
386
+ "no warning when every carrier of a key agrees on cross-partition scope",
387
+ () => {
388
+ let warnings = check([
389
+ ("Subscribe", subscribedCrossPartitionSchema->u),
390
+ ("Unsubscribe", unsubscribedCrossPartitionSchema->u),
391
+ ])
392
+ expect(warnings)->toEqual([])
393
+ },
394
+ )
395
+
396
+ testSync(
397
+ "warns when one producer leaves a cross-partition key partition-scoped",
398
+ () => {
399
+ let warnings = check([
400
+ ("Subscribe", subscribedCrossPartitionSchema->u),
401
+ ("Flag", flaggedPartitionScopedSchema->u),
402
+ ])
403
+ // 'Flag' carries studentId but partition-scoped, while 'Subscribe' declares it
404
+ // @crossPartition → scope mismatch.
405
+ expect(warnings->Array.length)->toBe(1)
406
+ let w = warnings->Array.getUnsafe(0)
407
+ expect(w.sliceName)->toBe("Flag")
408
+ expect(w.message->String.includes("studentId"))->toBe(true)
409
+ },
410
+ )
411
+
412
+ testSync(
413
+ "no warning when no key is cross-partition anywhere",
414
+ () => {
415
+ expect(check([("Flag", flaggedPartitionScopedSchema->u)]))->toEqual([])
416
+ },
417
+ )
348
418
  })
349
419
  })