@reventlessdev/reventless-gwt 1.0.0-alpha.237 → 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 +18 -0
- package/package.json +5 -5
- 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
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
3
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
4
9
|
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
5
10
|
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
6
11
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
12
|
+
import * as Capabilities_Fake$ReventlessGwt from "./Capabilities_Fake.res.mjs";
|
|
7
13
|
|
|
8
14
|
function Make(Spec) {
|
|
9
15
|
let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
|
|
@@ -181,7 +187,257 @@ function Make(Spec) {
|
|
|
181
187
|
};
|
|
182
188
|
}
|
|
183
189
|
|
|
190
|
+
function FromSlice(Spec) {
|
|
191
|
+
return Translation => {
|
|
192
|
+
let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
|
|
193
|
+
let testSync = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
194
|
+
let encItems = arr => arr.map(param => [
|
|
195
|
+
param[0],
|
|
196
|
+
Message$ReventlessCore.encode(param[1], Spec.outboundItemSchema)
|
|
197
|
+
]);
|
|
198
|
+
let commandPairJson = (id, cmd) => {
|
|
199
|
+
let d = {};
|
|
200
|
+
d["id"] = id;
|
|
201
|
+
d["command"] = Message$ReventlessCore.encode(cmd, Spec.inboundCommandSchema);
|
|
202
|
+
return d;
|
|
203
|
+
};
|
|
204
|
+
let errorOf = exn => {
|
|
205
|
+
if (exn.RE_EXN_ID === S.Exn) {
|
|
206
|
+
return exn._1.message;
|
|
207
|
+
} else if (exn.RE_EXN_ID === "JsExn") {
|
|
208
|
+
return Stdlib_Option.getOr(Stdlib_JsExn.message(exn._1), "unknown error");
|
|
209
|
+
} else {
|
|
210
|
+
return "unknown error";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
let givenEvent = e => e;
|
|
214
|
+
let whenCollect = (e, sourceIdOpt) => {
|
|
215
|
+
let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
|
|
216
|
+
return Translation.collect(e, sourceId);
|
|
217
|
+
};
|
|
218
|
+
let thenTodos = (actual, expected) => {
|
|
219
|
+
if (Primitive_object.equal(actual, expected)) {
|
|
220
|
+
return Outcome$ReventlessGwt.pass;
|
|
221
|
+
} else {
|
|
222
|
+
return Outcome$ReventlessGwt.fail({
|
|
223
|
+
TAG: "TodoMismatch",
|
|
224
|
+
expected: encItems(expected),
|
|
225
|
+
actual: encItems(actual)
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
let givenTodo = (id, item) => ({
|
|
230
|
+
id: id,
|
|
231
|
+
item: item,
|
|
232
|
+
fakes: Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
233
|
+
});
|
|
234
|
+
let givenCapabilities = (todo, fakes) => ({
|
|
235
|
+
id: todo.id,
|
|
236
|
+
item: todo.item,
|
|
237
|
+
fakes: fakes
|
|
238
|
+
});
|
|
239
|
+
let attemptWith = async (todo, run) => {
|
|
240
|
+
let result;
|
|
241
|
+
try {
|
|
242
|
+
result = await run(todo.id, todo.item);
|
|
243
|
+
} catch (raw_exn) {
|
|
244
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
245
|
+
result = {
|
|
246
|
+
TAG: "Error",
|
|
247
|
+
_0: errorOf(exn)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (result.TAG !== "Ok") {
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
let match = result._0;
|
|
254
|
+
if (match === undefined) {
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
Message$ReventlessCore.encode(match[1], Spec.inboundCommandSchema);
|
|
259
|
+
return result;
|
|
260
|
+
} catch (raw_exn$1) {
|
|
261
|
+
let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
|
|
262
|
+
return {
|
|
263
|
+
TAG: "Error",
|
|
264
|
+
_0: `failed to encode inbound command: ` + errorOf(exn$1)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
let attempts = async (todo, limit, maxRetries, run) => {
|
|
269
|
+
let result = await attemptWith(todo, run);
|
|
270
|
+
let failures = Stdlib_Result.isError(result) ? 1 : 0;
|
|
271
|
+
while (Stdlib_Result.isError(result) && failures < limit) {
|
|
272
|
+
result = await attemptWith(todo, run);
|
|
273
|
+
if (Stdlib_Result.isError(result)) {
|
|
274
|
+
failures = failures + 1 | 0;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
let match = result;
|
|
278
|
+
let tmp;
|
|
279
|
+
tmp = match.TAG === "Ok" ? "Completed" : (
|
|
280
|
+
failures >= maxRetries ? "Abandoned" : "Failed"
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
id: todo.id,
|
|
284
|
+
item: todo.item,
|
|
285
|
+
result: result,
|
|
286
|
+
retries: failures,
|
|
287
|
+
status: tmp,
|
|
288
|
+
sent: todo.fakes.calls()
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
let whenTranslated = todo => attempts(todo, 1, Spec.maxRetries, (id, item) => Translation.translate(id, item, todo.fakes.capabilities));
|
|
292
|
+
let whenTranslateMocked = (todo, mock) => attempts(todo, 1, Spec.maxRetries, mock);
|
|
293
|
+
let whenTranslateRetrying = (todo, maxRetriesOpt, mock) => {
|
|
294
|
+
let maxRetries = maxRetriesOpt !== undefined ? maxRetriesOpt : Spec.maxRetries;
|
|
295
|
+
return attempts(todo, maxRetries, maxRetries, mock);
|
|
296
|
+
};
|
|
297
|
+
let whenExhausted = async (todo, lastError) => ({
|
|
298
|
+
id: todo.id,
|
|
299
|
+
item: todo.item,
|
|
300
|
+
result: {
|
|
301
|
+
TAG: "Ok",
|
|
302
|
+
_0: Translation.onExhausted(todo.id, todo.item, lastError)
|
|
303
|
+
},
|
|
304
|
+
retries: Spec.maxRetries,
|
|
305
|
+
status: "Abandoned",
|
|
306
|
+
sent: []
|
|
307
|
+
});
|
|
308
|
+
let thenCommand = async (pending, expectedId, expectedCmd) => {
|
|
309
|
+
let match = await pending;
|
|
310
|
+
let result = match.result;
|
|
311
|
+
if (result.TAG !== "Ok") {
|
|
312
|
+
return Outcome$ReventlessGwt.fail({
|
|
313
|
+
TAG: "TranslateError",
|
|
314
|
+
expected: "(command)",
|
|
315
|
+
actual: result._0
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
let actual = result._0;
|
|
319
|
+
if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
|
|
320
|
+
return Outcome$ReventlessGwt.pass;
|
|
321
|
+
}
|
|
322
|
+
return Outcome$ReventlessGwt.fail({
|
|
323
|
+
TAG: "EventsMismatch",
|
|
324
|
+
expected: [commandPairJson(expectedId, expectedCmd)],
|
|
325
|
+
actual: Stdlib_Option.mapOr(actual, [], param => [commandPairJson(param[0], param[1])])
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
let thenNoCommand = async pending => {
|
|
329
|
+
let match = await pending;
|
|
330
|
+
let result = match.result;
|
|
331
|
+
if (result.TAG !== "Ok") {
|
|
332
|
+
return Outcome$ReventlessGwt.fail({
|
|
333
|
+
TAG: "TranslateError",
|
|
334
|
+
expected: "(no command)",
|
|
335
|
+
actual: result._0
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
let match$1 = result._0;
|
|
339
|
+
if (match$1 !== undefined) {
|
|
340
|
+
return Outcome$ReventlessGwt.fail({
|
|
341
|
+
TAG: "NoEventExpected",
|
|
342
|
+
actual: [commandPairJson(match$1[0], match$1[1])]
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
return Outcome$ReventlessGwt.pass;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
let thenSent = async (pending, expected) => {
|
|
349
|
+
let match = await pending;
|
|
350
|
+
let sent = match.sent;
|
|
351
|
+
if (Primitive_object.equal(sent, expected)) {
|
|
352
|
+
return Outcome$ReventlessGwt.pass;
|
|
353
|
+
} else {
|
|
354
|
+
return Outcome$ReventlessGwt.fail({
|
|
355
|
+
TAG: "EventsMismatch",
|
|
356
|
+
expected: expected.map(Capabilities_Fake$ReventlessGwt.toJson),
|
|
357
|
+
actual: sent.map(Capabilities_Fake$ReventlessGwt.toJson)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
let thenNothingSent = pending => thenSent(pending, []);
|
|
362
|
+
let thenRetryRecorded = async (pending, expectedRetries) => {
|
|
363
|
+
let match = await pending;
|
|
364
|
+
let retries = match.retries;
|
|
365
|
+
if (retries === expectedRetries) {
|
|
366
|
+
return Outcome$ReventlessGwt.pass;
|
|
367
|
+
} else {
|
|
368
|
+
return Outcome$ReventlessGwt.fail({
|
|
369
|
+
TAG: "StateMismatch",
|
|
370
|
+
key: "retries",
|
|
371
|
+
expected: expectedRetries,
|
|
372
|
+
actual: retries
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
let statusName = s => {
|
|
377
|
+
if (s === "Failed" || s === "Pending") {
|
|
378
|
+
return "#Failed";
|
|
379
|
+
} else if (s === "Abandoned") {
|
|
380
|
+
return "#Abandoned";
|
|
381
|
+
} else {
|
|
382
|
+
return "#Completed";
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
let thenTodoStatus = async (pending, expectedId, expectedStatus) => {
|
|
386
|
+
let match = await pending;
|
|
387
|
+
let status = match.status;
|
|
388
|
+
let id = match.id;
|
|
389
|
+
if (id === expectedId && statusName(status) === statusName(expectedStatus)) {
|
|
390
|
+
return Outcome$ReventlessGwt.pass;
|
|
391
|
+
}
|
|
392
|
+
let encOne = (rid, s) => Object.fromEntries([
|
|
393
|
+
[
|
|
394
|
+
"id",
|
|
395
|
+
rid
|
|
396
|
+
],
|
|
397
|
+
[
|
|
398
|
+
"status",
|
|
399
|
+
statusName(s)
|
|
400
|
+
]
|
|
401
|
+
]);
|
|
402
|
+
return Outcome$ReventlessGwt.fail({
|
|
403
|
+
TAG: "StateMismatch",
|
|
404
|
+
key: id,
|
|
405
|
+
expected: encOne(expectedId, expectedStatus),
|
|
406
|
+
actual: encOne(id, status)
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
describe: JestBind$ReventlessGwt.describe,
|
|
411
|
+
test: test,
|
|
412
|
+
testSync: testSync,
|
|
413
|
+
encItems: encItems,
|
|
414
|
+
commandPairJson: commandPairJson,
|
|
415
|
+
errorOf: errorOf,
|
|
416
|
+
givenEvent: givenEvent,
|
|
417
|
+
whenCollect: whenCollect,
|
|
418
|
+
thenTodos: thenTodos,
|
|
419
|
+
givenTodo: givenTodo,
|
|
420
|
+
fakes: Capabilities_Fake$ReventlessGwt.make,
|
|
421
|
+
givenCapabilities: givenCapabilities,
|
|
422
|
+
attemptWith: attemptWith,
|
|
423
|
+
attempts: attempts,
|
|
424
|
+
whenTranslated: whenTranslated,
|
|
425
|
+
whenTranslateMocked: whenTranslateMocked,
|
|
426
|
+
whenTranslateRetrying: whenTranslateRetrying,
|
|
427
|
+
whenExhausted: whenExhausted,
|
|
428
|
+
thenCommand: thenCommand,
|
|
429
|
+
thenNoCommand: thenNoCommand,
|
|
430
|
+
thenSent: thenSent,
|
|
431
|
+
thenNothingSent: thenNothingSent,
|
|
432
|
+
thenRetryRecorded: thenRetryRecorded,
|
|
433
|
+
statusName: statusName,
|
|
434
|
+
thenTodoStatus: thenTodoStatus
|
|
435
|
+
};
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
184
439
|
export {
|
|
185
440
|
Make,
|
|
441
|
+
FromSlice,
|
|
186
442
|
}
|
|
187
|
-
/*
|
|
443
|
+
/* S Not a pure module */
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Automation_GWT.FromSlice over a two-source slice written as the framework
|
|
2
|
+
// takes it: a Spec, and a body with per-source mappings, `process` and
|
|
3
|
+
// `onExhausted`. The second source's events do not name the order, so its
|
|
4
|
+
// mapping keys the to-do by `~sourceId`.
|
|
5
|
+
|
|
6
|
+
module ShipPaid = {
|
|
7
|
+
let name = "ShipPaid"
|
|
8
|
+
let moduleUrl = ""
|
|
9
|
+
|
|
10
|
+
@schema
|
|
11
|
+
type todoItem = {orderId: string}
|
|
12
|
+
|
|
13
|
+
@schema
|
|
14
|
+
type command =
|
|
15
|
+
| Ship({orderId: string})
|
|
16
|
+
| GiveUp({orderId: string})
|
|
17
|
+
|
|
18
|
+
let maxRetries = 3
|
|
19
|
+
let heartbeatInterval = 60
|
|
20
|
+
let targetName = "Order"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module ShipPaid_Automation = {
|
|
24
|
+
module M = Reventless.AutomationSlice.Mappings.Make(ShipPaid)
|
|
25
|
+
module type Mapping = M.Mapping
|
|
26
|
+
let moduleUrl = ""
|
|
27
|
+
|
|
28
|
+
module Orders = {
|
|
29
|
+
module Id = Reventless.Id.String
|
|
30
|
+
let name = "Orders"
|
|
31
|
+
@schema
|
|
32
|
+
type event =
|
|
33
|
+
| OrderPlaced({orderId: string})
|
|
34
|
+
| OrderCancelled({orderId: string})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module Payments = {
|
|
38
|
+
module Id = Reventless.Id.String
|
|
39
|
+
let name = "Payments"
|
|
40
|
+
@schema
|
|
41
|
+
type event =
|
|
42
|
+
| Captured({amount: int})
|
|
43
|
+
| Refunded({amount: int})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module FromOrders = Reventless.AutomationSlice.Mapping.Make(
|
|
47
|
+
Orders,
|
|
48
|
+
ShipPaid,
|
|
49
|
+
{
|
|
50
|
+
let collect = (event, ~sourceId as _, _ctx) =>
|
|
51
|
+
switch event {
|
|
52
|
+
| Orders.OrderPlaced({orderId}) => [(orderId, ({orderId: orderId}: ShipPaid.todoItem))]
|
|
53
|
+
| OrderCancelled(_) => []
|
|
54
|
+
}
|
|
55
|
+
let resolve = event =>
|
|
56
|
+
switch event {
|
|
57
|
+
| Orders.OrderCancelled({orderId}) => Some(orderId)
|
|
58
|
+
| OrderPlaced(_) => None
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
module FromPayments = Reventless.AutomationSlice.Mapping.Make(
|
|
64
|
+
Payments,
|
|
65
|
+
ShipPaid,
|
|
66
|
+
{
|
|
67
|
+
let collect = (event, ~sourceId, _ctx) =>
|
|
68
|
+
switch event {
|
|
69
|
+
| Payments.Captured(_) => [(sourceId, ({orderId: sourceId}: ShipPaid.todoItem))]
|
|
70
|
+
| Refunded(_) => []
|
|
71
|
+
}
|
|
72
|
+
let resolve = event =>
|
|
73
|
+
switch event {
|
|
74
|
+
| Payments.Refunded(_) => None
|
|
75
|
+
| Captured(_) => None
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
let mappings: array<module(Mapping)> = [module(FromOrders), module(FromPayments)]
|
|
81
|
+
|
|
82
|
+
let process = (id, _item: ShipPaid.todoItem) => Some((id, ShipPaid.Ship({orderId: id})))
|
|
83
|
+
let onExhausted = (id, _item: ShipPaid.todoItem) => Some((id, ShipPaid.GiveUp({orderId: id})))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
open ShipPaid
|
|
87
|
+
open ShipPaid_Automation
|
|
88
|
+
include Automation_GWT.FromSlice(ShipPaid, ShipPaid_Automation)
|
|
89
|
+
module O = Mapping(FromOrders)
|
|
90
|
+
module P = Mapping(FromPayments)
|
|
91
|
+
|
|
92
|
+
describe("Automation_GWT.FromSlice", () => {
|
|
93
|
+
test("collect through one mapping", () =>
|
|
94
|
+
O.givenEvent(OrderPlaced({orderId: "o1"}))
|
|
95
|
+
->O.whenCollect
|
|
96
|
+
->thenTodos([("o1", {orderId: "o1"})])
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
test("collect keys by the source id where the event does not name it", () =>
|
|
100
|
+
P.givenEvent(Captured({amount: 5}))
|
|
101
|
+
->P.whenCollect(~sourceId="o2")
|
|
102
|
+
->thenTodos([("o2", {orderId: "o2"})])
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
test("resolve through one mapping", () =>
|
|
106
|
+
O.givenEvent(OrderCancelled({orderId: "o1"}))
|
|
107
|
+
->O.whenResolve
|
|
108
|
+
->thenResolved(Some("o1"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
test("process", () =>
|
|
112
|
+
givenTodo("o1", {orderId: "o1"})->whenProcess->thenCommand("o1", Ship({orderId: "o1"}))
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
test("exhausted: onExhausted is what the domain hears", () =>
|
|
116
|
+
givenTodo("o1", {orderId: "o1"})->whenExhausted->thenCommand("o1", GiveUp({orderId: "o1"}))
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
test("sweep: both sources route by name, in event order", () =>
|
|
120
|
+
givenEvents([
|
|
121
|
+
P.event(Captured({amount: 5}), ~sourceId="o2"),
|
|
122
|
+
O.event(OrderPlaced({orderId: "o1"})),
|
|
123
|
+
])
|
|
124
|
+
->whenSweep
|
|
125
|
+
->thenCommands([("o2", Ship({orderId: "o2"})), ("o1", Ship({orderId: "o1"}))])
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
test("sweep: a resolve completes only a row that exists", () =>
|
|
129
|
+
givenEvents([
|
|
130
|
+
O.event(OrderCancelled({orderId: "o1"})),
|
|
131
|
+
O.event(OrderPlaced({orderId: "o1"})),
|
|
132
|
+
O.event(OrderPlaced({orderId: "o3"})),
|
|
133
|
+
O.event(OrderCancelled({orderId: "o3"})),
|
|
134
|
+
])
|
|
135
|
+
->whenSweep
|
|
136
|
+
->thenScenarioTodos([("o1", {orderId: "o1"})])
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
test("andThenEvents drains what later events resolve", () =>
|
|
140
|
+
givenEvents([O.event(OrderPlaced({orderId: "o1"}))])
|
|
141
|
+
->whenSweep
|
|
142
|
+
->andThenEvents([O.event(OrderCancelled({orderId: "o1"}))])
|
|
143
|
+
->thenScenarioTodos([])
|
|
144
|
+
)
|
|
145
|
+
})
|