@reventlessdev/reventless-gwt 1.0.0-alpha.238 → 1.0.0-alpha.239
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 +10 -0
- package/package.json +4 -4
- package/src/Automation_GWT.res +130 -0
- package/src/Automation_GWT.res.mjs +193 -0
- package/src/Capabilities_Fake.res +157 -0
- package/src/Capabilities_Fake.res.mjs +254 -0
- package/src/Flow_GWT.res +112 -1
- package/src/Flow_GWT.res.mjs +165 -11
- package/src/InboundTranslation_GWT.res +112 -0
- package/src/InboundTranslation_GWT.res.mjs +190 -1
- package/src/OutboundTranslation_GWT.res +207 -4
- package/src/OutboundTranslation_GWT.res.mjs +257 -1
- package/tests/AutomationFromSliceGwtTest.res +145 -0
- package/tests/AutomationFromSliceGwtTest.res.mjs +353 -0
- package/tests/InboundFromSliceGwtTest.res +63 -0
- package/tests/InboundFromSliceGwtTest.res.mjs +164 -0
- package/tests/InboundTranslationGwtTest.res.mjs +0 -5
- package/tests/OutboundFromSliceGwtTest.res +124 -0
- package/tests/OutboundFromSliceGwtTest.res.mjs +268 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
|
+
import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
|
|
6
|
+
import * as Automation_GWT$ReventlessGwt from "../src/Automation_GWT.res.mjs";
|
|
7
|
+
|
|
8
|
+
let todoItemSchema = Sury.$schema(s => ({
|
|
9
|
+
orderId: s.m(Sury.string)
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
let commandSchema = Sury.union([
|
|
13
|
+
Sury.$schema(s => ({
|
|
14
|
+
TAG: "Ship",
|
|
15
|
+
orderId: s.m(Sury.string)
|
|
16
|
+
})),
|
|
17
|
+
Sury.$schema(s => ({
|
|
18
|
+
TAG: "GiveUp",
|
|
19
|
+
orderId: s.m(Sury.string)
|
|
20
|
+
}))
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function commandAuthorization(param) {
|
|
24
|
+
return "AllowAuthenticated";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function commandTransition(param) {
|
|
28
|
+
return "Undeclared";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let traits = [];
|
|
32
|
+
|
|
33
|
+
let ShipPaid = {
|
|
34
|
+
name: "ShipPaid",
|
|
35
|
+
moduleUrl: "",
|
|
36
|
+
todoItemSchema: todoItemSchema,
|
|
37
|
+
commandSchema: commandSchema,
|
|
38
|
+
maxRetries: 3,
|
|
39
|
+
heartbeatInterval: 60,
|
|
40
|
+
targetName: "Order",
|
|
41
|
+
commandAuthorization: commandAuthorization,
|
|
42
|
+
commandTransition: commandTransition,
|
|
43
|
+
traits: traits
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let M = AutomationSlice$Reventless.Mappings.Make(ShipPaid);
|
|
47
|
+
|
|
48
|
+
let moduleUrl = "";
|
|
49
|
+
|
|
50
|
+
let name = "Orders";
|
|
51
|
+
|
|
52
|
+
let eventSchema = Sury.union([
|
|
53
|
+
Sury.$schema(s => ({
|
|
54
|
+
TAG: "OrderPlaced",
|
|
55
|
+
orderId: s.m(Sury.string)
|
|
56
|
+
})),
|
|
57
|
+
Sury.$schema(s => ({
|
|
58
|
+
TAG: "OrderCancelled",
|
|
59
|
+
orderId: s.m(Sury.string)
|
|
60
|
+
}))
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
let Orders = {
|
|
64
|
+
Id: undefined,
|
|
65
|
+
name: name,
|
|
66
|
+
eventSchema: eventSchema
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let name$1 = "Payments";
|
|
70
|
+
|
|
71
|
+
let eventSchema$1 = Sury.union([
|
|
72
|
+
Sury.$schema(s => ({
|
|
73
|
+
TAG: "Captured",
|
|
74
|
+
amount: s.m(Sury.int32)
|
|
75
|
+
})),
|
|
76
|
+
Sury.$schema(s => ({
|
|
77
|
+
TAG: "Refunded",
|
|
78
|
+
amount: s.m(Sury.int32)
|
|
79
|
+
}))
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
let Payments = {
|
|
83
|
+
Id: undefined,
|
|
84
|
+
name: name$1,
|
|
85
|
+
eventSchema: eventSchema$1
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function collect(event, param, _ctx) {
|
|
89
|
+
if (event.TAG !== "OrderPlaced") {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
let orderId = event.orderId;
|
|
93
|
+
return [[
|
|
94
|
+
orderId,
|
|
95
|
+
{
|
|
96
|
+
orderId: orderId
|
|
97
|
+
}
|
|
98
|
+
]];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolve(event) {
|
|
102
|
+
if (event.TAG === "OrderPlaced") {
|
|
103
|
+
return;
|
|
104
|
+
} else {
|
|
105
|
+
return event.orderId;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let FromOrders = AutomationSlice$Reventless.Mapping.Make({
|
|
110
|
+
Id: Id$Reventless.$$String,
|
|
111
|
+
name: name,
|
|
112
|
+
eventSchema: eventSchema
|
|
113
|
+
})(ShipPaid)({
|
|
114
|
+
collect: collect,
|
|
115
|
+
resolve: resolve
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function collect$1(event, sourceId, _ctx) {
|
|
119
|
+
if (event.TAG === "Captured") {
|
|
120
|
+
return [[
|
|
121
|
+
sourceId,
|
|
122
|
+
{
|
|
123
|
+
orderId: sourceId
|
|
124
|
+
}
|
|
125
|
+
]];
|
|
126
|
+
} else {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolve$1(event) {
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let FromPayments = AutomationSlice$Reventless.Mapping.Make({
|
|
136
|
+
Id: Id$Reventless.$$String,
|
|
137
|
+
name: name$1,
|
|
138
|
+
eventSchema: eventSchema$1
|
|
139
|
+
})(ShipPaid)({
|
|
140
|
+
collect: collect$1,
|
|
141
|
+
resolve: resolve$1
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let mappings = [
|
|
145
|
+
FromOrders,
|
|
146
|
+
FromPayments
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
function process(id, _item) {
|
|
150
|
+
return [
|
|
151
|
+
id,
|
|
152
|
+
{
|
|
153
|
+
TAG: "Ship",
|
|
154
|
+
orderId: id
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onExhausted(id, _item) {
|
|
160
|
+
return [
|
|
161
|
+
id,
|
|
162
|
+
{
|
|
163
|
+
TAG: "GiveUp",
|
|
164
|
+
orderId: id
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let ShipPaid_Automation = {
|
|
170
|
+
M: M,
|
|
171
|
+
moduleUrl: moduleUrl,
|
|
172
|
+
Orders: Orders,
|
|
173
|
+
Payments: Payments,
|
|
174
|
+
FromOrders: FromOrders,
|
|
175
|
+
FromPayments: FromPayments,
|
|
176
|
+
mappings: mappings,
|
|
177
|
+
process: process,
|
|
178
|
+
onExhausted: onExhausted
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let include = Automation_GWT$ReventlessGwt.FromSlice(ShipPaid)({
|
|
182
|
+
process: process,
|
|
183
|
+
onExhausted: onExhausted,
|
|
184
|
+
moduleUrl: moduleUrl,
|
|
185
|
+
mappings: mappings
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
let describe = include.describe;
|
|
189
|
+
|
|
190
|
+
let test = include.test;
|
|
191
|
+
|
|
192
|
+
let thenTodos = include.thenTodos;
|
|
193
|
+
|
|
194
|
+
let thenResolved = include.thenResolved;
|
|
195
|
+
|
|
196
|
+
let Mapping = include.Mapping;
|
|
197
|
+
|
|
198
|
+
let givenTodo = include.givenTodo;
|
|
199
|
+
|
|
200
|
+
let whenProcess = include.whenProcess;
|
|
201
|
+
|
|
202
|
+
let whenExhausted = include.whenExhausted;
|
|
203
|
+
|
|
204
|
+
let thenCommand = include.thenCommand;
|
|
205
|
+
|
|
206
|
+
let givenEvents = include.givenEvents;
|
|
207
|
+
|
|
208
|
+
let whenSweep = include.whenSweep;
|
|
209
|
+
|
|
210
|
+
let thenCommands = include.thenCommands;
|
|
211
|
+
|
|
212
|
+
let andThenEvents = include.andThenEvents;
|
|
213
|
+
|
|
214
|
+
let thenScenarioTodos = include.thenScenarioTodos;
|
|
215
|
+
|
|
216
|
+
let O = Mapping(FromOrders);
|
|
217
|
+
|
|
218
|
+
let P = Mapping(FromPayments);
|
|
219
|
+
|
|
220
|
+
describe("Automation_GWT.FromSlice", () => {
|
|
221
|
+
test("collect through one mapping", () => thenTodos(O.whenCollect(O.givenEvent({
|
|
222
|
+
TAG: "OrderPlaced",
|
|
223
|
+
orderId: "o1"
|
|
224
|
+
}), undefined, undefined), [[
|
|
225
|
+
"o1",
|
|
226
|
+
{
|
|
227
|
+
orderId: "o1"
|
|
228
|
+
}
|
|
229
|
+
]]));
|
|
230
|
+
test("collect keys by the source id where the event does not name it", () => thenTodos(P.whenCollect(P.givenEvent({
|
|
231
|
+
TAG: "Captured",
|
|
232
|
+
amount: 5
|
|
233
|
+
}), "o2", undefined), [[
|
|
234
|
+
"o2",
|
|
235
|
+
{
|
|
236
|
+
orderId: "o2"
|
|
237
|
+
}
|
|
238
|
+
]]));
|
|
239
|
+
test("resolve through one mapping", () => thenResolved(O.whenResolve(O.givenEvent({
|
|
240
|
+
TAG: "OrderCancelled",
|
|
241
|
+
orderId: "o1"
|
|
242
|
+
})), "o1"));
|
|
243
|
+
test("process", () => thenCommand(whenProcess(givenTodo("o1", {
|
|
244
|
+
orderId: "o1"
|
|
245
|
+
})), "o1", {
|
|
246
|
+
TAG: "Ship",
|
|
247
|
+
orderId: "o1"
|
|
248
|
+
}));
|
|
249
|
+
test("exhausted: onExhausted is what the domain hears", () => thenCommand(whenExhausted(givenTodo("o1", {
|
|
250
|
+
orderId: "o1"
|
|
251
|
+
})), "o1", {
|
|
252
|
+
TAG: "GiveUp",
|
|
253
|
+
orderId: "o1"
|
|
254
|
+
}));
|
|
255
|
+
test("sweep: both sources route by name, in event order", () => thenCommands(whenSweep(givenEvents([
|
|
256
|
+
P.event({
|
|
257
|
+
TAG: "Captured",
|
|
258
|
+
amount: 5
|
|
259
|
+
}, "o2"),
|
|
260
|
+
O.event({
|
|
261
|
+
TAG: "OrderPlaced",
|
|
262
|
+
orderId: "o1"
|
|
263
|
+
}, undefined)
|
|
264
|
+
]), undefined), [
|
|
265
|
+
[
|
|
266
|
+
"o2",
|
|
267
|
+
{
|
|
268
|
+
TAG: "Ship",
|
|
269
|
+
orderId: "o2"
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"o1",
|
|
274
|
+
{
|
|
275
|
+
TAG: "Ship",
|
|
276
|
+
orderId: "o1"
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
]));
|
|
280
|
+
test("sweep: a resolve completes only a row that exists", () => thenScenarioTodos(whenSweep(givenEvents([
|
|
281
|
+
O.event({
|
|
282
|
+
TAG: "OrderCancelled",
|
|
283
|
+
orderId: "o1"
|
|
284
|
+
}, undefined),
|
|
285
|
+
O.event({
|
|
286
|
+
TAG: "OrderPlaced",
|
|
287
|
+
orderId: "o1"
|
|
288
|
+
}, undefined),
|
|
289
|
+
O.event({
|
|
290
|
+
TAG: "OrderPlaced",
|
|
291
|
+
orderId: "o3"
|
|
292
|
+
}, undefined),
|
|
293
|
+
O.event({
|
|
294
|
+
TAG: "OrderCancelled",
|
|
295
|
+
orderId: "o3"
|
|
296
|
+
}, undefined)
|
|
297
|
+
]), undefined), [[
|
|
298
|
+
"o1",
|
|
299
|
+
{
|
|
300
|
+
orderId: "o1"
|
|
301
|
+
}
|
|
302
|
+
]]));
|
|
303
|
+
test("andThenEvents drains what later events resolve", () => thenScenarioTodos(andThenEvents(whenSweep(givenEvents([O.event({
|
|
304
|
+
TAG: "OrderPlaced",
|
|
305
|
+
orderId: "o1"
|
|
306
|
+
}, undefined)]), undefined), [O.event({
|
|
307
|
+
TAG: "OrderCancelled",
|
|
308
|
+
orderId: "o1"
|
|
309
|
+
}, undefined)], undefined), []));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
let Route = include.Route;
|
|
313
|
+
|
|
314
|
+
let testContext = include.testContext;
|
|
315
|
+
|
|
316
|
+
let encTodos = include.encTodos;
|
|
317
|
+
|
|
318
|
+
let encPair = include.encPair;
|
|
319
|
+
|
|
320
|
+
let thenNoCommand = include.thenNoCommand;
|
|
321
|
+
|
|
322
|
+
let routeAll = include.routeAll;
|
|
323
|
+
|
|
324
|
+
let sweep = include.sweep;
|
|
325
|
+
|
|
326
|
+
export {
|
|
327
|
+
ShipPaid,
|
|
328
|
+
ShipPaid_Automation,
|
|
329
|
+
Route,
|
|
330
|
+
describe,
|
|
331
|
+
test,
|
|
332
|
+
testContext,
|
|
333
|
+
encTodos,
|
|
334
|
+
encPair,
|
|
335
|
+
thenTodos,
|
|
336
|
+
thenResolved,
|
|
337
|
+
Mapping,
|
|
338
|
+
givenTodo,
|
|
339
|
+
whenProcess,
|
|
340
|
+
whenExhausted,
|
|
341
|
+
thenCommand,
|
|
342
|
+
thenNoCommand,
|
|
343
|
+
givenEvents,
|
|
344
|
+
routeAll,
|
|
345
|
+
sweep,
|
|
346
|
+
whenSweep,
|
|
347
|
+
thenCommands,
|
|
348
|
+
andThenEvents,
|
|
349
|
+
thenScenarioTodos,
|
|
350
|
+
O,
|
|
351
|
+
P,
|
|
352
|
+
}
|
|
353
|
+
/* todoItemSchema Not a pure module */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// InboundTranslation_GWT.FromSlice: input decoded as it arrives, translated by
|
|
2
|
+
// the slice's own `translate`, and commands checked against their schema.
|
|
3
|
+
|
|
4
|
+
module PaymentHook = {
|
|
5
|
+
let name = "PaymentHook"
|
|
6
|
+
let moduleUrl = ""
|
|
7
|
+
|
|
8
|
+
@schema
|
|
9
|
+
type externalInput = {orderId: string, status: string}
|
|
10
|
+
|
|
11
|
+
@schema
|
|
12
|
+
type command = ConfirmPayment({orderId: string})
|
|
13
|
+
|
|
14
|
+
let targetName = "Order"
|
|
15
|
+
let externalSystem = Some("Payments")
|
|
16
|
+
type role = Reventless.Role.name
|
|
17
|
+
let commandAuthorization = _ => Reventless.Authorization.AllowAuthenticated
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module PaymentHook_Translation = {
|
|
21
|
+
let moduleUrl = ""
|
|
22
|
+
let translate = (input: PaymentHook.externalInput) =>
|
|
23
|
+
switch input.status {
|
|
24
|
+
| "paid" => Ok([(input.orderId, PaymentHook.ConfirmPayment({orderId: input.orderId}))])
|
|
25
|
+
| "pending" => Ok([])
|
|
26
|
+
| other => Error(`Unknown status: ${other}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
open PaymentHook
|
|
31
|
+
include InboundTranslation_GWT.FromSlice(PaymentHook, PaymentHook_Translation)
|
|
32
|
+
|
|
33
|
+
let json = (pairs: array<(string, string)>) =>
|
|
34
|
+
pairs->Array.map(((k, v)) => (k, JSON.Encode.string(v)))->Dict.fromArray->JSON.Encode.object
|
|
35
|
+
|
|
36
|
+
describe("InboundTranslation_GWT.FromSlice", () => {
|
|
37
|
+
test("input translates to a command", () =>
|
|
38
|
+
whenInput({orderId: "o1", status: "paid"})->thenCommand("o1", ConfirmPayment({orderId: "o1"}))
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
test("received JSON decodes before it translates", () =>
|
|
42
|
+
whenReceived(json([("orderId", "o1"), ("status", "paid")]))->thenCommand(
|
|
43
|
+
"o1",
|
|
44
|
+
ConfirmPayment({orderId: "o1"}),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
test("input that does not decode is refused before translate", () =>
|
|
49
|
+
whenReceived(json([("status", "paid")]))->thenRefusedInput("orderId")
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
test("input translate declines is not understood", () =>
|
|
53
|
+
whenInput({orderId: "o1", status: "lost"})->thenNotUnderstood("Unknown status: lost")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
test("thenTranslateError is the older name", () =>
|
|
57
|
+
whenInput({orderId: "o1", status: "lost"})->thenTranslateError("Unknown status: lost")
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
test("no commands is an answer", () =>
|
|
61
|
+
whenInput({orderId: "o1", status: "pending"})->thenNoCommand
|
|
62
|
+
)
|
|
63
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as InboundTranslation_GWT$ReventlessGwt from "../src/InboundTranslation_GWT.res.mjs";
|
|
5
|
+
|
|
6
|
+
let externalInputSchema = Sury.$schema(s => ({
|
|
7
|
+
orderId: s.m(Sury.string),
|
|
8
|
+
status: s.m(Sury.string)
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
let commandSchema = Sury.$schema(s => ({
|
|
12
|
+
TAG: "ConfirmPayment",
|
|
13
|
+
orderId: s.m(Sury.string)
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
function commandAuthorization(param) {
|
|
17
|
+
return "AllowAuthenticated";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let traits = [];
|
|
21
|
+
|
|
22
|
+
let PaymentHook_externalSystem = "Payments";
|
|
23
|
+
|
|
24
|
+
let PaymentHook = {
|
|
25
|
+
name: "PaymentHook",
|
|
26
|
+
moduleUrl: "",
|
|
27
|
+
externalInputSchema: externalInputSchema,
|
|
28
|
+
commandSchema: commandSchema,
|
|
29
|
+
targetName: "Order",
|
|
30
|
+
externalSystem: PaymentHook_externalSystem,
|
|
31
|
+
commandAuthorization: commandAuthorization,
|
|
32
|
+
traits: traits
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let moduleUrl = "";
|
|
36
|
+
|
|
37
|
+
function translate(input) {
|
|
38
|
+
let other = input.status;
|
|
39
|
+
switch (other) {
|
|
40
|
+
case "paid" :
|
|
41
|
+
return {
|
|
42
|
+
TAG: "Ok",
|
|
43
|
+
_0: [[
|
|
44
|
+
input.orderId,
|
|
45
|
+
{
|
|
46
|
+
TAG: "ConfirmPayment",
|
|
47
|
+
orderId: input.orderId
|
|
48
|
+
}
|
|
49
|
+
]]
|
|
50
|
+
};
|
|
51
|
+
case "pending" :
|
|
52
|
+
return {
|
|
53
|
+
TAG: "Ok",
|
|
54
|
+
_0: []
|
|
55
|
+
};
|
|
56
|
+
default:
|
|
57
|
+
return {
|
|
58
|
+
TAG: "Error",
|
|
59
|
+
_0: `Unknown status: ` + other
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let PaymentHook_Translation = {
|
|
65
|
+
moduleUrl: moduleUrl,
|
|
66
|
+
translate: translate
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let include = InboundTranslation_GWT$ReventlessGwt.FromSlice(PaymentHook)({
|
|
70
|
+
translate: translate,
|
|
71
|
+
moduleUrl: moduleUrl
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let describe = include.describe;
|
|
75
|
+
|
|
76
|
+
let test = include.test;
|
|
77
|
+
|
|
78
|
+
let whenInput = include.whenInput;
|
|
79
|
+
|
|
80
|
+
let whenReceived = include.whenReceived;
|
|
81
|
+
|
|
82
|
+
let thenCommand = include.thenCommand;
|
|
83
|
+
|
|
84
|
+
let thenNoCommand = include.thenNoCommand;
|
|
85
|
+
|
|
86
|
+
let thenNotUnderstood = include.thenNotUnderstood;
|
|
87
|
+
|
|
88
|
+
let thenTranslateError = include.thenTranslateError;
|
|
89
|
+
|
|
90
|
+
let thenRefusedInput = include.thenRefusedInput;
|
|
91
|
+
|
|
92
|
+
function json(pairs) {
|
|
93
|
+
return Object.fromEntries(pairs.map(param => [
|
|
94
|
+
param[0],
|
|
95
|
+
param[1]
|
|
96
|
+
]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("InboundTranslation_GWT.FromSlice", () => {
|
|
100
|
+
test("input translates to a command", () => thenCommand(whenInput({
|
|
101
|
+
orderId: "o1",
|
|
102
|
+
status: "paid"
|
|
103
|
+
}), "o1", {
|
|
104
|
+
TAG: "ConfirmPayment",
|
|
105
|
+
orderId: "o1"
|
|
106
|
+
}));
|
|
107
|
+
test("received JSON decodes before it translates", () => thenCommand(whenReceived(json([
|
|
108
|
+
[
|
|
109
|
+
"orderId",
|
|
110
|
+
"o1"
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
"status",
|
|
114
|
+
"paid"
|
|
115
|
+
]
|
|
116
|
+
])), "o1", {
|
|
117
|
+
TAG: "ConfirmPayment",
|
|
118
|
+
orderId: "o1"
|
|
119
|
+
}));
|
|
120
|
+
test("input that does not decode is refused before translate", () => thenRefusedInput(whenReceived(json([[
|
|
121
|
+
"status",
|
|
122
|
+
"paid"
|
|
123
|
+
]])), "orderId"));
|
|
124
|
+
test("input translate declines is not understood", () => thenNotUnderstood(whenInput({
|
|
125
|
+
orderId: "o1",
|
|
126
|
+
status: "lost"
|
|
127
|
+
}), "Unknown status: lost"));
|
|
128
|
+
test("thenTranslateError is the older name", () => thenTranslateError(whenInput({
|
|
129
|
+
orderId: "o1",
|
|
130
|
+
status: "lost"
|
|
131
|
+
}), "Unknown status: lost"));
|
|
132
|
+
test("no commands is an answer", () => thenNoCommand(whenInput({
|
|
133
|
+
orderId: "o1",
|
|
134
|
+
status: "pending"
|
|
135
|
+
})));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let encPairs = include.encPairs;
|
|
139
|
+
|
|
140
|
+
let describeOther = include.describeOther;
|
|
141
|
+
|
|
142
|
+
let roundTrips = include.roundTrips;
|
|
143
|
+
|
|
144
|
+
let thenCommands = include.thenCommands;
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
PaymentHook,
|
|
148
|
+
PaymentHook_Translation,
|
|
149
|
+
describe,
|
|
150
|
+
test,
|
|
151
|
+
encPairs,
|
|
152
|
+
whenInput,
|
|
153
|
+
whenReceived,
|
|
154
|
+
describeOther,
|
|
155
|
+
roundTrips,
|
|
156
|
+
thenCommands,
|
|
157
|
+
thenCommand,
|
|
158
|
+
thenNoCommand,
|
|
159
|
+
thenNotUnderstood,
|
|
160
|
+
thenTranslateError,
|
|
161
|
+
thenRefusedInput,
|
|
162
|
+
json,
|
|
163
|
+
}
|
|
164
|
+
/* externalInputSchema Not a pure module */
|
|
@@ -41,10 +41,6 @@ function commandAuthorization(param) {
|
|
|
41
41
|
return "AllowAuthenticated";
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function commandTransition(param) {
|
|
45
|
-
return "Undeclared";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
44
|
let traits = [];
|
|
49
45
|
|
|
50
46
|
let PaymentWebhookSlice = {
|
|
@@ -53,7 +49,6 @@ let PaymentWebhookSlice = {
|
|
|
53
49
|
commandSchema: commandSchema,
|
|
54
50
|
translate: translate,
|
|
55
51
|
commandAuthorization: commandAuthorization,
|
|
56
|
-
commandTransition: commandTransition,
|
|
57
52
|
traits: traits,
|
|
58
53
|
externalSystem: undefined
|
|
59
54
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// OutboundTranslation_GWT.FromSlice: the slice's real `translate`, run against
|
|
2
|
+
// recording capability fakes, and statuses counted as the runtime counts them.
|
|
3
|
+
|
|
4
|
+
module Welcome = {
|
|
5
|
+
let name = "Welcome"
|
|
6
|
+
let moduleUrl = ""
|
|
7
|
+
|
|
8
|
+
@schema
|
|
9
|
+
type consumedEvent = Registered({customerId: string, email: string})
|
|
10
|
+
|
|
11
|
+
@schema
|
|
12
|
+
type outboundItem = {email: string}
|
|
13
|
+
|
|
14
|
+
@schema
|
|
15
|
+
type inboundCommand = MarkWelcomed({customerId: string}) | MarkUnreachable({customerId: string})
|
|
16
|
+
|
|
17
|
+
let maxRetries = 2
|
|
18
|
+
let heartbeatInterval = 60
|
|
19
|
+
let targetName = Some("Customer")
|
|
20
|
+
let sourceNames = []
|
|
21
|
+
let externalSystem = None
|
|
22
|
+
let capabilityNeeds = []
|
|
23
|
+
let traits = []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module Welcome_Translation = {
|
|
27
|
+
let moduleUrl = ""
|
|
28
|
+
|
|
29
|
+
let collect = (event, ~sourceId) =>
|
|
30
|
+
switch event {
|
|
31
|
+
| Welcome.Registered({email, _}) => [(sourceId, ({email: email}: Welcome.outboundItem))]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let translate = async (
|
|
35
|
+
id,
|
|
36
|
+
item: Welcome.outboundItem,
|
|
37
|
+
~capabilities: Reventless.Capabilities.t,
|
|
38
|
+
) =>
|
|
39
|
+
switch await capabilities.messaging.send(
|
|
40
|
+
~recipient=ToEmail(item.email),
|
|
41
|
+
~message={subject: "Welcome", body: "Hello"},
|
|
42
|
+
) {
|
|
43
|
+
| Ok(_) => Ok(Some((id, Welcome.MarkWelcomed({customerId: id}))))
|
|
44
|
+
| Error(failure) => Error(failure->Reventless.Messaging.failureReason)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let onExhausted = (id, _item, ~lastError as _) => Some((
|
|
48
|
+
id,
|
|
49
|
+
Welcome.MarkUnreachable({customerId: id}),
|
|
50
|
+
))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
open Welcome
|
|
54
|
+
include OutboundTranslation_GWT.FromSlice(Welcome, Welcome_Translation)
|
|
55
|
+
|
|
56
|
+
let down = Capabilities_Fake.make(
|
|
57
|
+
~send=async (~recipient as _, ~message as _) => Error(Unavailable("smtp down")),
|
|
58
|
+
(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
let welcome: Capabilities_Fake.call = Sent({
|
|
62
|
+
recipient: ToEmail("a@b.c"),
|
|
63
|
+
message: {subject: "Welcome", body: "Hello"},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("OutboundTranslation_GWT.FromSlice", () => {
|
|
67
|
+
testSync("collect keys by the source id", () =>
|
|
68
|
+
givenEvent(Registered({customerId: "c1", email: "a@b.c"}))
|
|
69
|
+
->whenCollect(~sourceId="c1")
|
|
70
|
+
->thenTodos([("c1", {email: "a@b.c"})])
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
test("translate sends through the messaging capability", () =>
|
|
74
|
+
givenTodo("c1", {email: "a@b.c"})->whenTranslated->thenSent([welcome])
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
test("translate answers with a command", () =>
|
|
78
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
79
|
+
->whenTranslated
|
|
80
|
+
->thenCommand("c1", MarkWelcomed({customerId: "c1"}))
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
test("a failed attempt leaves the row Failed", () =>
|
|
84
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
85
|
+
->givenCapabilities(down)
|
|
86
|
+
->whenTranslated
|
|
87
|
+
->thenTodoStatus("c1", #Failed)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
test("#Pending still reads as #Failed", () =>
|
|
91
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
92
|
+
->givenCapabilities(down)
|
|
93
|
+
->whenTranslated
|
|
94
|
+
->thenTodoStatus("c1", #Pending)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
test("maxRetries failed attempts abandon the row", () =>
|
|
98
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
99
|
+
->whenTranslateRetrying(async (_, _) => Error("down"))
|
|
100
|
+
->thenTodoStatus("c1", #Abandoned)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
test("maxRetries attempts in all, as the runtime makes them", () =>
|
|
104
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
105
|
+
->whenTranslateRetrying(async (_, _) => Error("down"))
|
|
106
|
+
->thenRetryRecorded(2)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
test("a throw is a failed attempt", () =>
|
|
110
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
111
|
+
->whenTranslateMocked(async (_, _) => JsError.throwWithMessage("boom"))
|
|
112
|
+
->thenTodoStatus("c1", #Failed)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
test("exhausted: onExhausted is what the domain hears", () =>
|
|
116
|
+
givenTodo("c1", {email: "a@b.c"})
|
|
117
|
+
->whenExhausted(~lastError="smtp down")
|
|
118
|
+
->thenCommand("c1", MarkUnreachable({customerId: "c1"}))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
test("nothing sent when the slice is never translated", () =>
|
|
122
|
+
givenTodo("c1", {email: "a@b.c"})->whenExhausted->thenNothingSent
|
|
123
|
+
)
|
|
124
|
+
})
|