@reventlessdev/reventless-local 3.0.0-alpha.167 → 3.0.0-alpha.169
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 +20 -0
- package/package.json +11 -11
- package/src/Platform.res +11 -1
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +16 -8
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +7 -7
- package/src/adapter/DomainGraphQL_Server.res +96 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +84 -0
- package/src/adapter/GraphQL_Server.res.mjs +18 -0
- package/src/adapter/LocalObjectStore.res +64 -0
- package/src/adapter/LocalObjectStore.res.mjs +55 -0
- package/src/adapter/PgProjectionCatchup.res +6 -1
- package/src/adapter/PgProjectionCatchup.res.mjs +4 -2
- package/src/adapter/ProjectionCheckpoint.res +11 -2
- package/src/adapter/ProjectionCheckpoint.res.mjs +9 -3
- package/tests/PluginEventDecodeTest.res +2 -0
- package/tests/PluginEventDecodeTest.res.mjs +2 -2
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +52 -1
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +39 -1
- package/tests/adapter/InboundTranslationMutationTest.res +139 -0
- package/tests/adapter/InboundTranslationMutationTest.res.mjs +132 -0
- package/tests/adapter/QueryDbListResolverTest.res +2 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -0
- package/tests/adapter/ServedBucketHttpTest.res +174 -0
- package/tests/adapter/ServedBucketHttpTest.res.mjs +177 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +13 -9
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +21 -13
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +215 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +273 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformTest.res +38 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformTest.res.mjs +28 -0
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +1 -1
- package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +2 -1
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +1 -1
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +2 -1
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Platform-level fixtures for OutboundTranslationSlice.
|
|
2
|
+
//
|
|
3
|
+
// The sibling callback test calls phase1/phase2 directly and passes even when
|
|
4
|
+
// the component is never reached by an event — it cannot observe wiring. This
|
|
5
|
+
// fixture builds the real thing: a DcbEventLog, a StateChangeSlice that appends
|
|
6
|
+
// to it, and an OutboundTranslationSlice subscribed to that log's event topic.
|
|
7
|
+
// A command published through `publishJsons` is the only input; the assertions
|
|
8
|
+
// read the TODO QueryDb and the recorded external calls.
|
|
9
|
+
//
|
|
10
|
+
// Wiring summary:
|
|
11
|
+
// DcbEventLog "TestLog" — publishes events to TestLogDcbEventLogEventTopic
|
|
12
|
+
// StateChangeSlice "Place" — Place command → Placed event
|
|
13
|
+
// OutboundTranslationSlice "SendConfirm" — Placed event → external call
|
|
14
|
+
|
|
15
|
+
open TestFixtures
|
|
16
|
+
open Reventless
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────────────────────────────────────
|
|
19
|
+
// Place StateChangeSlice
|
|
20
|
+
// ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
module PlaceSpec = {
|
|
23
|
+
let name = "Place"
|
|
24
|
+
module Id = Reventless.Id.String
|
|
25
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
26
|
+
|
|
27
|
+
@schema
|
|
28
|
+
type event = Placed({
|
|
29
|
+
orderId: @s.matches(Reventless.DcbTag.string) string,
|
|
30
|
+
customerId: string,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
@schema
|
|
34
|
+
type consumedEvent = Placed
|
|
35
|
+
|
|
36
|
+
@schema
|
|
37
|
+
type command = Place({
|
|
38
|
+
orderId: @s.matches(Reventless.DcbTag.string) string,
|
|
39
|
+
customerId: string,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
@schema
|
|
43
|
+
type error = AlreadyPlaced
|
|
44
|
+
|
|
45
|
+
let commandSchema = commandSchema
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module PlaceBehavior = {
|
|
49
|
+
module Spec = PlaceSpec
|
|
50
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
51
|
+
|
|
52
|
+
type state = bool
|
|
53
|
+
let initialState = false
|
|
54
|
+
let evolve = (_state: state, _event: Spec.consumedEvent) => true
|
|
55
|
+
let decide = (state: state, command: Spec.command): result<array<Spec.event>, Spec.error> =>
|
|
56
|
+
if state {
|
|
57
|
+
Error(AlreadyPlaced)
|
|
58
|
+
} else {
|
|
59
|
+
switch command {
|
|
60
|
+
| Place({orderId, customerId}) => Ok([Spec.Placed({orderId, customerId})])
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────
|
|
66
|
+
// OutboundTranslationSlice — consumes Placed, fire-and-forget
|
|
67
|
+
//
|
|
68
|
+
// `consumedEvent` deliberately declares a strict subset of the appended event's
|
|
69
|
+
// fields (no customerId beyond the two it needs is added, but the stored event
|
|
70
|
+
// carries fields this slice ignores) — the same shape the hybrid example uses.
|
|
71
|
+
// ─────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
module SendConfirmSpec = {
|
|
74
|
+
let name = "SendConfirm"
|
|
75
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
76
|
+
|
|
77
|
+
@schema
|
|
78
|
+
type consumedEvent = Placed({orderId: string})
|
|
79
|
+
|
|
80
|
+
@schema
|
|
81
|
+
type outboundItem = {orderId: string}
|
|
82
|
+
|
|
83
|
+
@schema
|
|
84
|
+
type inboundCommand = unit
|
|
85
|
+
|
|
86
|
+
let maxRetries = 3
|
|
87
|
+
let heartbeatInterval = 60
|
|
88
|
+
let targetName = None
|
|
89
|
+
let externalSystem = Some("EmailService")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Two separate records so a failure says which phase stalled: `collectCalls`
|
|
93
|
+
// empty means the event never reached phase 1 at all, while `collectCalls`
|
|
94
|
+
// populated with `externalCalls` empty isolates the fault to phase 2.
|
|
95
|
+
let collectCalls: array<string> = []
|
|
96
|
+
let externalCalls: array<string> = []
|
|
97
|
+
|
|
98
|
+
module SendConfirmTranslation: OutboundTranslationSlice.Translation
|
|
99
|
+
with module Spec := SendConfirmSpec = {
|
|
100
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
101
|
+
|
|
102
|
+
let collect = (event: SendConfirmSpec.consumedEvent) =>
|
|
103
|
+
switch event {
|
|
104
|
+
| Placed({orderId}) =>
|
|
105
|
+
collectCalls->Array.push(orderId)
|
|
106
|
+
[(orderId, ({orderId: orderId}: SendConfirmSpec.outboundItem))]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let translate = async (_id, item: SendConfirmSpec.outboundItem) => {
|
|
110
|
+
externalCalls->Array.push(item.orderId)
|
|
111
|
+
Ok(None)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────────────────────
|
|
116
|
+
// Bus + Pulumi mock setup
|
|
117
|
+
// ─────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
module Bus = LocalBus.Make()
|
|
120
|
+
let _ = TestRunner.setup()
|
|
121
|
+
|
|
122
|
+
module DcbLogMaker = DcbEventLog_Builder.Make(Bus)
|
|
123
|
+
let dcbEventLog = DcbLogMaker.make(
|
|
124
|
+
~name="TestLog",
|
|
125
|
+
~partitionTag=Reventless.DcbTag.Simple({key: "orderId"}),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────
|
|
129
|
+
// publishJsons routes by TAG through the global handler registry.
|
|
130
|
+
// (Same shape AutomationSliceSelfDeadlockFixtures uses — substitutes for a real
|
|
131
|
+
// CommandTopic without the runtime wiring overhead.)
|
|
132
|
+
// ─────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmdJsons => {
|
|
135
|
+
let _ =
|
|
136
|
+
await cmdJsons
|
|
137
|
+
->Array.map(async cmdJson => {
|
|
138
|
+
let typeName = switch cmdJson.commandJson {
|
|
139
|
+
| JSON.Object(dict) =>
|
|
140
|
+
dict
|
|
141
|
+
->Dict.get("TAG")
|
|
142
|
+
->Option.flatMap(j =>
|
|
143
|
+
switch j {
|
|
144
|
+
| JSON.String(s) => Some(s)
|
|
145
|
+
| _ => None
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
->Option.getOr("")
|
|
149
|
+
| _ => ""
|
|
150
|
+
}
|
|
151
|
+
let fullBody = JSON.Encode.object(
|
|
152
|
+
Dict.fromArray([
|
|
153
|
+
("id", JSON.Encode.string(cmdJson.id)),
|
|
154
|
+
("meta", cmdJson.meta->S.reverseConvertToJsonOrThrow(Reventless.Message.metaSchema)),
|
|
155
|
+
("command", cmdJson.commandJson),
|
|
156
|
+
]),
|
|
157
|
+
)
|
|
158
|
+
let handlers = ReventlessCore.CommandTopic.getHandlers(typeName)
|
|
159
|
+
let _ =
|
|
160
|
+
await handlers
|
|
161
|
+
->Array.map(async entry => {
|
|
162
|
+
let item: ReventlessInfra.CommandTopic.topicItem<JSON.t> = {
|
|
163
|
+
reference: cmdJson.id,
|
|
164
|
+
command: fullBody,
|
|
165
|
+
}
|
|
166
|
+
let _ = await entry.handler(Stream.fromIterable([item]))->Effect.runPromise
|
|
167
|
+
})
|
|
168
|
+
->Promise.all
|
|
169
|
+
})
|
|
170
|
+
->Promise.all
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let publishJsonsOutput = publishJsons->Pulumi.Output.make
|
|
174
|
+
|
|
175
|
+
// ─────────────────────────────────────────────────────────────
|
|
176
|
+
// Wire the StateChangeSlice + the OutboundTranslationSlice
|
|
177
|
+
// ─────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
module PlaceMaker = StateChangeSlice_Builder.Make(PlaceSpec, PlaceBehavior)
|
|
180
|
+
let _placeSlice = PlaceMaker.make(~dcbEventLog, ~publishJsons=publishJsonsOutput)
|
|
181
|
+
|
|
182
|
+
let dcbTopicOutputs: ReventlessInfra.EventTopic.outputs = (
|
|
183
|
+
dcbEventLog->ReventlessInfra.Component.outputs
|
|
184
|
+
).eventTopic
|
|
185
|
+
|
|
186
|
+
module OutboundMaker = OutboundTranslationSlice_Builder.Make(Bus)
|
|
187
|
+
module SendConfirm = OutboundMaker.Make(SendConfirmSpec, SendConfirmTranslation)
|
|
188
|
+
let sendConfirmSlice = SendConfirm.make(~dcbEventLog, ~publishJsons=publishJsonsOutput)
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────
|
|
191
|
+
// Test helpers
|
|
192
|
+
// ─────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
let placeCmdJson = (orderId: string): Reventless.Message.commandJson => {
|
|
195
|
+
id: orderId,
|
|
196
|
+
meta: testMeta,
|
|
197
|
+
commandJson: PlaceSpec.Place({orderId, customerId: "cust-" ++ orderId})
|
|
198
|
+
->S.reverseConvertToJsonOrThrow(PlaceSpec.commandSchema),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let readEventTypes = async (orderId: string) => {
|
|
202
|
+
let logOps = await dcbEventLog->DcbLogMaker.operations->TestRunner.resolve
|
|
203
|
+
let result = await logOps.read(
|
|
204
|
+
~query=[{tags: [{Reventless.DcbTag.key: "orderId", value: orderId}]}],
|
|
205
|
+
)
|
|
206
|
+
result.events->Array.map(e => e.eventType)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Drains pending microtasks so detached work (phase 2, QueryDb sync) settles.
|
|
210
|
+
let flush = async () => {
|
|
211
|
+
let _ = await Promise.resolve()
|
|
212
|
+
let _ = await Promise.resolve()
|
|
213
|
+
let _ = await Promise.resolve()
|
|
214
|
+
let _ = await Promise.resolve()
|
|
215
|
+
}
|
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
9
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
|
+
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
11
|
+
import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
|
|
12
|
+
import * as Component$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Component.res.mjs";
|
|
13
|
+
import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
|
|
14
|
+
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
15
|
+
import * as TestFixtures$ReventlessLocal from "../../TestFixtures.res.mjs";
|
|
16
|
+
import * as DcbEventLog_Builder$ReventlessLocal from "../../../src/components/DcbEventLog_Builder.res.mjs";
|
|
17
|
+
import * as StateChangeSlice_Builder$ReventlessLocal from "../../../src/components/StateChangeSlice_Builder.res.mjs";
|
|
18
|
+
import * as OutboundTranslationSlice_Builder$ReventlessLocal from "../../../src/components/OutboundTranslationSlice_Builder.res.mjs";
|
|
19
|
+
|
|
20
|
+
let name = "Place";
|
|
21
|
+
|
|
22
|
+
let moduleUrl = import.meta.url;
|
|
23
|
+
|
|
24
|
+
let eventSchema = S.schema(s => ({
|
|
25
|
+
TAG: "Placed",
|
|
26
|
+
orderId: s.m(DcbTag$Reventless.string),
|
|
27
|
+
customerId: s.m(S.string)
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
let consumedEventSchema = S.literal("Placed");
|
|
31
|
+
|
|
32
|
+
let commandSchema = S.schema(s => ({
|
|
33
|
+
TAG: "Place",
|
|
34
|
+
orderId: s.m(DcbTag$Reventless.string),
|
|
35
|
+
customerId: s.m(S.string)
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
let errorSchema = S.literal("AlreadyPlaced");
|
|
39
|
+
|
|
40
|
+
function commandAuthorization(param) {
|
|
41
|
+
return "AllowAuthenticated";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let PlaceSpec = {
|
|
45
|
+
name: name,
|
|
46
|
+
Id: undefined,
|
|
47
|
+
moduleUrl: moduleUrl,
|
|
48
|
+
eventSchema: eventSchema,
|
|
49
|
+
consumedEventSchema: consumedEventSchema,
|
|
50
|
+
errorSchema: errorSchema,
|
|
51
|
+
commandSchema: commandSchema,
|
|
52
|
+
commandAuthorization: commandAuthorization,
|
|
53
|
+
readConsistency: "EscalateOnRetry"
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let moduleUrl$1 = import.meta.url;
|
|
57
|
+
|
|
58
|
+
function evolve(_state, _event) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decide(state, command) {
|
|
63
|
+
if (state) {
|
|
64
|
+
return {
|
|
65
|
+
TAG: "Error",
|
|
66
|
+
_0: "AlreadyPlaced"
|
|
67
|
+
};
|
|
68
|
+
} else {
|
|
69
|
+
return {
|
|
70
|
+
TAG: "Ok",
|
|
71
|
+
_0: [{
|
|
72
|
+
TAG: "Placed",
|
|
73
|
+
orderId: command.orderId,
|
|
74
|
+
customerId: command.customerId
|
|
75
|
+
}]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let PlaceBehavior = {
|
|
81
|
+
Spec: undefined,
|
|
82
|
+
moduleUrl: moduleUrl$1,
|
|
83
|
+
initialState: false,
|
|
84
|
+
evolve: evolve,
|
|
85
|
+
decide: decide
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let moduleUrl$2 = import.meta.url;
|
|
89
|
+
|
|
90
|
+
let consumedEventSchema$1 = S.schema(s => ({
|
|
91
|
+
TAG: "Placed",
|
|
92
|
+
orderId: s.m(S.string)
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
let outboundItemSchema = S.schema(s => ({
|
|
96
|
+
orderId: s.m(S.string)
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
let SendConfirmSpec_externalSystem = "EmailService";
|
|
100
|
+
|
|
101
|
+
let SendConfirmSpec = {
|
|
102
|
+
name: "SendConfirm",
|
|
103
|
+
moduleUrl: moduleUrl$2,
|
|
104
|
+
consumedEventSchema: consumedEventSchema$1,
|
|
105
|
+
outboundItemSchema: outboundItemSchema,
|
|
106
|
+
inboundCommandSchema: S.unit,
|
|
107
|
+
maxRetries: 3,
|
|
108
|
+
heartbeatInterval: 60,
|
|
109
|
+
targetName: undefined,
|
|
110
|
+
externalSystem: SendConfirmSpec_externalSystem
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let collectCalls = [];
|
|
114
|
+
|
|
115
|
+
let externalCalls = [];
|
|
116
|
+
|
|
117
|
+
let moduleUrl$3 = import.meta.url;
|
|
118
|
+
|
|
119
|
+
function collect(event) {
|
|
120
|
+
let orderId = event.orderId;
|
|
121
|
+
collectCalls.push(orderId);
|
|
122
|
+
return [[
|
|
123
|
+
orderId,
|
|
124
|
+
{
|
|
125
|
+
orderId: orderId
|
|
126
|
+
}
|
|
127
|
+
]];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function translate(_id, item) {
|
|
131
|
+
externalCalls.push(item.orderId);
|
|
132
|
+
return {
|
|
133
|
+
TAG: "Ok",
|
|
134
|
+
_0: undefined
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let SendConfirmTranslation = {
|
|
139
|
+
collect: collect,
|
|
140
|
+
translate: translate,
|
|
141
|
+
moduleUrl: moduleUrl$3
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
145
|
+
|
|
146
|
+
TestRunner$ReventlessLocal.setup();
|
|
147
|
+
|
|
148
|
+
let DcbLogMaker = DcbEventLog_Builder$ReventlessLocal.Make(Bus);
|
|
149
|
+
|
|
150
|
+
let dcbEventLog = DcbLogMaker.make("TestLog", undefined, {
|
|
151
|
+
TAG: "Simple",
|
|
152
|
+
_0: {
|
|
153
|
+
key: "orderId"
|
|
154
|
+
}
|
|
155
|
+
}, undefined);
|
|
156
|
+
|
|
157
|
+
async function publishJsons(cmdJsons) {
|
|
158
|
+
await Promise.all(cmdJsons.map(async cmdJson => {
|
|
159
|
+
let dict = cmdJson.commandJson;
|
|
160
|
+
let typeName;
|
|
161
|
+
typeName = typeof dict === "object" && dict !== null && !Array.isArray(dict) ? Stdlib_Option.getOr(Stdlib_Option.flatMap(dict["TAG"], j => {
|
|
162
|
+
if (typeof j === "string") {
|
|
163
|
+
return j;
|
|
164
|
+
}
|
|
165
|
+
}), "") : "";
|
|
166
|
+
let fullBody = Object.fromEntries([
|
|
167
|
+
[
|
|
168
|
+
"id",
|
|
169
|
+
cmdJson.id
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"meta",
|
|
173
|
+
S.reverseConvertToJsonOrThrow(cmdJson.meta, Message$Reventless.metaSchema)
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
"command",
|
|
177
|
+
cmdJson.commandJson
|
|
178
|
+
]
|
|
179
|
+
]);
|
|
180
|
+
let handlers = CommandTopic$ReventlessCore.getHandlers(typeName);
|
|
181
|
+
await Promise.all(handlers.map(async entry => {
|
|
182
|
+
let item_reference = cmdJson.id;
|
|
183
|
+
let item = {
|
|
184
|
+
command: fullBody,
|
|
185
|
+
reference: item_reference
|
|
186
|
+
};
|
|
187
|
+
await Effect.runPromise(entry.handler(Stream.fromIterable([item])));
|
|
188
|
+
}));
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let publishJsonsOutput = Pulumi.output(publishJsons);
|
|
193
|
+
|
|
194
|
+
let PlaceMaker = StateChangeSlice_Builder$ReventlessLocal.Make({
|
|
195
|
+
name: name,
|
|
196
|
+
moduleUrl: moduleUrl,
|
|
197
|
+
Id: Id$Reventless.$$String,
|
|
198
|
+
consumedEventSchema: consumedEventSchema,
|
|
199
|
+
errorSchema: errorSchema,
|
|
200
|
+
eventSchema: eventSchema,
|
|
201
|
+
commandSchema: commandSchema,
|
|
202
|
+
commandAuthorization: commandAuthorization,
|
|
203
|
+
readConsistency: "EscalateOnRetry"
|
|
204
|
+
})({
|
|
205
|
+
initialState: false,
|
|
206
|
+
evolve: evolve,
|
|
207
|
+
decide: decide,
|
|
208
|
+
moduleUrl: moduleUrl$1
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
let _placeSlice = PlaceMaker.make(dcbEventLog, publishJsonsOutput, undefined, undefined, undefined, undefined);
|
|
212
|
+
|
|
213
|
+
let dcbTopicOutputs = Component$ReventlessInfra.outputs(dcbEventLog).eventTopic;
|
|
214
|
+
|
|
215
|
+
let OutboundMaker = OutboundTranslationSlice_Builder$ReventlessLocal.Make(Bus);
|
|
216
|
+
|
|
217
|
+
let SendConfirm = OutboundMaker.Make(SendConfirmSpec)(SendConfirmTranslation);
|
|
218
|
+
|
|
219
|
+
let sendConfirmSlice = SendConfirm.make(dcbEventLog, publishJsonsOutput, undefined, undefined);
|
|
220
|
+
|
|
221
|
+
function placeCmdJson(orderId) {
|
|
222
|
+
return {
|
|
223
|
+
id: orderId,
|
|
224
|
+
meta: TestFixtures$ReventlessLocal.testMeta,
|
|
225
|
+
commandJson: S.reverseConvertToJsonOrThrow({
|
|
226
|
+
TAG: "Place",
|
|
227
|
+
orderId: orderId,
|
|
228
|
+
customerId: "cust-" + orderId
|
|
229
|
+
}, commandSchema)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function readEventTypes(orderId) {
|
|
234
|
+
let logOps = await TestRunner$ReventlessLocal.resolve(DcbLogMaker.operations(dcbEventLog));
|
|
235
|
+
let result = await logOps.read([{
|
|
236
|
+
tags: [{
|
|
237
|
+
key: "orderId",
|
|
238
|
+
value: orderId
|
|
239
|
+
}]
|
|
240
|
+
}], undefined);
|
|
241
|
+
return result.events.map(e => e.eventType);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function flush() {
|
|
245
|
+
await Promise.resolve();
|
|
246
|
+
await Promise.resolve();
|
|
247
|
+
await Promise.resolve();
|
|
248
|
+
await Promise.resolve();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export {
|
|
252
|
+
PlaceSpec,
|
|
253
|
+
PlaceBehavior,
|
|
254
|
+
SendConfirmSpec,
|
|
255
|
+
collectCalls,
|
|
256
|
+
externalCalls,
|
|
257
|
+
SendConfirmTranslation,
|
|
258
|
+
Bus,
|
|
259
|
+
DcbLogMaker,
|
|
260
|
+
dcbEventLog,
|
|
261
|
+
publishJsons,
|
|
262
|
+
publishJsonsOutput,
|
|
263
|
+
PlaceMaker,
|
|
264
|
+
_placeSlice,
|
|
265
|
+
dcbTopicOutputs,
|
|
266
|
+
OutboundMaker,
|
|
267
|
+
SendConfirm,
|
|
268
|
+
sendConfirmSlice,
|
|
269
|
+
placeCmdJson,
|
|
270
|
+
readEventTypes,
|
|
271
|
+
flush,
|
|
272
|
+
}
|
|
273
|
+
/* moduleUrl Not a pure module */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Platform-level regression test for OutboundTranslationSlice wiring.
|
|
2
|
+
//
|
|
3
|
+
// The callback test next door exercises phase1/phase2 directly and stays green
|
|
4
|
+
// while the component never receives an event. This one publishes a command
|
|
5
|
+
// through the real chain — StateChangeSlice → DcbEventLog → event topic →
|
|
6
|
+
// EventCollector → phase 1 → phase 2 — and asserts the external call lands.
|
|
7
|
+
|
|
8
|
+
open JestGlobals
|
|
9
|
+
open OutboundTranslationSlicePlatformFixtures
|
|
10
|
+
|
|
11
|
+
describe("OutboundTranslationSlice platform wiring:", () => {
|
|
12
|
+
let _ = beforeAllAsync(async () => {
|
|
13
|
+
let _ = await dcbEventLog->DcbLogMaker.operations->TestRunner.resolve
|
|
14
|
+
let _ = await sendConfirmSlice->SendConfirm.operations->TestRunner.resolve
|
|
15
|
+
// LocalEventCollectorChannel.connect registers the bus subscription inside a
|
|
16
|
+
// fire-and-forget Effect.runPromise; flush so it is in place before publishing.
|
|
17
|
+
let _ = await flush()
|
|
18
|
+
let resource = dcbTopicOutputs.resources->Array.getUnsafe(0)
|
|
19
|
+
let _ = await resource.name->TestRunner.resolve
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
testPromise("Place → Placed reaches phase 1 collect", async () => {
|
|
23
|
+
let _ = await publishJsons([placeCmdJson("order-1")])
|
|
24
|
+
let _ = await flush()
|
|
25
|
+
|
|
26
|
+
let eventTypes = await readEventTypes("order-1")
|
|
27
|
+
expect(eventTypes->Array.includes("Placed"))->toBe(true)
|
|
28
|
+
|
|
29
|
+
expect(collectCalls->Array.includes("order-1"))->toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
testPromise("phase 2 translates the collected item without the heartbeat", async () => {
|
|
33
|
+
let _ = await publishJsons([placeCmdJson("order-2")])
|
|
34
|
+
let _ = await flush()
|
|
35
|
+
|
|
36
|
+
expect(externalCalls->Array.includes("order-2"))->toBe(true)
|
|
37
|
+
})
|
|
38
|
+
})
|
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformTest.res.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
|
|
4
|
+
import * as OutboundTranslationSlicePlatformFixtures$ReventlessLocal from "./OutboundTranslationSlicePlatformFixtures.res.mjs";
|
|
5
|
+
|
|
6
|
+
globalThis.describe("OutboundTranslationSlice platform wiring:", () => {
|
|
7
|
+
globalThis.beforeAll(async () => {
|
|
8
|
+
await TestRunner$ReventlessLocal.resolve(OutboundTranslationSlicePlatformFixtures$ReventlessLocal.DcbLogMaker.operations(OutboundTranslationSlicePlatformFixtures$ReventlessLocal.dcbEventLog));
|
|
9
|
+
await TestRunner$ReventlessLocal.resolve(OutboundTranslationSlicePlatformFixtures$ReventlessLocal.SendConfirm.operations(OutboundTranslationSlicePlatformFixtures$ReventlessLocal.sendConfirmSlice));
|
|
10
|
+
await OutboundTranslationSlicePlatformFixtures$ReventlessLocal.flush();
|
|
11
|
+
let resource = OutboundTranslationSlicePlatformFixtures$ReventlessLocal.dcbTopicOutputs.resources[0];
|
|
12
|
+
await TestRunner$ReventlessLocal.resolve(resource.name);
|
|
13
|
+
});
|
|
14
|
+
globalThis.test("Place → Placed reaches phase 1 collect", async () => {
|
|
15
|
+
await OutboundTranslationSlicePlatformFixtures$ReventlessLocal.publishJsons([OutboundTranslationSlicePlatformFixtures$ReventlessLocal.placeCmdJson("order-1")]);
|
|
16
|
+
await OutboundTranslationSlicePlatformFixtures$ReventlessLocal.flush();
|
|
17
|
+
let eventTypes = await OutboundTranslationSlicePlatformFixtures$ReventlessLocal.readEventTypes("order-1");
|
|
18
|
+
globalThis.expect(eventTypes.includes("Placed")).toBe(true);
|
|
19
|
+
globalThis.expect(OutboundTranslationSlicePlatformFixtures$ReventlessLocal.collectCalls.includes("order-1")).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
globalThis.test("phase 2 translates the collected item without the heartbeat", async () => {
|
|
22
|
+
await OutboundTranslationSlicePlatformFixtures$ReventlessLocal.publishJsons([OutboundTranslationSlicePlatformFixtures$ReventlessLocal.placeCmdJson("order-2")]);
|
|
23
|
+
await OutboundTranslationSlicePlatformFixtures$ReventlessLocal.flush();
|
|
24
|
+
globalThis.expect(OutboundTranslationSlicePlatformFixtures$ReventlessLocal.externalCalls.includes("order-2")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/* Not a pure module */
|
|
@@ -42,7 +42,7 @@ module ItemsViewProjection = {
|
|
|
42
42
|
|
|
43
43
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
44
44
|
|
|
45
|
-
let project = (event: consumedEvent) =>
|
|
45
|
+
let project = ({event}: Reventless.StateViewSlice.consumed<consumedEvent>) =>
|
|
46
46
|
switch event {
|
|
47
47
|
| ItemAdded({id, name}) => [Set(id, {id, name})]
|
|
48
48
|
| ItemRenamed({id, name}) => [Update(id, s => {...s, name})]
|
|
@@ -44,7 +44,7 @@ module ScoresViewProjection = {
|
|
|
44
44
|
|
|
45
45
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
46
46
|
|
|
47
|
-
let project = (event: consumedEvent) =>
|
|
47
|
+
let project = ({event}: Reventless.StateViewSlice.consumed<consumedEvent>) =>
|
|
48
48
|
switch event {
|
|
49
49
|
| ScoreRecorded({id, category, date, score}) =>
|
|
50
50
|
[Set(id, {id, category, date, score})]
|