@reventlessdev/reventless-local 3.0.0-alpha.233 → 3.0.0-alpha.235
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 +23 -0
- package/package.json +10 -10
- package/src/adapter/DomainGraphQL_Server.res +36 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +40 -1
- package/src/adapter/GraphQL_Server.res.mjs +6 -0
- package/src/adapter/PlatformGraphQL_Server.res +5 -0
- package/src/adapter/PlatformGraphQL_Server.res.mjs +4 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +171 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +118 -1
- package/tests/adapter/QueryDbListResolverTest.res +150 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +133 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res +253 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res.mjs +291 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +64 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +55 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +10 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +1 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +5 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +5 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +135 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +202 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +15 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +5 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
9
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
10
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
11
|
+
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
12
|
+
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
13
|
+
import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
|
|
14
|
+
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
15
|
+
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
16
|
+
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
17
|
+
import * as QueryDb_Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Adapter.res.mjs";
|
|
18
|
+
import * as QueryDb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Builder.res.mjs";
|
|
19
|
+
import * as LocalQueryDbStorage$ReventlessLocal from "../../src/adapter/QueryDb/LocalQueryDbStorage.res.mjs";
|
|
20
|
+
import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
|
|
21
|
+
import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
|
|
22
|
+
|
|
23
|
+
TestRunner$ReventlessLocal.setup();
|
|
24
|
+
|
|
25
|
+
let productRowSchema = Sury.$schema(s => ({
|
|
26
|
+
productId: s.m(Sury.string),
|
|
27
|
+
name: s.m(Sury.string),
|
|
28
|
+
archived: s.m(Sury.bool)
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
let orderRowSchema = Sury.$schema(s => ({
|
|
32
|
+
orderId: s.m(Sury.string),
|
|
33
|
+
productId: s.m(Sury.string),
|
|
34
|
+
productIds: s.m(Sury.array(Sury.string))
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
function annotations(retired) {
|
|
38
|
+
return {
|
|
39
|
+
ids: [],
|
|
40
|
+
compositeIds: [],
|
|
41
|
+
subIds: [],
|
|
42
|
+
compositeSubIds: [],
|
|
43
|
+
indexes: [],
|
|
44
|
+
hidden: [],
|
|
45
|
+
summary: [],
|
|
46
|
+
drillTargets: [],
|
|
47
|
+
drillTargetKeys: [],
|
|
48
|
+
collapsed: [],
|
|
49
|
+
scan: [],
|
|
50
|
+
scanSort: [],
|
|
51
|
+
semantic: [],
|
|
52
|
+
metric: [],
|
|
53
|
+
lifecycle: undefined,
|
|
54
|
+
groupBy: undefined,
|
|
55
|
+
visibility: undefined,
|
|
56
|
+
live: undefined,
|
|
57
|
+
retired: retired
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ctxFor(groups) {
|
|
62
|
+
let newrecord = {...Identity$Reventless.anonymous};
|
|
63
|
+
return Object.fromEntries([
|
|
64
|
+
[
|
|
65
|
+
"request",
|
|
66
|
+
Object.fromEntries([[
|
|
67
|
+
"headers",
|
|
68
|
+
{}
|
|
69
|
+
]])
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"identity",
|
|
73
|
+
(newrecord.groups = groups, newrecord.username = "test-user", newrecord.userId = "test-user", newrecord)
|
|
74
|
+
]
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let plainCtx = ctxFor(["User"]);
|
|
79
|
+
|
|
80
|
+
let noArgs = {};
|
|
81
|
+
|
|
82
|
+
function getString(json, key) {
|
|
83
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.string);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function names(json) {
|
|
87
|
+
return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(json), []), item => getString(item, "name"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function buildFixture(suffix, retired) {
|
|
91
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
92
|
+
let Storage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
|
|
93
|
+
let Resolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
|
|
94
|
+
let productsName = "Products" + suffix;
|
|
95
|
+
let ordersName = "Orders" + suffix;
|
|
96
|
+
let orderTypeName = "Shop_Order" + suffix;
|
|
97
|
+
let moduleUrl = import.meta.url;
|
|
98
|
+
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
99
|
+
let moduleUrl$1 = import.meta.url;
|
|
100
|
+
let config$1 = ReadModel$Reventless.config([{
|
|
101
|
+
source: {
|
|
102
|
+
idField: "productId",
|
|
103
|
+
subId: "NoSubId",
|
|
104
|
+
resolvedField: {
|
|
105
|
+
TAG: "Single",
|
|
106
|
+
_0: "product"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
target: {
|
|
110
|
+
tableName: productsName,
|
|
111
|
+
idField: "Id"
|
|
112
|
+
}
|
|
113
|
+
}], [{
|
|
114
|
+
source: {
|
|
115
|
+
idsField: "productIds",
|
|
116
|
+
resolvedField: "products"
|
|
117
|
+
},
|
|
118
|
+
target: {
|
|
119
|
+
tableName: productsName
|
|
120
|
+
}
|
|
121
|
+
}], undefined);
|
|
122
|
+
let NoResolvers = QueryDb_Adapter$ReventlessCore.NoResolvers({
|
|
123
|
+
make: Storage.make
|
|
124
|
+
});
|
|
125
|
+
let ProductsDb = QueryDb_Builder$ReventlessCore.Make({
|
|
126
|
+
Id: {
|
|
127
|
+
schema: Id$Reventless.StringPure.schema,
|
|
128
|
+
make: prim => prim,
|
|
129
|
+
makeFromString: prim => prim,
|
|
130
|
+
toString: prim => prim,
|
|
131
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
132
|
+
},
|
|
133
|
+
name: productsName,
|
|
134
|
+
moduleUrl: moduleUrl,
|
|
135
|
+
stateSchema: productRowSchema,
|
|
136
|
+
config: config,
|
|
137
|
+
subIdConfig: undefined,
|
|
138
|
+
authorization: "AllowAuthenticated",
|
|
139
|
+
visibility: "Public"
|
|
140
|
+
})({
|
|
141
|
+
make: Storage.make
|
|
142
|
+
})(NoResolvers);
|
|
143
|
+
let OrdersDb = QueryDb_Builder$ReventlessCore.Make({
|
|
144
|
+
Id: {
|
|
145
|
+
schema: Id$Reventless.StringPure.schema,
|
|
146
|
+
make: prim => prim,
|
|
147
|
+
makeFromString: prim => prim,
|
|
148
|
+
toString: prim => prim,
|
|
149
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
150
|
+
},
|
|
151
|
+
name: ordersName,
|
|
152
|
+
moduleUrl: moduleUrl$1,
|
|
153
|
+
stateSchema: orderRowSchema,
|
|
154
|
+
config: config$1,
|
|
155
|
+
subIdConfig: undefined,
|
|
156
|
+
authorization: "AllowAuthenticated",
|
|
157
|
+
visibility: "Public"
|
|
158
|
+
})({
|
|
159
|
+
make: Storage.make
|
|
160
|
+
})(NoResolvers);
|
|
161
|
+
let register = (viewName, returnTypeName) => {
|
|
162
|
+
Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[viewName] = {
|
|
163
|
+
singleFieldName: returnTypeName,
|
|
164
|
+
listFieldName: returnTypeName + "s",
|
|
165
|
+
returnTypeName: returnTypeName,
|
|
166
|
+
pluralTypeName: returnTypeName + "s",
|
|
167
|
+
includeIdParam: true,
|
|
168
|
+
connectionSpec: true
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
register(productsName, "Shop_Product" + suffix);
|
|
172
|
+
register(ordersName, orderTypeName);
|
|
173
|
+
Plugin_Helpers$ReventlessCore.stateSchemaRegistry[productsName] = Sury.$Metadata_set(productRowSchema, StateAnnotations$Reventless.stateAnnotationsId, annotations(retired));
|
|
174
|
+
let productsDb = ProductsDb.make(undefined, undefined, undefined, undefined, undefined);
|
|
175
|
+
let productOps = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(productsDb));
|
|
176
|
+
let ordersDb = OrdersDb.make(undefined, undefined, undefined, undefined, undefined);
|
|
177
|
+
let orderOps = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(ordersDb));
|
|
178
|
+
Resolvers.make(ordersName, undefined, undefined, Pulumi.output(""), [], undefined, config$1.idResolvers, config$1.idsResolvers, "AllowAuthenticated", {});
|
|
179
|
+
await productOps.save("p-1", {
|
|
180
|
+
productId: "p-1",
|
|
181
|
+
name: "Book",
|
|
182
|
+
archived: false
|
|
183
|
+
}, "Init", undefined);
|
|
184
|
+
await productOps.save("p-2", {
|
|
185
|
+
productId: "p-2",
|
|
186
|
+
name: "Pen",
|
|
187
|
+
archived: true
|
|
188
|
+
}, "Init", undefined);
|
|
189
|
+
await orderOps.save("o-1", {
|
|
190
|
+
orderId: "o-1",
|
|
191
|
+
productId: "p-1",
|
|
192
|
+
productIds: [
|
|
193
|
+
"p-1",
|
|
194
|
+
"gone",
|
|
195
|
+
"p-2"
|
|
196
|
+
]
|
|
197
|
+
}, "Init", undefined);
|
|
198
|
+
let resolverFor = field => {
|
|
199
|
+
let r = DomainGraphQL_Server$ReventlessLocal.getFieldResolver(orderTypeName, field);
|
|
200
|
+
if (r !== undefined) {
|
|
201
|
+
return r;
|
|
202
|
+
} else {
|
|
203
|
+
return Stdlib_JsError.throwWithMessage("field resolver not registered: " + field);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
let order = Object.fromEntries([
|
|
207
|
+
[
|
|
208
|
+
"orderId",
|
|
209
|
+
"o-1"
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
"productId",
|
|
213
|
+
"p-1"
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
"productIds",
|
|
217
|
+
[
|
|
218
|
+
"p-1",
|
|
219
|
+
"gone",
|
|
220
|
+
"p-2"
|
|
221
|
+
].map(prim => prim)
|
|
222
|
+
]
|
|
223
|
+
]);
|
|
224
|
+
return [
|
|
225
|
+
resolverFor,
|
|
226
|
+
order
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
globalThis.describe("QueryDb cross-table field resolvers", () => {
|
|
231
|
+
globalThis.beforeEach(() => DomainGraphQL_Server$ReventlessLocal.reset());
|
|
232
|
+
globalThis.test("@resolves follows the foreign key into the target's row", async () => {
|
|
233
|
+
let match = await buildFixture("A", undefined);
|
|
234
|
+
let product = await match[0]("product")(match[1], noArgs, plainCtx);
|
|
235
|
+
globalThis.expect([
|
|
236
|
+
getString(product, "name"),
|
|
237
|
+
getString(product, "id")
|
|
238
|
+
]).toEqual([
|
|
239
|
+
"Book",
|
|
240
|
+
"p-1"
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
globalThis.test("@resolvesMany batch-follows the id array", async () => {
|
|
244
|
+
let match = await buildFixture("B", undefined);
|
|
245
|
+
let products = await match[0]("products")(match[1], noArgs, plainCtx);
|
|
246
|
+
globalThis.expect(names(products)).toEqual([
|
|
247
|
+
"Book",
|
|
248
|
+
"Pen"
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
globalThis.test("a foreign key naming no row resolves to nothing", async () => {
|
|
252
|
+
let match = await buildFixture("C", undefined);
|
|
253
|
+
let orphan = Object.fromEntries([[
|
|
254
|
+
"productId",
|
|
255
|
+
"nope"
|
|
256
|
+
]]);
|
|
257
|
+
let product = await match[0]("product")(orphan, noArgs, plainCtx);
|
|
258
|
+
globalThis.expect(product).toBe(null);
|
|
259
|
+
});
|
|
260
|
+
globalThis.test("a retired target row is withheld from both forms", async () => {
|
|
261
|
+
let match = await buildFixture("D", {
|
|
262
|
+
field: "archived",
|
|
263
|
+
label: "",
|
|
264
|
+
showWhenFalse: false,
|
|
265
|
+
values: undefined,
|
|
266
|
+
namedWhenRetired: false
|
|
267
|
+
});
|
|
268
|
+
let resolverFor = match[0];
|
|
269
|
+
let products = await resolverFor("products")(match[1], noArgs, plainCtx);
|
|
270
|
+
globalThis.expect(names(products)).toEqual(["Book"]);
|
|
271
|
+
let archivedOnly = Object.fromEntries([[
|
|
272
|
+
"productId",
|
|
273
|
+
"p-2"
|
|
274
|
+
]]);
|
|
275
|
+
let product = await resolverFor("product")(archivedOnly, noArgs, plainCtx);
|
|
276
|
+
globalThis.expect(product).toBe(null);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
export {
|
|
281
|
+
productRowSchema,
|
|
282
|
+
orderRowSchema,
|
|
283
|
+
annotations,
|
|
284
|
+
ctxFor,
|
|
285
|
+
plainCtx,
|
|
286
|
+
noArgs,
|
|
287
|
+
getString,
|
|
288
|
+
names,
|
|
289
|
+
buildFixture,
|
|
290
|
+
}
|
|
291
|
+
/* Not a pure module */
|
|
@@ -179,6 +179,70 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
179
179
|
})
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
+
// A row used to leave the retry budget by falling out of phase2's filter, so
|
|
183
|
+
// `Failed` meant both "will be retried" and "never again" and only the count
|
|
184
|
+
// told them apart — against a ceiling that lives in the Spec, not in the row.
|
|
185
|
+
describe("retry exhaustion", () => {
|
|
186
|
+
let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
|
|
187
|
+
JsError.throwWithMessage("publish failed")
|
|
188
|
+
|
|
189
|
+
let collectOne = () =>
|
|
190
|
+
Callback.phase1(
|
|
191
|
+
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
192
|
+
testContext,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
let statusOf = id => (Callback.todoItems->Dict.get(id)->Option.getOrThrow).status
|
|
196
|
+
|
|
197
|
+
testPromise("the attempt that spends the budget writes Abandoned, not Failed", async () => {
|
|
198
|
+
collectOne()
|
|
199
|
+
// ShipOrderSpec.maxRetries is 3: attempts at retryCount 0, 1 and 2 leave the
|
|
200
|
+
// row retriable, and the third failure is the one that ends it.
|
|
201
|
+
await Callback.phase2(failingPublish)
|
|
202
|
+
expect(statusOf("ord-1"))->toBe(Failed)
|
|
203
|
+
await Callback.phase2(failingPublish)
|
|
204
|
+
expect(statusOf("ord-1"))->toBe(Failed)
|
|
205
|
+
await Callback.phase2(failingPublish)
|
|
206
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
207
|
+
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
testPromise("an abandoned row is not picked up again", async () => {
|
|
211
|
+
collectOne()
|
|
212
|
+
await Callback.phase2(failingPublish)
|
|
213
|
+
await Callback.phase2(failingPublish)
|
|
214
|
+
await Callback.phase2(failingPublish)
|
|
215
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
216
|
+
|
|
217
|
+
// A pass that would have succeeded: the row must still not be attempted,
|
|
218
|
+
// which is what makes the status a decision rather than a label.
|
|
219
|
+
let published = ref([])
|
|
220
|
+
await Callback.phase2(async cmds => published := cmds)
|
|
221
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
222
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
223
|
+
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
testPromise("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
227
|
+
collectOne()
|
|
228
|
+
// What an older build wrote, and what a Spec that lowered maxRetries leaves
|
|
229
|
+
// behind: Failed at the ceiling, which no pass would ever pick up or mark.
|
|
230
|
+
let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
231
|
+
Callback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 3})
|
|
232
|
+
|
|
233
|
+
let published = ref([])
|
|
234
|
+
await Callback.phase2(async cmds => published := cmds)
|
|
235
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
236
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
testPromise("the row carries the ceiling its count is racing", async () => {
|
|
240
|
+
collectOne()
|
|
241
|
+
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)
|
|
242
|
+
->toEqual(Some(ShipOrderSpec.maxRetries))
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
182
246
|
describe("full lifecycle", () => {
|
|
183
247
|
testPromise("collect → process → resolve completes the TODO item", async () => {
|
|
184
248
|
Callback.phase1(
|
|
@@ -192,6 +192,61 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
192
192
|
globalThis.expect(row2.status).toBe("Processing");
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
|
+
globalThis.describe("retry exhaustion", () => {
|
|
196
|
+
let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
|
|
197
|
+
let collectOne = () => Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
198
|
+
TAG: "OrderPlaced",
|
|
199
|
+
orderId: "ord-1",
|
|
200
|
+
address: "123 Main St"
|
|
201
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
202
|
+
let statusOf = id => Stdlib_Option.getOrThrow(Callback.todoItems[id], undefined).status;
|
|
203
|
+
globalThis.test("the attempt that spends the budget writes Abandoned, not Failed", async () => {
|
|
204
|
+
collectOne();
|
|
205
|
+
await Callback.phase2(failingPublish);
|
|
206
|
+
globalThis.expect(statusOf("ord-1")).toBe("Failed");
|
|
207
|
+
await Callback.phase2(failingPublish);
|
|
208
|
+
globalThis.expect(statusOf("ord-1")).toBe("Failed");
|
|
209
|
+
await Callback.phase2(failingPublish);
|
|
210
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
211
|
+
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
|
|
212
|
+
});
|
|
213
|
+
globalThis.test("an abandoned row is not picked up again", async () => {
|
|
214
|
+
collectOne();
|
|
215
|
+
await Callback.phase2(failingPublish);
|
|
216
|
+
await Callback.phase2(failingPublish);
|
|
217
|
+
await Callback.phase2(failingPublish);
|
|
218
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
219
|
+
let published = {
|
|
220
|
+
contents: []
|
|
221
|
+
};
|
|
222
|
+
await Callback.phase2(async cmds => {
|
|
223
|
+
published.contents = cmds;
|
|
224
|
+
});
|
|
225
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
226
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
227
|
+
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
|
|
228
|
+
});
|
|
229
|
+
globalThis.test("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
230
|
+
collectOne();
|
|
231
|
+
let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
|
|
232
|
+
let newrecord = {...row};
|
|
233
|
+
newrecord.retryCount = 3;
|
|
234
|
+
newrecord.status = "Failed";
|
|
235
|
+
Callback.todoItems["ord-1"] = newrecord;
|
|
236
|
+
let published = {
|
|
237
|
+
contents: []
|
|
238
|
+
};
|
|
239
|
+
await Callback.phase2(async cmds => {
|
|
240
|
+
published.contents = cmds;
|
|
241
|
+
});
|
|
242
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
243
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
244
|
+
});
|
|
245
|
+
globalThis.test("the row carries the ceiling its count is racing", async () => {
|
|
246
|
+
collectOne();
|
|
247
|
+
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).maxRetries).toEqual(AutomationSliceFixtures$ReventlessLocal.ShipOrderSpec.maxRetries);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
195
250
|
globalThis.describe("full lifecycle", () => {
|
|
196
251
|
globalThis.test("collect → process → resolve completes the TODO item", async () => {
|
|
197
252
|
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
@@ -108,6 +108,7 @@ module ShipOrderAutomation: Reventless.AutomationSlice.Automation
|
|
|
108
108
|
with module Spec := ShipOrderSpec = {
|
|
109
109
|
let process = (id, _item: ShipOrderSpec.todoItem) =>
|
|
110
110
|
Some((id, ShipOrderSpec.CreateShipment({orderId: id})))
|
|
111
|
+
let onExhausted = (_id, _item: ShipOrderSpec.todoItem) => None
|
|
111
112
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
112
113
|
module M = Reventless.AutomationSlice.Mappings.Make(ShipOrderSpec)
|
|
113
114
|
module type Mapping = M.Mapping
|
|
@@ -117,6 +118,7 @@ module ShipOrderAutomation: Reventless.AutomationSlice.Automation
|
|
|
117
118
|
module SkipProcessAutomation: Reventless.AutomationSlice.Automation
|
|
118
119
|
with module Spec := SkipProcessSpec = {
|
|
119
120
|
let process = (_id, _item: SkipProcessSpec.todoItem) => None
|
|
121
|
+
let onExhausted = (_id, _item: SkipProcessSpec.todoItem) => None
|
|
120
122
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
121
123
|
module M = Reventless.AutomationSlice.Mappings.Make(SkipProcessSpec)
|
|
122
124
|
module type Mapping = M.Mapping
|
|
@@ -168,6 +168,10 @@ function process(id, _item) {
|
|
|
168
168
|
];
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function onExhausted(_id, _item) {
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
let moduleUrl$2 = import.meta.url;
|
|
172
176
|
|
|
173
177
|
AutomationSlice$Reventless.Mappings.Make(ShipOrderSpec);
|
|
@@ -176,6 +180,7 @@ let mappings = [ShipOrderMapping];
|
|
|
176
180
|
|
|
177
181
|
let ShipOrderAutomation = {
|
|
178
182
|
process: process,
|
|
183
|
+
onExhausted: onExhausted,
|
|
179
184
|
moduleUrl: moduleUrl$2,
|
|
180
185
|
mappings: mappings
|
|
181
186
|
};
|
|
@@ -184,6 +189,10 @@ function process$1(_id, _item) {
|
|
|
184
189
|
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
function onExhausted$1(_id, _item) {
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
187
196
|
let moduleUrl$3 = import.meta.url;
|
|
188
197
|
|
|
189
198
|
AutomationSlice$Reventless.Mappings.Make(SkipProcessSpec);
|
|
@@ -192,6 +201,7 @@ let mappings$1 = [SkipProcessMapping];
|
|
|
192
201
|
|
|
193
202
|
let SkipProcessAutomation = {
|
|
194
203
|
process: process$1,
|
|
204
|
+
onExhausted: onExhausted$1,
|
|
195
205
|
moduleUrl: moduleUrl$3,
|
|
196
206
|
mappings: mappings$1
|
|
197
207
|
};
|
|
@@ -158,6 +158,7 @@ module FromDcb = AutomationSlice.Mapping.Make(
|
|
|
158
158
|
module AutoShipAutomation: AutomationSlice.Automation with module Spec := AutoShipSpec = {
|
|
159
159
|
let process = (id, _item: AutoShipSpec.todoItem) =>
|
|
160
160
|
Some((id, AutoShipSpec.Ship({orderId: id})))
|
|
161
|
+
let onExhausted = (_id, _item: AutoShipSpec.todoItem) => None
|
|
161
162
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
162
163
|
module M = AutomationSlice.Mappings.Make(AutoShipSpec)
|
|
163
164
|
module type Mapping = M.Mapping
|
|
@@ -264,6 +264,10 @@ function process(id, _item) {
|
|
|
264
264
|
];
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function onExhausted(_id, _item) {
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
267
271
|
let moduleUrl$5 = import.meta.url;
|
|
268
272
|
|
|
269
273
|
AutomationSlice$Reventless.Mappings.Make(AutoShipSpec);
|
|
@@ -272,6 +276,7 @@ let mappings = [FromDcb];
|
|
|
272
276
|
|
|
273
277
|
let AutoShipAutomation = {
|
|
274
278
|
process: process,
|
|
279
|
+
onExhausted: onExhausted,
|
|
275
280
|
moduleUrl: moduleUrl$5,
|
|
276
281
|
mappings: mappings
|
|
277
282
|
};
|
|
@@ -117,6 +117,7 @@ module AutoFulfillAutomation: AutomationSlice.Automation
|
|
|
117
117
|
with module Spec := AutoFulfillSpec = {
|
|
118
118
|
let process = (id, item: AutoFulfillSpec.todoItem) =>
|
|
119
119
|
Some((id, AutoFulfillSpec.MarkFulfilled({orderId: item.orderId, productId: item.productId})))
|
|
120
|
+
let onExhausted = (_id, _item: AutoFulfillSpec.todoItem) => None
|
|
120
121
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
121
122
|
module M = AutomationSlice.Mappings.Make(AutoFulfillSpec)
|
|
122
123
|
module type Mapping = M.Mapping
|
|
@@ -149,6 +149,10 @@ function process(id, item) {
|
|
|
149
149
|
];
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function onExhausted(_id, _item) {
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
152
156
|
let moduleUrl$1 = import.meta.url;
|
|
153
157
|
|
|
154
158
|
AutomationSlice$Reventless.Mappings.Make(AutoFulfillSpec);
|
|
@@ -160,6 +164,7 @@ let mappings = [
|
|
|
160
164
|
|
|
161
165
|
let AutoFulfillAutomation = {
|
|
162
166
|
process: process,
|
|
167
|
+
onExhausted: onExhausted,
|
|
163
168
|
moduleUrl: moduleUrl$1,
|
|
164
169
|
mappings: mappings
|
|
165
170
|
};
|