@reventlessdev/reventless-core 3.0.0-alpha.260 → 3.0.0-alpha.261
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.
- package/CHANGELOG.md +8 -0
- package/package.json +9 -9
- package/src/Behavior.res +1 -4
- package/src/Message.res +17 -14
- package/src/Projection.res +51 -25
- package/src/RequestContext.res +8 -9
- package/src/ResourceAttribution.res +28 -14
- package/src/adapter/Adapter.res +48 -4
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
- package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
- package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
- package/src/adapter/Monitoring/Monitoring.res +16 -17
- package/src/adapter/QueryInterception/QueryInterception.res +0 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
- package/src/admin/Platform_Admin.res +17 -6
- package/src/admin/Platform_AdminApi.res +14 -9
- package/src/admin/Platform_Admin_Callback.res +1 -1
- package/src/admin/Platform_BakedManifest.res +34 -34
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
- package/src/admin/Platform_PluginStructuresApi.res +15 -12
- package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate.res +13 -12
- package/src/components/Aggregate/Aggregate_Builder.res +22 -21
- package/src/components/Aggregate/Aggregate_Callback.res +91 -86
- package/src/components/Api/ApiNoApiHelpers.res +1 -3
- package/src/components/Api/Api_Ids.res +0 -1
- package/src/components/Api/Api_Naming.res +41 -18
- package/src/components/Api/GraphQL_FragmentGenerator.res +38 -51
- package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
- package/src/components/Api/GraphQL_Stitcher.res +9 -10
- package/src/components/Api/MCP_SchemaGenerator.res +8 -12
- package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
- package/src/components/Api/QueryDbListQuery.res +13 -6
- package/src/components/Api/SchemaType.res +28 -23
- package/src/components/Api/SuryToJsonSchema.res +9 -10
- package/src/components/AutomationSlice/AutomationSlice.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
- package/src/components/Builder_Helpers.res +12 -23
- package/src/components/CommandGenerator/CommandGenerator.res +6 -1
- package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
- package/src/components/CommandTopic/CommandTopic.res +3 -3
- package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
- package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
- package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
- package/src/components/Counter/Counter_Builder.res +1 -6
- package/src/components/Counter/Counter_Callback.res +3 -3
- package/src/components/Dcb/Dcb_Builder.res +970 -956
- package/src/components/DcbEventLog/DcbEventLog.res +4 -4
- package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
- package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
- package/src/components/EventCollector/EventCollector_Builder.res +2 -1
- package/src/components/EventLog/EventLog.res +5 -1
- package/src/components/EventLog/EventLog_Operations.res +12 -8
- package/src/components/EventMapper/EventMapper.res +8 -10
- package/src/components/EventMapper/EventMapper_Builder.res +4 -1
- package/src/components/EventMapper/EventMapper_Callback.res +46 -31
- package/src/components/EventTopic/EventTopic.res +3 -3
- package/src/components/EventTopic/EventTopic_Builder.res +1 -1
- package/src/components/Extension/Extension.res +4 -1
- package/src/components/Extension/Extension_Operations.res +11 -3
- package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
- package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
- package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
- package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
- package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
- package/src/components/QueryDb/QueryDb_Operations.res +10 -10
- package/src/components/ReadModel/ReadModel.res +3 -3
- package/src/components/ReadModel/ReadModel_Builder.res +1 -2
- package/src/components/ReadModel/ReadModel_Callback.res +2 -1
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
- package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
- package/src/components/StateTopic.res +1 -3
- package/src/components/StateViewSlice/StateViewSlice.res +4 -4
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
- package/src/components/Task/Task.res +3 -3
- package/src/components/Task/Task_Adapter.res +4 -1
- package/src/components/Task/Task_Builder.res +24 -21
- package/src/plugin/api/PluginBaseFragment.res +9 -3
- package/src/plugin/component/Capability_Inference.res +8 -10
- package/src/plugin/component/Plugin.res +6 -6
- package/src/plugin/component/Plugin_Builder.res +54 -50
- package/src/plugin/component/Plugin_Callback.res +10 -6
- package/src/plugin/component/Plugin_Helpers.res +264 -230
- package/src/plugin/component/Plugin_Structure.res +370 -324
- package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
- package/src/plugin/component/SchemaWalker.res +11 -12
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
- package/src/plugin/lifecycle/PluginBehavior.res +3 -4
- package/src/plugin/lifecycle/PluginSpec.res +1 -2
- package/src/plugin/lifecycle/PluginsProjection.res +16 -5
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
- package/src/util/CommandPublisher.res +9 -2
- package/src/util/CsvStream.res +3 -3
- package/src/util/Interstack.res +7 -15
- package/src/util/Interstack.resi +3 -3
- package/src/util/LogFormat.res +5 -9
- package/src/util/Logger.res +1 -2
- package/src/util/Lru.res +2 -1
- package/src/util/Lru.resi +2 -3
- package/src/util/ScheduleOps.res +24 -27
- package/src/util/StorePrefixCollision.res +6 -9
- package/src/util/Util_Promise.res +4 -1
- package/src/util/Util_Promise.res.mjs +1 -1
- package/tests/IdentityTest.res +166 -113
- package/tests/ResourceAttributionTest.res +28 -25
- package/tests/StorePrefixCollisionTest.res +2 -1
- package/tests/TaskBucketNamingTest.res +3 -2
- package/tests/adapter/EventLogProvisioningTest.res +51 -48
- package/tests/adapter/MonitoringTest.res +81 -38
- package/tests/adapter/QueryInterceptionTest.res +2 -2
- package/tests/adapter/RefusalVocabularyTest.res +31 -23
- package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
- package/tests/adapter/RuntimeExtensionTest.res +5 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
- package/tests/admin/Platform_Admin_StructureTest.res +10 -11
- package/tests/admin/Platform_BakedManifestTest.res +31 -35
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
- package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
- package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
- package/tests/aggregate/AggregateCacheTest.res +10 -12
- package/tests/aggregate/AggregateCallbackTest.res +75 -46
- package/tests/aggregate/AggregateCausationTest.res +19 -20
- package/tests/aggregate/AggregateConflictTest.res +41 -19
- package/tests/aggregate/AggregateFixtures.res +15 -5
- package/tests/aggregate/AggregateRejectionTest.res +97 -80
- package/tests/aggregate/AggregateSnapshotTest.res +44 -16
- package/tests/api/Api_IdsTest.res +10 -4
- package/tests/api/BrandedMarkerCompositionTest.res +48 -36
- package/tests/api/GeolocationSemanticTest.res +3 -3
- package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
- package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
- package/tests/api/StorageRefFieldsTest.res +30 -18
- package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
- package/tests/api/TaggedUnionSchemaTest.res +17 -24
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
- package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
- package/tests/commandgenerator/OwnerStampingTest.res +91 -76
- package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
- package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
- package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
- package/tests/counter/CounterCallbackTest.res +40 -34
- package/tests/counter/CounterFixtures.res +4 -4
- package/tests/dcb/DcbDecodeTest.res +115 -84
- package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
- package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
- package/tests/dcb/DcbFixtures.res +4 -5
- package/tests/dcb/DcbScopeInferenceTest.res +173 -114
- package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
- package/tests/dcb/DcbTagTest.res +22 -11
- package/tests/dcb/DcbValidationTest.res +288 -218
- package/tests/eventlog/EventLogFixtures.res +4 -4
- package/tests/eventlog/EventLogOperationsTest.res +102 -67
- package/tests/eventlog/EventLogRetryTest.res +104 -65
- package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
- package/tests/eventmapper/EventMapperFixtures.res +15 -23
- package/tests/eventmapper/EventMapperTest.res +54 -38
- package/tests/eventtopic/EventTopicFixtures.res +3 -4
- package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
- package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
- package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
- package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
- package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
- package/tests/logger/LogFormatTest.res +104 -82
- package/tests/logger/LoggerLazyTest.res +21 -12
- package/tests/message/MessageTest.res +96 -93
- package/tests/message/MetaEnvelopeTest.res +26 -30
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
- package/tests/plugin/ManifestVisibilityTest.res +20 -12
- package/tests/plugin/PluginDefinitionScalars.res +1 -2
- package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
- package/tests/plugin/PluginSpecExperiment.res +0 -1
- package/tests/plugin/PluginStructureTest.res +1529 -1045
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
- package/tests/plugin/SchemaWalkerTest.res +116 -70
- package/tests/plugin/StateChange/PsReserveStock.res +4 -4
- package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
- package/tests/plugin/StateView/PsCustomersView.res +4 -4
- package/tests/plugin/StateView/PsShipmentsView.res +3 -2
- package/tests/projection/LifecycleTrailTest.res +1 -4
- package/tests/projection/ProjectionOptimizeTest.res +13 -9
- package/tests/querydb/QueryDbFixtures.res +0 -1
- package/tests/querydb/QueryDbOperationsTest.res +99 -64
- package/tests/querydb/TaggedUnionStampTest.res +10 -8
- package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
- package/tests/util/CommandPublisherTest.res +0 -1
- package/tests/util/CsvStreamTest.res +3 -3
- package/tests/util/LruTest.res +84 -56
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
open JestGlobals
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
// Driven through `makeGenerateCommand` rather than by calling `stampOwnerFields`
|
|
5
4
|
// directly. The unit under test is not really the stamp — it is the claim that
|
|
6
5
|
// the stamp is reached on the path a command actually takes, which is the half a
|
|
@@ -16,7 +15,10 @@ type command =
|
|
|
16
15
|
})
|
|
17
16
|
| // The control variant: same union, no owner. Whatever the caller sends here
|
|
18
17
|
// must survive untouched, or the stamp is reaching across constructors.
|
|
19
|
-
ImportProducts({
|
|
18
|
+
ImportProducts({
|
|
19
|
+
batchId: @s.matches(Reventless.DcbTag.string) string,
|
|
20
|
+
source: string,
|
|
21
|
+
})
|
|
20
22
|
|
|
21
23
|
let published: ref<array<Message.commandJson>> = ref([])
|
|
22
24
|
|
|
@@ -86,30 +88,27 @@ describe("@owner stamping on the command path:", () => {
|
|
|
86
88
|
// The decisive case. An implementation that fills only the absent field passes
|
|
87
89
|
// every other test in this file and hands one customer another's orders.
|
|
88
90
|
testPromise("a FORGED owner field is overwritten, not respected", async () => {
|
|
89
|
-
let _ =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)->Effect.runPromise
|
|
91
|
+
let _ = await generate(
|
|
92
|
+
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
|
|
93
|
+
)->Effect.runPromise
|
|
93
94
|
expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
|
|
94
95
|
})
|
|
95
96
|
|
|
96
97
|
testPromise("non-owner fields of the same command are untouched", async () => {
|
|
97
|
-
let _ =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)->Effect.runPromise
|
|
98
|
+
let _ = await generate(
|
|
99
|
+
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
|
|
100
|
+
)->Effect.runPromise
|
|
101
101
|
expect(lastPublished()->fieldOf("note"))->toEqual(Some(str("gift wrap")))
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
testPromise("a command in the same union with no owner field is left alone", async () => {
|
|
105
|
-
let _ =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)->Effect.runPromise
|
|
105
|
+
let _ = await generate(
|
|
106
|
+
payload(
|
|
107
|
+
~command="ImportProducts",
|
|
108
|
+
~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
|
|
109
|
+
~identity=cognito(~userId="cust-A", ~groups=["User"]),
|
|
110
|
+
),
|
|
111
|
+
)->Effect.runPromise
|
|
113
112
|
expect((lastPublished()->fieldOf("source"), lastPublished()->fieldOf("customerId")))->toEqual((
|
|
114
113
|
Some(str("acme")),
|
|
115
114
|
None,
|
|
@@ -119,21 +118,25 @@ describe("@owner stamping on the command path:", () => {
|
|
|
119
118
|
describe("exempt callers keep the value they sent:", () => {
|
|
120
119
|
// Acting on another principal's behalf must stay possible for those who may
|
|
121
120
|
// — otherwise an operator can never correct or back-fill anything.
|
|
122
|
-
testPromise(
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
testPromise(
|
|
122
|
+
"an elevated caller's chosen customerId survives",
|
|
123
|
+
async () => {
|
|
124
|
+
let _ = await generate(
|
|
125
125
|
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="ops-1", ~groups=["Admin"])),
|
|
126
126
|
)->Effect.runPromise
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
|
|
128
|
+
},
|
|
129
|
+
)
|
|
129
130
|
|
|
130
131
|
// The IAM shape carries no userId at all. Stamping it would write
|
|
131
132
|
// `undefined`; refusing it would break internal traffic. It is exempt.
|
|
132
|
-
testPromise(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
testPromise(
|
|
134
|
+
"an IAM system caller's chosen customerId survives",
|
|
135
|
+
async () => {
|
|
136
|
+
let _ = await generate(placeOrder(~customerId="cust-B", ~identity=iam))->Effect.runPromise
|
|
137
|
+
expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
|
|
138
|
+
},
|
|
139
|
+
)
|
|
137
140
|
})
|
|
138
141
|
|
|
139
142
|
describe("a caller that cannot be identified is refused:", () => {
|
|
@@ -143,45 +146,56 @@ describe("@owner stamping on the command path:", () => {
|
|
|
143
146
|
| exception _ => true
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
testPromise(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
testPromise(
|
|
150
|
+
"an anonymous caller cannot place an owned command",
|
|
151
|
+
async () =>
|
|
152
|
+
expect(
|
|
153
|
+
await refused(placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous)),
|
|
154
|
+
)->toBe(true),
|
|
150
155
|
)
|
|
151
156
|
|
|
152
157
|
// The refusal describes the caller's own request, so a transport is allowed
|
|
153
158
|
// to report it. Unmarked it arrives as "Unexpected error", which tells a
|
|
154
159
|
// caller who needs to authenticate nothing at all.
|
|
155
|
-
testPromise(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
testPromise(
|
|
161
|
+
"the refusal is marked as the caller's fault",
|
|
162
|
+
async () => {
|
|
163
|
+
let failure = switch await generate(
|
|
164
|
+
placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
|
|
165
|
+
)->Effect.runPromise {
|
|
166
|
+
| _ => None
|
|
167
|
+
| exception e => Some(e)
|
|
168
|
+
}
|
|
169
|
+
expect(failure->Option.mapOr(false, Plugin_ResolverError.isCallerFault))->toBe(true)
|
|
170
|
+
},
|
|
171
|
+
)
|
|
164
172
|
|
|
165
|
-
testPromise(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
testPromise(
|
|
174
|
+
"nothing is published when the caller is refused",
|
|
175
|
+
async () => {
|
|
176
|
+
let _ = await refused(
|
|
177
|
+
placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
|
|
178
|
+
)
|
|
179
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
180
|
+
},
|
|
181
|
+
)
|
|
171
182
|
|
|
172
183
|
// The refusal is scoped to commands that actually record an owner. An
|
|
173
184
|
// anonymous caller invoking an unowned command has nothing to prove, and
|
|
174
185
|
// refusing it here would silently narrow `AllowAnonymous`.
|
|
175
|
-
testPromise(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
testPromise(
|
|
187
|
+
"an anonymous caller may still issue an unowned command",
|
|
188
|
+
async () => {
|
|
189
|
+
let _ = await generate(
|
|
190
|
+
payload(
|
|
191
|
+
~command="ImportProducts",
|
|
192
|
+
~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
|
|
193
|
+
~identity=Reventless.Identity.anonymous,
|
|
194
|
+
),
|
|
195
|
+
)->Effect.runPromise
|
|
196
|
+
expect(lastPublished()->fieldOf("source"))->toEqual(Some(str("acme")))
|
|
197
|
+
},
|
|
198
|
+
)
|
|
185
199
|
})
|
|
186
200
|
})
|
|
187
201
|
|
|
@@ -207,10 +221,9 @@ describe("the schema a generator is built with must answer for its commands:", (
|
|
|
207
221
|
)
|
|
208
222
|
|
|
209
223
|
testPromise("a permissive schema silently stamps nothing — the defect, pinned", async () => {
|
|
210
|
-
let _ =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)->Effect.runPromise
|
|
224
|
+
let _ = await permissive(
|
|
225
|
+
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
|
|
226
|
+
)->Effect.runPromise
|
|
214
227
|
// Not the behaviour we want anywhere — recorded so that a change making the
|
|
215
228
|
// permissive path stamp correctly is a deliberate one, and so the contrast
|
|
216
229
|
// with the case below cannot be read as incidental.
|
|
@@ -218,10 +231,9 @@ describe("the schema a generator is built with must answer for its commands:", (
|
|
|
218
231
|
})
|
|
219
232
|
|
|
220
233
|
testPromise("the real schema stamps, from the same payload and caller", async () => {
|
|
221
|
-
let _ =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)->Effect.runPromise
|
|
234
|
+
let _ = await generate(
|
|
235
|
+
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
|
|
236
|
+
)->Effect.runPromise
|
|
225
237
|
expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
|
|
226
238
|
})
|
|
227
239
|
|
|
@@ -229,15 +241,18 @@ describe("the schema a generator is built with must answer for its commands:", (
|
|
|
229
241
|
// maps now satisfy by construction: every command a spec exposes resolves to
|
|
230
242
|
// its own owner fields through the schema that command will be dispatched
|
|
231
243
|
// with. Reading `[]` here is exactly the state that stamps nothing.
|
|
232
|
-
testPromise(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
testPromise(
|
|
245
|
+
"every command of a spec resolves its owner fields through that spec's schema",
|
|
246
|
+
async () => {
|
|
247
|
+
let schema = commandSchema->S.castToUnknown
|
|
248
|
+
expect(Reventless.Owner.variantFieldNames(schema, ~variant="PlaceOrder"))->toEqual([
|
|
249
|
+
"customerId",
|
|
250
|
+
])
|
|
251
|
+
expect(Reventless.Owner.variantFieldNames(schema, ~variant="ImportProducts"))->toEqual([])
|
|
252
|
+
// The substitution, asked the same question.
|
|
253
|
+
expect(
|
|
254
|
+
Reventless.Owner.variantFieldNames(S.json->S.castToUnknown, ~variant="PlaceOrder"),
|
|
255
|
+
)->toEqual([])
|
|
256
|
+
},
|
|
257
|
+
)
|
|
243
258
|
})
|
|
@@ -47,8 +47,10 @@ let sliceItem = (reference, command): CommandTopic.topicItem<
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
let runSlice = (reference, command) =>
|
|
50
|
-
SliceHandler.handleCommands(
|
|
51
|
-
|
|
50
|
+
SliceHandler.handleCommands(
|
|
51
|
+
dcbEventLog,
|
|
52
|
+
Stream.fromIterable([sliceItem(reference, command)]),
|
|
53
|
+
)->Effect.runPromise
|
|
52
54
|
|
|
53
55
|
let runAgg = (~aggId="agg-1", reference, command) =>
|
|
54
56
|
Stream.fromIterable([AggregateFixtures.makeTopicItem(~aggId, reference, command)])
|
|
@@ -78,9 +80,7 @@ let seen = () =>
|
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
let record = () =>
|
|
81
|
-
CommandTopic_Helpers.registerCommandOutcome(r =>
|
|
82
|
-
observed := observed.contents->Array.concat([r])
|
|
83
|
-
)
|
|
83
|
+
CommandTopic_Helpers.registerCommandOutcome(r => observed := observed.contents->Array.concat([r]))
|
|
84
84
|
|
|
85
85
|
let _ = beforeEach(() => {
|
|
86
86
|
AggregateFixtures.mock.reset()
|
|
@@ -94,166 +94,209 @@ let _ = beforeEach(() => {
|
|
|
94
94
|
|
|
95
95
|
describe("CommandTopic command-outcome hook:", () => {
|
|
96
96
|
describe("aggregate", () => {
|
|
97
|
-
testPromise(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
97
|
+
testPromise(
|
|
98
|
+
"observes an accepted command with no side-channel installed",
|
|
99
|
+
async () => {
|
|
100
|
+
record()
|
|
101
|
+
let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
102
|
+
|
|
103
|
+
expect(results)->toEqual([Ok("ref-1")])
|
|
104
|
+
expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
testPromise(
|
|
109
|
+
"carries the entity id of an accepted command",
|
|
110
|
+
async () => {
|
|
111
|
+
record()
|
|
112
|
+
let _ = await runAgg(~aggId="agg-7", "ref-1", AggregateFixtures.AggSpec.Create({name: "W"}))
|
|
113
|
+
|
|
114
|
+
let entityId = switch (observed.contents->Array.getUnsafe(0)).outcome {
|
|
115
|
+
| OutcomeAccepted({entityId}) => entityId
|
|
116
|
+
| OutcomeRejected(_) => None
|
|
117
|
+
}
|
|
118
|
+
expect(entityId)->toEqual(Some("agg-7"))
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
testPromise(
|
|
123
|
+
"reports a decide rejection as domain, with the declared error code",
|
|
124
|
+
async () => {
|
|
125
|
+
// Seed before registering, so only the second (rejected) Create is observed.
|
|
126
|
+
let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
127
|
+
record()
|
|
128
|
+
let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
129
|
+
|
|
130
|
+
expect(results)->toEqual([Ok("ref-1")])
|
|
131
|
+
expect(seen())->toEqual([("TestAggregate", "ref-1", "rejected/domain", "AlreadyExists", 0)])
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
testPromise(
|
|
136
|
+
"reports a failed append as infrastructure, not a domain rejection",
|
|
137
|
+
async () => {
|
|
138
|
+
record()
|
|
139
|
+
AggregateFixtures.mock.failNextAppend := true
|
|
140
|
+
let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
141
|
+
|
|
142
|
+
// `decide` said Ok here — the store is what failed, and the hook has to say so.
|
|
143
|
+
expect(seen())->toEqual([
|
|
144
|
+
("TestAggregate", "ref-1", "rejected/infrastructure", "AppendFailed", 0),
|
|
145
|
+
])
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
testPromise(
|
|
150
|
+
"observes every command of a mixed batch",
|
|
151
|
+
async () => {
|
|
152
|
+
let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
153
|
+
record()
|
|
154
|
+
let _ = await Stream.fromIterable([
|
|
155
|
+
AggregateFixtures.makeTopicItem(
|
|
156
|
+
"ref-1",
|
|
157
|
+
AggregateFixtures.AggSpec.Rename({newName: "A"}),
|
|
158
|
+
),
|
|
145
159
|
AggregateFixtures.makeTopicItem("ref-2", AggregateFixtures.AggSpec.Create({name: "B"})),
|
|
146
|
-
AggregateFixtures.makeTopicItem(
|
|
160
|
+
AggregateFixtures.makeTopicItem(
|
|
161
|
+
"ref-3",
|
|
162
|
+
AggregateFixtures.AggSpec.Rename({newName: "C"}),
|
|
163
|
+
),
|
|
147
164
|
])
|
|
148
165
|
->AggregateFixtures.TestHandler.handleCommands
|
|
149
166
|
->Effect.runPromise
|
|
150
167
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
expect(seen())->toEqual([
|
|
169
|
+
("TestAggregate", "ref-1", "accepted", "", 2),
|
|
170
|
+
("TestAggregate", "ref-2", "rejected/domain", "AlreadyExists", 0),
|
|
171
|
+
("TestAggregate", "ref-3", "accepted", "", 2),
|
|
172
|
+
])
|
|
173
|
+
},
|
|
174
|
+
)
|
|
157
175
|
})
|
|
158
176
|
|
|
159
177
|
describe("state-change slice", () => {
|
|
160
|
-
testPromise(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
178
|
+
testPromise(
|
|
179
|
+
"observes an accepted command with no side-channel installed",
|
|
180
|
+
async () => {
|
|
181
|
+
record()
|
|
182
|
+
let _ = await runSlice(
|
|
183
|
+
"ref-1",
|
|
184
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
expect(seen())->toEqual([
|
|
188
|
+
("TestStateChangeSlice", DcbFixtures.testMeta.msgId, "accepted", "", 1),
|
|
189
|
+
])
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
testPromise(
|
|
194
|
+
"reports a decide rejection as domain, with the declared error code",
|
|
195
|
+
async () => {
|
|
196
|
+
let _ = await runSlice(
|
|
197
|
+
"ref-seed",
|
|
198
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
199
|
+
)
|
|
200
|
+
record()
|
|
201
|
+
let _ = await runSlice(
|
|
202
|
+
"ref-1",
|
|
203
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Again"}),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
expect(seen())->toEqual([
|
|
207
|
+
(
|
|
208
|
+
"TestStateChangeSlice",
|
|
209
|
+
DcbFixtures.testMeta.msgId,
|
|
210
|
+
"rejected/domain",
|
|
211
|
+
"ItemAlreadyExists",
|
|
212
|
+
0,
|
|
213
|
+
),
|
|
214
|
+
])
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
testPromise(
|
|
219
|
+
"reports exhausted append conflicts as infrastructure",
|
|
220
|
+
async () => {
|
|
221
|
+
record()
|
|
222
|
+
// One more failure than the retry loop can absorb.
|
|
223
|
+
dcbMock.failNextAppends := 4
|
|
224
|
+
let _ = await runSlice(
|
|
225
|
+
"ref-1",
|
|
226
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
expect(seen())->toEqual([
|
|
230
|
+
(
|
|
231
|
+
"TestStateChangeSlice",
|
|
232
|
+
DcbFixtures.testMeta.msgId,
|
|
233
|
+
"rejected/infrastructure",
|
|
234
|
+
"Conflict",
|
|
235
|
+
0,
|
|
236
|
+
),
|
|
237
|
+
])
|
|
238
|
+
},
|
|
239
|
+
)
|
|
207
240
|
})
|
|
208
241
|
|
|
209
242
|
describe("registration", () => {
|
|
210
|
-
testPromise(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
243
|
+
testPromise(
|
|
244
|
+
"with nothing registered nothing is observed and the command is unaffected",
|
|
245
|
+
async () => {
|
|
246
|
+
let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
247
|
+
|
|
248
|
+
expect(results)->toEqual([Ok("ref-1")])
|
|
249
|
+
expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
|
|
250
|
+
expect(observed.contents->Array.length)->toBe(0)
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
testPromise(
|
|
255
|
+
"clearCommandOutcome stops observation",
|
|
256
|
+
async () => {
|
|
257
|
+
record()
|
|
258
|
+
CommandTopic_Helpers.clearCommandOutcome()
|
|
259
|
+
let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
260
|
+
|
|
261
|
+
expect(observed.contents->Array.length)->toBe(0)
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
testPromise(
|
|
266
|
+
"an out-of-tree extension can register it from onColdStart",
|
|
267
|
+
async () => {
|
|
268
|
+
// The hook needs no registration path of its own: it has the shape the cold-start
|
|
269
|
+
// seam already reaches, so an extension picks it up alongside the other four.
|
|
270
|
+
module Accountant: RuntimeExtension.Extension = {
|
|
271
|
+
let moduleUrl = "file:///pkg/accountant.res.mjs"
|
|
272
|
+
let companionModuleUrls = []
|
|
273
|
+
let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) =>
|
|
274
|
+
record()
|
|
275
|
+
}
|
|
276
|
+
RuntimeExtension.use(module(Accountant: RuntimeExtension.Extension))
|
|
277
|
+
RuntimeExtension.notifyColdStart(
|
|
278
|
+
~runtimeKind=ComponentType.Aggregate,
|
|
279
|
+
~component="AllAggregates",
|
|
280
|
+
~plugin=Some("Catalog"),
|
|
281
|
+
~platform=Some("Shop"),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
285
|
+
|
|
286
|
+
expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
testPromise(
|
|
291
|
+
"a throwing hook is swallowed and the command still succeeds",
|
|
292
|
+
async () => {
|
|
293
|
+
CommandTopic_Helpers.registerCommandOutcome(_ => JsError.throwWithMessage("hook exploded"))
|
|
294
|
+
let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
|
|
295
|
+
|
|
296
|
+
// The outcome the hook observes has already happened — an observer must not undo it.
|
|
297
|
+
expect(results)->toEqual([Ok("ref-1")])
|
|
298
|
+
expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
|
|
299
|
+
},
|
|
300
|
+
)
|
|
258
301
|
})
|
|
259
302
|
})
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
// ─────────────────────────────────────────────────────────────
|
|
3
2
|
// Command spec for CommandTopic_Callback tests
|
|
4
3
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -43,9 +42,11 @@ module TestOps: CommandTopic_Callback.Ops with module Spec = TestSpec = {
|
|
|
43
42
|
}
|
|
44
43
|
capturedItems :=
|
|
45
44
|
capturedItems.contents->Array.concat(
|
|
46
|
-
items->Array.map((
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
items->Array.map((
|
|
46
|
+
item: ReventlessInfra.CommandTopic.topicItem<
|
|
47
|
+
Message.command'<TestSpec.Id.t, TestSpec.command>,
|
|
48
|
+
>,
|
|
49
|
+
) => {
|
|
49
50
|
reference: item.reference,
|
|
50
51
|
command: item.command.command,
|
|
51
52
|
}),
|