@reventlessdev/reventless-local 3.0.0-alpha.167 → 3.0.0-alpha.168
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 +13 -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 +6 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +6 -0
- 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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodehttp from "node:http";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
7
|
+
import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
|
|
8
|
+
|
|
9
|
+
TestRunner$ReventlessLocal.setup();
|
|
10
|
+
|
|
11
|
+
function postJson(path, body) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let bodyStr = JSON.stringify(body);
|
|
14
|
+
let req = Nodehttp.request({
|
|
15
|
+
hostname: "localhost",
|
|
16
|
+
port: 49322,
|
|
17
|
+
path: path,
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: Object.fromEntries([
|
|
20
|
+
[
|
|
21
|
+
"content-type",
|
|
22
|
+
"application/json"
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"content-length",
|
|
26
|
+
bodyStr.length.toString()
|
|
27
|
+
]
|
|
28
|
+
])
|
|
29
|
+
}, res => {
|
|
30
|
+
let status = res.statusCode;
|
|
31
|
+
let buf = {
|
|
32
|
+
contents: ""
|
|
33
|
+
};
|
|
34
|
+
res.setEncoding("utf8");
|
|
35
|
+
res.on("data", chunk => {
|
|
36
|
+
buf.contents = buf.contents + chunk;
|
|
37
|
+
});
|
|
38
|
+
res.on("end", () => {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(buf.contents);
|
|
42
|
+
} catch (exn) {
|
|
43
|
+
parsed = null;
|
|
44
|
+
}
|
|
45
|
+
resolve([
|
|
46
|
+
status,
|
|
47
|
+
parsed
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
req.on("error", err => reject(err));
|
|
52
|
+
req.write(bodyStr);
|
|
53
|
+
req.end(null);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function putRaw(path, body, contentType) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let req = Nodehttp.request({
|
|
60
|
+
hostname: "localhost",
|
|
61
|
+
port: 49322,
|
|
62
|
+
path: path,
|
|
63
|
+
method: "PUT",
|
|
64
|
+
headers: Object.fromEntries([
|
|
65
|
+
[
|
|
66
|
+
"content-type",
|
|
67
|
+
contentType
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"content-length",
|
|
71
|
+
body.length.toString()
|
|
72
|
+
]
|
|
73
|
+
])
|
|
74
|
+
}, res => {
|
|
75
|
+
let status = res.statusCode;
|
|
76
|
+
res.on("data", param => {});
|
|
77
|
+
res.on("end", () => resolve(status));
|
|
78
|
+
});
|
|
79
|
+
req.on("error", err => reject(err));
|
|
80
|
+
req.write(body);
|
|
81
|
+
req.end(null);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getRaw(path) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
let req = Nodehttp.request({
|
|
88
|
+
hostname: "localhost",
|
|
89
|
+
port: 49322,
|
|
90
|
+
path: path,
|
|
91
|
+
method: "GET",
|
|
92
|
+
headers: {}
|
|
93
|
+
}, res => {
|
|
94
|
+
let status = res.statusCode;
|
|
95
|
+
let contentType = Stdlib_Option.getOr(res.headers["content-type"], "");
|
|
96
|
+
let buf = {
|
|
97
|
+
contents: ""
|
|
98
|
+
};
|
|
99
|
+
res.setEncoding("utf8");
|
|
100
|
+
res.on("data", chunk => {
|
|
101
|
+
buf.contents = buf.contents + chunk;
|
|
102
|
+
});
|
|
103
|
+
res.on("end", () => resolve([
|
|
104
|
+
status,
|
|
105
|
+
buf.contents,
|
|
106
|
+
contentType
|
|
107
|
+
]));
|
|
108
|
+
});
|
|
109
|
+
req.on("error", err => reject(err));
|
|
110
|
+
req.end(null);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getString(j, k) {
|
|
115
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(j), d => d[k]), Stdlib_JSON.Decode.string), "");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
globalThis.beforeAll(async () => {
|
|
119
|
+
DomainGraphQL_Server$ReventlessLocal.reset();
|
|
120
|
+
DomainGraphQL_Server$ReventlessLocal.start(49322, undefined, undefined);
|
|
121
|
+
return await new Promise((resolve, param) => {
|
|
122
|
+
setTimeout(() => resolve(), 50);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
globalThis.afterAll(() => DomainGraphQL_Server$ReventlessLocal.stop());
|
|
127
|
+
|
|
128
|
+
globalThis.test("POST /__inmemory/upload returns matching uploadUrl + storageRef under the prefix", async () => {
|
|
129
|
+
let match = await postJson("/__inmemory/upload", Object.fromEntries([
|
|
130
|
+
[
|
|
131
|
+
"fileName",
|
|
132
|
+
"logo.svg"
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
"contentType",
|
|
136
|
+
"image/svg+xml"
|
|
137
|
+
]
|
|
138
|
+
]));
|
|
139
|
+
let body = match[1];
|
|
140
|
+
globalThis.expect(match[0]).toEqual(200);
|
|
141
|
+
let uploadUrl = getString(body, "uploadUrl");
|
|
142
|
+
let storageRef = getString(body, "storageRef");
|
|
143
|
+
globalThis.expect(uploadUrl).toEqual(storageRef);
|
|
144
|
+
globalThis.expect(storageRef.startsWith("/uploads/")).toEqual(true);
|
|
145
|
+
globalThis.expect(storageRef.endsWith("/logo.svg")).toEqual(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
globalThis.test("PUT then GET round-trips the bytes and content-type", async () => {
|
|
149
|
+
let match = await postJson("/__inmemory/upload", Object.fromEntries([[
|
|
150
|
+
"fileName",
|
|
151
|
+
"pixel.svg"
|
|
152
|
+
]]));
|
|
153
|
+
let ref = getString(match[1], "storageRef");
|
|
154
|
+
let svg = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'></svg>";
|
|
155
|
+
let putStatus = await putRaw(ref, svg, "image/svg+xml");
|
|
156
|
+
globalThis.expect(putStatus).toEqual(200);
|
|
157
|
+
let match$1 = await getRaw(ref);
|
|
158
|
+
globalThis.expect(match$1[0]).toEqual(200);
|
|
159
|
+
globalThis.expect(match$1[1]).toEqual(svg);
|
|
160
|
+
globalThis.expect(match$1[2]).toEqual("image/svg+xml");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
globalThis.test("GET a missing served object returns 404", async () => {
|
|
164
|
+
let match = await getRaw("/uploads/missing/none.svg");
|
|
165
|
+
globalThis.expect(match[0]).toEqual(404);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let port = 49322;
|
|
169
|
+
|
|
170
|
+
export {
|
|
171
|
+
port,
|
|
172
|
+
postJson,
|
|
173
|
+
putRaw,
|
|
174
|
+
getRaw,
|
|
175
|
+
getString,
|
|
176
|
+
}
|
|
177
|
+
/* Not a pure module */
|
|
@@ -34,9 +34,11 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
34
34
|
let result = await Callback.receive(mockPublish, inputJson)
|
|
35
35
|
|
|
36
36
|
switch result {
|
|
37
|
-
| Ok(targetIds) =>
|
|
38
|
-
expect(targetIds
|
|
39
|
-
expect(
|
|
37
|
+
| Ok({requestId, targetIds, commandCount}) =>
|
|
38
|
+
expect(targetIds)->toEqual(["ord-1"])
|
|
39
|
+
expect(commandCount)->toBe(1)
|
|
40
|
+
// requestId keys the audit row, so the caller can correlate the response.
|
|
41
|
+
expect(Callback.auditLog->Dict.get(requestId)->Option.isSome)->toBe(true)
|
|
40
42
|
| Error(_) => expect(true)->toBe(false)
|
|
41
43
|
}
|
|
42
44
|
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
@@ -67,7 +69,7 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
67
69
|
let result = await Callback.receive(mockPublish, inputJson)
|
|
68
70
|
|
|
69
71
|
switch result {
|
|
70
|
-
| Error(
|
|
72
|
+
| Error({error}) => expect(error)->toBe("Unknown payment status: pending")
|
|
71
73
|
| Ok(_) => expect(true)->toBe(false)
|
|
72
74
|
}
|
|
73
75
|
expect(publishedCommands.contents->Array.length)->toBe(0)
|
|
@@ -112,7 +114,7 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
112
114
|
let result = await Callback.receive(failingPublish, inputJson)
|
|
113
115
|
|
|
114
116
|
switch result {
|
|
115
|
-
| Error(
|
|
117
|
+
| Error({error}) => expect(error)->toBe("publish failed")
|
|
116
118
|
| Ok(_) => expect(true)->toBe(false)
|
|
117
119
|
}
|
|
118
120
|
|
|
@@ -177,9 +179,9 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
177
179
|
let result = await MultiCallback.receive(mockPublish, inputJson)
|
|
178
180
|
|
|
179
181
|
switch result {
|
|
180
|
-
| Ok(targetIds) =>
|
|
181
|
-
expect(targetIds
|
|
182
|
-
expect(
|
|
182
|
+
| Ok({targetIds, commandCount}) =>
|
|
183
|
+
expect(targetIds)->toEqual(["ord-1", "ord-1", "ord-1"])
|
|
184
|
+
expect(commandCount)->toBe(3)
|
|
183
185
|
| Error(_) => expect(true)->toBe(false)
|
|
184
186
|
}
|
|
185
187
|
// All 3 commands published in one batch
|
|
@@ -234,7 +236,9 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
234
236
|
let result = await EmptyCallback.receive(mockPublish, inputJson)
|
|
235
237
|
|
|
236
238
|
switch result {
|
|
237
|
-
| Ok(targetIds) =>
|
|
239
|
+
| Ok({targetIds, commandCount}) =>
|
|
240
|
+
expect(targetIds)->toEqual([])
|
|
241
|
+
expect(commandCount)->toBe(0)
|
|
238
242
|
| Error(_) => expect(true)->toBe(false)
|
|
239
243
|
}
|
|
240
244
|
expect(publishedCommands.contents->Array.length)->toBe(0)
|
package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
7
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
7
8
|
import * as InboundTranslationSliceFixtures$ReventlessLocal from "./InboundTranslationSliceFixtures.res.mjs";
|
|
@@ -45,9 +46,10 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
45
46
|
};
|
|
46
47
|
let result = await Callback.receive(mockPublish, inputJson);
|
|
47
48
|
if (result.TAG === "Ok") {
|
|
48
|
-
let
|
|
49
|
-
globalThis.expect(targetIds
|
|
50
|
-
globalThis.expect(
|
|
49
|
+
let match = result._0;
|
|
50
|
+
globalThis.expect(match.targetIds).toEqual(["ord-1"]);
|
|
51
|
+
globalThis.expect(match.commandCount).toBe(1);
|
|
52
|
+
globalThis.expect(Stdlib_Option.isSome(Callback.auditLog[match.requestId])).toBe(true);
|
|
51
53
|
} else {
|
|
52
54
|
globalThis.expect(true).toBe(false);
|
|
53
55
|
}
|
|
@@ -56,8 +58,8 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
56
58
|
globalThis.expect(cmd.id).toBe("ord-1");
|
|
57
59
|
let auditEntries = Object.entries(Callback.auditLog);
|
|
58
60
|
globalThis.expect(auditEntries.length).toBe(1);
|
|
59
|
-
let match = auditEntries[0];
|
|
60
|
-
let auditRow = match[1];
|
|
61
|
+
let match$1 = auditEntries[0];
|
|
62
|
+
let auditRow = match$1[1];
|
|
61
63
|
globalThis.expect(auditRow.status).toBe("Success");
|
|
62
64
|
globalThis.expect(auditRow.commandCount).toBe(1);
|
|
63
65
|
});
|
|
@@ -77,7 +79,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
77
79
|
if (result.TAG === "Ok") {
|
|
78
80
|
globalThis.expect(true).toBe(false);
|
|
79
81
|
} else {
|
|
80
|
-
globalThis.expect(result._0).toBe("Unknown payment status: pending");
|
|
82
|
+
globalThis.expect(result._0.error).toBe("Unknown payment status: pending");
|
|
81
83
|
}
|
|
82
84
|
globalThis.expect(publishedCommands.contents.length).toBe(0);
|
|
83
85
|
let auditEntries = Object.entries(Callback.auditLog);
|
|
@@ -112,7 +114,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
112
114
|
if (result.TAG === "Ok") {
|
|
113
115
|
globalThis.expect(true).toBe(false);
|
|
114
116
|
} else {
|
|
115
|
-
globalThis.expect(result._0).toBe("publish failed");
|
|
117
|
+
globalThis.expect(result._0.error).toBe("publish failed");
|
|
116
118
|
}
|
|
117
119
|
let auditEntries = Object.entries(Callback.auditLog);
|
|
118
120
|
globalThis.expect(auditEntries.length).toBe(1);
|
|
@@ -173,17 +175,21 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
173
175
|
};
|
|
174
176
|
let result = await MultiCallback.receive(mockPublish, inputJson);
|
|
175
177
|
if (result.TAG === "Ok") {
|
|
176
|
-
let
|
|
177
|
-
globalThis.expect(targetIds
|
|
178
|
-
|
|
178
|
+
let match = result._0;
|
|
179
|
+
globalThis.expect(match.targetIds).toEqual([
|
|
180
|
+
"ord-1",
|
|
181
|
+
"ord-1",
|
|
182
|
+
"ord-1"
|
|
183
|
+
]);
|
|
184
|
+
globalThis.expect(match.commandCount).toBe(3);
|
|
179
185
|
} else {
|
|
180
186
|
globalThis.expect(true).toBe(false);
|
|
181
187
|
}
|
|
182
188
|
globalThis.expect(publishedCommands.contents.length).toBe(3);
|
|
183
189
|
let auditEntries = Object.entries(MultiCallback.auditLog);
|
|
184
190
|
globalThis.expect(auditEntries.length).toBe(1);
|
|
185
|
-
let match = auditEntries[0];
|
|
186
|
-
let auditRow = match[1];
|
|
191
|
+
let match$1 = auditEntries[0];
|
|
192
|
+
let auditRow = match$1[1];
|
|
187
193
|
globalThis.expect(auditRow.status).toBe("Success");
|
|
188
194
|
globalThis.expect(auditRow.commandCount).toBe(3);
|
|
189
195
|
});
|
|
@@ -226,7 +232,9 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
226
232
|
};
|
|
227
233
|
let result = await EmptyCallback.receive(mockPublish, inputJson);
|
|
228
234
|
if (result.TAG === "Ok") {
|
|
229
|
-
|
|
235
|
+
let match = result._0;
|
|
236
|
+
globalThis.expect(match.targetIds).toEqual([]);
|
|
237
|
+
globalThis.expect(match.commandCount).toBe(0);
|
|
230
238
|
} else {
|
|
231
239
|
globalThis.expect(true).toBe(false);
|
|
232
240
|
}
|
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
|
+
}
|