@reventlessdev/reventless-local 3.0.0-alpha.312 → 3.0.0-alpha.314
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 +15 -0
- package/package.json +9 -9
- package/src/components/AutomationSlice_Builder.res +1 -0
- package/src/components/AutomationSlice_Builder.res.mjs +3 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +45 -22
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +34 -33
- package/tests/components/automationslice/AutomationSlicePiiTest.res +178 -0
- package/tests/components/automationslice/AutomationSlicePiiTest.res.mjs +352 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.314 (2026-10-30)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.313 (2026-10-30)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **automation:** a work item with personal data is stored like an event ([f9e173f](https://github.com/ReventlessDev/reventless-core/commit/f9e173fb6f1dc671900052f2c4b6590ee99b5bdf))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.312 (2026-10-30)
|
|
7
22
|
|
|
8
23
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.314",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -35,23 +35,23 @@
|
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
38
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
39
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
40
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
41
40
|
"@reventlessdev/rescript-node": "2.0.0-alpha.24",
|
|
42
|
-
"@reventlessdev/
|
|
41
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
43
42
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.323",
|
|
44
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.171",
|
|
45
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.219",
|
|
46
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.187",
|
|
47
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.271",
|
|
47
48
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.39",
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.189"
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.190"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
53
53
|
"sury-ppx": "11.0.0",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.111"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "^12.3.1"
|
|
@@ -7,6 +7,7 @@ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
|
7
7
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
8
8
|
import * as TestRunner$ReventlessLocal from "../test/TestRunner.res.mjs";
|
|
9
9
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
10
|
+
import * as LocalCapabilities$ReventlessLocal from "../adapter/LocalCapabilities.res.mjs";
|
|
10
11
|
import * as LocalQueryDbStorage$ReventlessLocal from "../adapter/QueryDb/LocalQueryDbStorage.res.mjs";
|
|
11
12
|
import * as LocalAutomationSweep$ReventlessLocal from "./LocalAutomationSweep.res.mjs";
|
|
12
13
|
import * as AutomationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs";
|
|
@@ -42,6 +43,8 @@ function Make(Bus) {
|
|
|
42
43
|
make: QueryDbResolvers.make
|
|
43
44
|
})(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api)({
|
|
44
45
|
platformFor: LocalAutomationSchedules$ReventlessLocal.platformFor
|
|
46
|
+
})({
|
|
47
|
+
capabilities: LocalCapabilities$ReventlessLocal.capabilities
|
|
45
48
|
});
|
|
46
49
|
let Make$1 = Spec => (Automation => {
|
|
47
50
|
let Inner = CoreMaker.Make(Spec)(Automation);
|
|
@@ -29,7 +29,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
29
29
|
"OrderPlaced JSON creates a pending TODO item",
|
|
30
30
|
async () => {
|
|
31
31
|
let json = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
|
|
32
|
-
Callback.phase1([json], testContext)
|
|
32
|
+
await Callback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
33
33
|
let items = Callback.todoItems
|
|
34
34
|
expect(items->Dict.toArray->Array.length)->toBe(1)
|
|
35
35
|
let row = items->Dict.get("ord-1")->Option.getOrThrow
|
|
@@ -43,8 +43,8 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
43
43
|
async () => {
|
|
44
44
|
let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
|
|
45
45
|
let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "456 Oak Ave"}))
|
|
46
|
-
Callback.phase1([j1], testContext)
|
|
47
|
-
Callback.phase1([j2], testContext)
|
|
46
|
+
await Callback.phase1([j1], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
47
|
+
await Callback.phase1([j2], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
48
48
|
expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(1)
|
|
49
49
|
},
|
|
50
50
|
)
|
|
@@ -54,7 +54,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
54
54
|
async () => {
|
|
55
55
|
let j1 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))
|
|
56
56
|
let j2 = encodeShipOrderEvent(OrderPlaced({orderId: "ord-2", address: "456 Oak Ave"}))
|
|
57
|
-
Callback.phase1([j1, j2], testContext)
|
|
57
|
+
await Callback.phase1([j1, j2], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
58
58
|
expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(2)
|
|
59
59
|
},
|
|
60
60
|
)
|
|
@@ -63,7 +63,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
63
63
|
"ShipmentCreated does not create a TODO item",
|
|
64
64
|
async () => {
|
|
65
65
|
let json = encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))
|
|
66
|
-
Callback.phase1([json], testContext)
|
|
66
|
+
await Callback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
67
67
|
expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
|
|
68
68
|
},
|
|
69
69
|
)
|
|
@@ -73,11 +73,16 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
73
73
|
testPromise(
|
|
74
74
|
"ShipmentCreated marks pending TODO item as completed",
|
|
75
75
|
async () => {
|
|
76
|
-
Callback.phase1(
|
|
76
|
+
await Callback.phase1(
|
|
77
77
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
78
78
|
testContext,
|
|
79
|
+
~capabilities=Reventless.Capabilities.none,
|
|
80
|
+
)
|
|
81
|
+
await Callback.phase1(
|
|
82
|
+
[encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
|
|
83
|
+
testContext,
|
|
84
|
+
~capabilities=Reventless.Capabilities.none,
|
|
79
85
|
)
|
|
80
|
-
Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
|
|
81
86
|
let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
82
87
|
expect(row.status)->toBe(Completed)
|
|
83
88
|
},
|
|
@@ -86,7 +91,11 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
86
91
|
testPromise(
|
|
87
92
|
"ShipmentCreated for unknown id is a no-op",
|
|
88
93
|
async () => {
|
|
89
|
-
Callback.phase1(
|
|
94
|
+
await Callback.phase1(
|
|
95
|
+
[encodeShipOrderEvent(ShipmentCreated({orderId: "unknown"}))],
|
|
96
|
+
testContext,
|
|
97
|
+
~capabilities=Reventless.Capabilities.none,
|
|
98
|
+
)
|
|
90
99
|
expect(Callback.todoItems->Dict.toArray->Array.length)->toBe(0)
|
|
91
100
|
},
|
|
92
101
|
)
|
|
@@ -96,9 +105,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
96
105
|
testPromise(
|
|
97
106
|
"pending items produce commands via publishJsons",
|
|
98
107
|
async () => {
|
|
99
|
-
Callback.phase1(
|
|
108
|
+
await Callback.phase1(
|
|
100
109
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
101
110
|
testContext,
|
|
111
|
+
~capabilities=Reventless.Capabilities.none,
|
|
102
112
|
)
|
|
103
113
|
let publishedCommands = ref([])
|
|
104
114
|
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
@@ -118,11 +128,16 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
118
128
|
testPromise(
|
|
119
129
|
"completed items are not processed",
|
|
120
130
|
async () => {
|
|
121
|
-
Callback.phase1(
|
|
131
|
+
await Callback.phase1(
|
|
122
132
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
123
133
|
testContext,
|
|
134
|
+
~capabilities=Reventless.Capabilities.none,
|
|
135
|
+
)
|
|
136
|
+
await Callback.phase1(
|
|
137
|
+
[encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
|
|
138
|
+
testContext,
|
|
139
|
+
~capabilities=Reventless.Capabilities.none,
|
|
124
140
|
)
|
|
125
|
-
Callback.phase1([encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))], testContext)
|
|
126
141
|
let publishedCommands = ref([])
|
|
127
142
|
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
128
143
|
publishedCommands := cmds
|
|
@@ -136,7 +151,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
136
151
|
"items where process returns None are skipped",
|
|
137
152
|
async () => {
|
|
138
153
|
let json = encodeSkipProcessEvent(OrderPlaced({orderId: "ord-1"}))
|
|
139
|
-
SkipCallback.phase1([json], testContext)
|
|
154
|
+
await SkipCallback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
140
155
|
let publishedCommands = ref([])
|
|
141
156
|
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
142
157
|
publishedCommands := cmds
|
|
@@ -163,9 +178,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
163
178
|
testPromise(
|
|
164
179
|
"publishJsons failure marks items as Failed",
|
|
165
180
|
async () => {
|
|
166
|
-
Callback.phase1(
|
|
181
|
+
await Callback.phase1(
|
|
167
182
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
168
183
|
testContext,
|
|
184
|
+
~capabilities=Reventless.Capabilities.none,
|
|
169
185
|
)
|
|
170
186
|
let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
|
|
171
187
|
JsError.throwWithMessage("publish failed")
|
|
@@ -180,9 +196,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
180
196
|
testPromise(
|
|
181
197
|
"failed items with retryCount < maxRetries are retried",
|
|
182
198
|
async () => {
|
|
183
|
-
Callback.phase1(
|
|
199
|
+
await Callback.phase1(
|
|
184
200
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
185
201
|
testContext,
|
|
202
|
+
~capabilities=Reventless.Capabilities.none,
|
|
186
203
|
)
|
|
187
204
|
let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
|
|
188
205
|
JsError.throwWithMessage("publish failed")
|
|
@@ -210,10 +227,11 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
210
227
|
let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
|
|
211
228
|
JsError.throwWithMessage("publish failed")
|
|
212
229
|
|
|
213
|
-
let collectOne = () =>
|
|
214
|
-
Callback.phase1(
|
|
230
|
+
let collectOne = async () =>
|
|
231
|
+
await Callback.phase1(
|
|
215
232
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
216
233
|
testContext,
|
|
234
|
+
~capabilities=Reventless.Capabilities.none,
|
|
217
235
|
)
|
|
218
236
|
|
|
219
237
|
let statusOf = id => (Callback.todoItems->Dict.get(id)->Option.getOrThrow).status
|
|
@@ -221,7 +239,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
221
239
|
testPromise(
|
|
222
240
|
"the attempt that spends the budget writes Abandoned, not Failed",
|
|
223
241
|
async () => {
|
|
224
|
-
collectOne()
|
|
242
|
+
await collectOne()
|
|
225
243
|
// ShipOrderSpec.maxRetries is 3: attempts at retryCount 0, 1 and 2 leave the
|
|
226
244
|
// row retriable, and the third failure is the one that ends it.
|
|
227
245
|
await Callback.phase2(failingPublish)
|
|
@@ -237,7 +255,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
237
255
|
testPromise(
|
|
238
256
|
"an abandoned row is not picked up again",
|
|
239
257
|
async () => {
|
|
240
|
-
collectOne()
|
|
258
|
+
await collectOne()
|
|
241
259
|
await Callback.phase2(failingPublish)
|
|
242
260
|
await Callback.phase2(failingPublish)
|
|
243
261
|
await Callback.phase2(failingPublish)
|
|
@@ -256,7 +274,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
256
274
|
testPromise(
|
|
257
275
|
"a row left stranded at the ceiling is normalised on the next pass",
|
|
258
276
|
async () => {
|
|
259
|
-
collectOne()
|
|
277
|
+
await collectOne()
|
|
260
278
|
// What an older build wrote, and what a Spec that lowered maxRetries leaves
|
|
261
279
|
// behind: Failed at the ceiling, which no pass would ever pick up or mark.
|
|
262
280
|
let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
@@ -272,7 +290,7 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
272
290
|
testPromise(
|
|
273
291
|
"the row carries the ceiling its count is racing",
|
|
274
292
|
async () => {
|
|
275
|
-
collectOne()
|
|
293
|
+
await collectOne()
|
|
276
294
|
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)->toEqual(
|
|
277
295
|
Some(ShipOrderSpec.maxRetries),
|
|
278
296
|
)
|
|
@@ -284,9 +302,10 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
284
302
|
testPromise(
|
|
285
303
|
"collect → process → resolve completes the TODO item",
|
|
286
304
|
async () => {
|
|
287
|
-
Callback.phase1(
|
|
305
|
+
await Callback.phase1(
|
|
288
306
|
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
289
307
|
testContext,
|
|
308
|
+
~capabilities=Reventless.Capabilities.none,
|
|
290
309
|
)
|
|
291
310
|
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Pending)
|
|
292
311
|
|
|
@@ -298,7 +317,11 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
298
317
|
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
299
318
|
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Processing)
|
|
300
319
|
|
|
301
|
-
Callback.phase1(
|
|
320
|
+
await Callback.phase1(
|
|
321
|
+
[encodeShipOrderEvent(ShipmentCreated({orderId: "ord-1"}))],
|
|
322
|
+
testContext,
|
|
323
|
+
~capabilities=Reventless.Capabilities.none,
|
|
324
|
+
)
|
|
302
325
|
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(Completed)
|
|
303
326
|
|
|
304
327
|
let publishedCommands2 = ref([])
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
|
+
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
6
7
|
import * as AutomationSliceFixtures$ReventlessLocal from "./AutomationSliceFixtures.res.mjs";
|
|
7
8
|
import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
|
|
8
9
|
|
|
@@ -22,7 +23,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
22
23
|
orderId: "ord-1",
|
|
23
24
|
address: "123 Main St"
|
|
24
25
|
});
|
|
25
|
-
Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
26
|
+
await Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
26
27
|
let items = Callback.todoItems;
|
|
27
28
|
globalThis.expect(Object.entries(items).length).toBe(1);
|
|
28
29
|
let row = Stdlib_Option.getOrThrow(items["ord-1"], undefined);
|
|
@@ -40,8 +41,8 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
40
41
|
orderId: "ord-1",
|
|
41
42
|
address: "456 Oak Ave"
|
|
42
43
|
});
|
|
43
|
-
Callback.phase1([j1], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
44
|
-
Callback.phase1([j2], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
44
|
+
await Callback.phase1([j1], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
45
|
+
await Callback.phase1([j2], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
45
46
|
globalThis.expect(Object.entries(Callback.todoItems).length).toBe(1);
|
|
46
47
|
});
|
|
47
48
|
globalThis.test("multiple OrderPlaced events create multiple TODO items", async () => {
|
|
@@ -55,10 +56,10 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
55
56
|
orderId: "ord-2",
|
|
56
57
|
address: "456 Oak Ave"
|
|
57
58
|
});
|
|
58
|
-
Callback.phase1([
|
|
59
|
+
await Callback.phase1([
|
|
59
60
|
j1,
|
|
60
61
|
j2
|
|
61
|
-
], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
62
|
+
], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
62
63
|
globalThis.expect(Object.entries(Callback.todoItems).length).toBe(2);
|
|
63
64
|
});
|
|
64
65
|
globalThis.test("ShipmentCreated does not create a TODO item", async () => {
|
|
@@ -66,39 +67,39 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
66
67
|
TAG: "ShipmentCreated",
|
|
67
68
|
orderId: "ord-1"
|
|
68
69
|
});
|
|
69
|
-
Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
70
|
+
await Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
70
71
|
globalThis.expect(Object.entries(Callback.todoItems).length).toBe(0);
|
|
71
72
|
});
|
|
72
73
|
});
|
|
73
74
|
globalThis.describe("Phase 1 — resolve", () => {
|
|
74
75
|
globalThis.test("ShipmentCreated marks pending TODO item as completed", async () => {
|
|
75
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
76
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
76
77
|
TAG: "OrderPlaced",
|
|
77
78
|
orderId: "ord-1",
|
|
78
79
|
address: "123 Main St"
|
|
79
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
80
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
80
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
81
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
81
82
|
TAG: "ShipmentCreated",
|
|
82
83
|
orderId: "ord-1"
|
|
83
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
84
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
84
85
|
let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
|
|
85
86
|
globalThis.expect(row.status).toBe("Completed");
|
|
86
87
|
});
|
|
87
88
|
globalThis.test("ShipmentCreated for unknown id is a no-op", async () => {
|
|
88
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
89
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
89
90
|
TAG: "ShipmentCreated",
|
|
90
91
|
orderId: "unknown"
|
|
91
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
92
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
92
93
|
globalThis.expect(Object.entries(Callback.todoItems).length).toBe(0);
|
|
93
94
|
});
|
|
94
95
|
});
|
|
95
96
|
globalThis.describe("Phase 2 — process", () => {
|
|
96
97
|
globalThis.test("pending items produce commands via publishJsons", async () => {
|
|
97
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
98
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
98
99
|
TAG: "OrderPlaced",
|
|
99
100
|
orderId: "ord-1",
|
|
100
101
|
address: "123 Main St"
|
|
101
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
102
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
102
103
|
let publishedCommands = {
|
|
103
104
|
contents: []
|
|
104
105
|
};
|
|
@@ -113,15 +114,15 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
113
114
|
globalThis.expect(row.status).toBe("Processing");
|
|
114
115
|
});
|
|
115
116
|
globalThis.test("completed items are not processed", async () => {
|
|
116
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
117
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
117
118
|
TAG: "OrderPlaced",
|
|
118
119
|
orderId: "ord-1",
|
|
119
120
|
address: "123 Main St"
|
|
120
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
121
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
121
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
122
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
122
123
|
TAG: "ShipmentCreated",
|
|
123
124
|
orderId: "ord-1"
|
|
124
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
125
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
125
126
|
let publishedCommands = {
|
|
126
127
|
contents: []
|
|
127
128
|
};
|
|
@@ -136,7 +137,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
136
137
|
TAG: "OrderPlaced",
|
|
137
138
|
orderId: "ord-1"
|
|
138
139
|
});
|
|
139
|
-
SkipCallback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
140
|
+
await SkipCallback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
140
141
|
let publishedCommands = {
|
|
141
142
|
contents: []
|
|
142
143
|
};
|
|
@@ -159,11 +160,11 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
159
160
|
globalThis.expect(publishedCommands.contents.length).toBe(0);
|
|
160
161
|
});
|
|
161
162
|
globalThis.test("publishJsons failure marks items as Failed", async () => {
|
|
162
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
163
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
163
164
|
TAG: "OrderPlaced",
|
|
164
165
|
orderId: "ord-1",
|
|
165
166
|
address: "123 Main St"
|
|
166
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
167
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
167
168
|
let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
|
|
168
169
|
await Callback.phase2(failingPublish);
|
|
169
170
|
let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
|
|
@@ -171,11 +172,11 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
171
172
|
globalThis.expect(row.retryCount).toBe(1);
|
|
172
173
|
});
|
|
173
174
|
globalThis.test("failed items with retryCount < maxRetries are retried", async () => {
|
|
174
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
175
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
175
176
|
TAG: "OrderPlaced",
|
|
176
177
|
orderId: "ord-1",
|
|
177
178
|
address: "123 Main St"
|
|
178
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
179
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
179
180
|
let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
|
|
180
181
|
await Callback.phase2(failingPublish);
|
|
181
182
|
let row1 = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
|
|
@@ -194,14 +195,14 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
194
195
|
});
|
|
195
196
|
globalThis.describe("retry exhaustion", () => {
|
|
196
197
|
let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
|
|
197
|
-
let collectOne = () => Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
198
|
+
let collectOne = async () => await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
198
199
|
TAG: "OrderPlaced",
|
|
199
200
|
orderId: "ord-1",
|
|
200
201
|
address: "123 Main St"
|
|
201
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
202
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
202
203
|
let statusOf = id => Stdlib_Option.getOrThrow(Callback.todoItems[id], undefined).status;
|
|
203
204
|
globalThis.test("the attempt that spends the budget writes Abandoned, not Failed", async () => {
|
|
204
|
-
collectOne();
|
|
205
|
+
await collectOne();
|
|
205
206
|
await Callback.phase2(failingPublish);
|
|
206
207
|
globalThis.expect(statusOf("ord-1")).toBe("Failed");
|
|
207
208
|
await Callback.phase2(failingPublish);
|
|
@@ -211,7 +212,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
211
212
|
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
|
|
212
213
|
});
|
|
213
214
|
globalThis.test("an abandoned row is not picked up again", async () => {
|
|
214
|
-
collectOne();
|
|
215
|
+
await collectOne();
|
|
215
216
|
await Callback.phase2(failingPublish);
|
|
216
217
|
await Callback.phase2(failingPublish);
|
|
217
218
|
await Callback.phase2(failingPublish);
|
|
@@ -227,7 +228,7 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
227
228
|
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
|
|
228
229
|
});
|
|
229
230
|
globalThis.test("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
230
|
-
collectOne();
|
|
231
|
+
await collectOne();
|
|
231
232
|
let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
|
|
232
233
|
let newrecord = {...row};
|
|
233
234
|
newrecord.retryCount = 3;
|
|
@@ -243,17 +244,17 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
243
244
|
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
244
245
|
});
|
|
245
246
|
globalThis.test("the row carries the ceiling its count is racing", async () => {
|
|
246
|
-
collectOne();
|
|
247
|
+
await collectOne();
|
|
247
248
|
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).maxRetries).toEqual(AutomationSliceFixtures$ReventlessLocal.ShipOrderSpec.maxRetries);
|
|
248
249
|
});
|
|
249
250
|
});
|
|
250
251
|
globalThis.describe("full lifecycle", () => {
|
|
251
252
|
globalThis.test("collect → process → resolve completes the TODO item", async () => {
|
|
252
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
253
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
253
254
|
TAG: "OrderPlaced",
|
|
254
255
|
orderId: "ord-1",
|
|
255
256
|
address: "123 Main St"
|
|
256
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
257
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
257
258
|
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).status).toBe("Pending");
|
|
258
259
|
let publishedCommands = {
|
|
259
260
|
contents: []
|
|
@@ -264,10 +265,10 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
264
265
|
await Callback.phase2(mockPublish);
|
|
265
266
|
globalThis.expect(publishedCommands.contents.length).toBe(1);
|
|
266
267
|
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).status).toBe("Processing");
|
|
267
|
-
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
268
|
+
await Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
268
269
|
TAG: "ShipmentCreated",
|
|
269
270
|
orderId: "ord-1"
|
|
270
|
-
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
271
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
271
272
|
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).status).toBe("Completed");
|
|
272
273
|
let publishedCommands2 = {
|
|
273
274
|
contents: []
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// An automation's work item that carries personal data is stored as an event is:
|
|
2
|
+
// its marked fields sealed, its row keyed by a fingerprint of the key the mapping
|
|
3
|
+
// chose, that key sealed inside the item for `process` and `resolve`, and an item
|
|
4
|
+
// whose subject was erased completed without processing.
|
|
5
|
+
|
|
6
|
+
open JestGlobals
|
|
7
|
+
|
|
8
|
+
module Source = {
|
|
9
|
+
module Id = Reventless.Id.String
|
|
10
|
+
let name = "GreetDcbEventLog"
|
|
11
|
+
@schema
|
|
12
|
+
type event =
|
|
13
|
+
| MemberJoined({memberId: @s.matches(Reventless.DcbTag.string) string, email: string})
|
|
14
|
+
| MemberGreeted({memberId: @s.matches(Reventless.DcbTag.string) string, email: string})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module Spec = {
|
|
18
|
+
let name = "GreetMember"
|
|
19
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
20
|
+
|
|
21
|
+
@schema
|
|
22
|
+
type todoItem = {
|
|
23
|
+
memberId: string,
|
|
24
|
+
email: @s.matches(Reventless.Pii.mark(~subject="memberId", S.string)) string,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@schema
|
|
28
|
+
type command = SendGreeting({memberId: @s.matches(Reventless.DcbTag.string) string, key: string})
|
|
29
|
+
|
|
30
|
+
let maxRetries = 3
|
|
31
|
+
let targetName = "SendGreeting"
|
|
32
|
+
let schedule = None
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Keyed by the address, as a translation keyed by contact would be.
|
|
36
|
+
module Mapping = Reventless.AutomationSlice.Mapping.Make(
|
|
37
|
+
Source,
|
|
38
|
+
Spec,
|
|
39
|
+
{
|
|
40
|
+
let collect = (event: Source.event, ~sourceId as _, _ctx) =>
|
|
41
|
+
switch event {
|
|
42
|
+
| MemberJoined({memberId, email}) => [
|
|
43
|
+
(`${memberId}:${email}`, ({memberId, email}: Spec.todoItem)),
|
|
44
|
+
]
|
|
45
|
+
| MemberGreeted(_) => []
|
|
46
|
+
}
|
|
47
|
+
let resolve = (event: Source.event) =>
|
|
48
|
+
switch event {
|
|
49
|
+
| MemberGreeted({memberId, email}) => Some(`${memberId}:${email}`)
|
|
50
|
+
| MemberJoined(_) => None
|
|
51
|
+
}
|
|
52
|
+
let reschedule = _ => []
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
let processed: ref<array<(string, string)>> = ref([])
|
|
57
|
+
|
|
58
|
+
module Automation: Reventless.AutomationSlice.Automation with module Spec := Spec = {
|
|
59
|
+
let process = (key, item: Spec.todoItem, ~now as _) => {
|
|
60
|
+
processed := processed.contents->Array.concat([(key, item.email)])
|
|
61
|
+
Some((item.memberId, Spec.SendGreeting({memberId: item.memberId, key})))
|
|
62
|
+
}
|
|
63
|
+
let onExhausted = (_id, _item: Spec.todoItem) => None
|
|
64
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
65
|
+
module M = Reventless.AutomationSlice.Mappings.Make(Spec)
|
|
66
|
+
module type Mapping = M.Mapping
|
|
67
|
+
let mappings: array<module(Mapping)> = [module(Mapping)]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module Callback = ReventlessCore.AutomationSlice_Callback.Make(Spec, Automation)
|
|
71
|
+
|
|
72
|
+
let context: Reventless.AutomationSlice.context = {
|
|
73
|
+
environment: "test",
|
|
74
|
+
platformName: "local",
|
|
75
|
+
pluginName: "TestPlugin",
|
|
76
|
+
sliceName: "GreetMember",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let envelope = (event: Source.event) =>
|
|
80
|
+
JSON.Encode.object(
|
|
81
|
+
Dict.fromArray([
|
|
82
|
+
("id", JSON.Encode.string("m-1")),
|
|
83
|
+
(
|
|
84
|
+
"meta",
|
|
85
|
+
ReventlessCore.Message.generateMeta(~service=Source.name)->Reventless.Util_Sury.toJson(
|
|
86
|
+
Reventless.Message.metaSchema,
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
("event", event->Reventless.Util_Sury.toJson(Source.eventSchema)),
|
|
90
|
+
]),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
let toBase64 = (s: string) =>
|
|
94
|
+
NodeCrypto.bufferFromString(s, "utf8")->NodeCrypto.bufferToString("base64")
|
|
95
|
+
let fromBase64 = (s: string) =>
|
|
96
|
+
NodeCrypto.bufferFromString(s, "base64")->NodeCrypto.bufferToString("utf8")
|
|
97
|
+
|
|
98
|
+
let erased = ref(false)
|
|
99
|
+
let provider: Reventless.Pii.provider = {
|
|
100
|
+
name: "fake",
|
|
101
|
+
encrypts: true,
|
|
102
|
+
seal: async (~subject, ~field as _, ~forgettable as _, json) => Ok(
|
|
103
|
+
Reventless.Pii.wrap(`enc:v1:${subject}:fake:${json->JSON.stringify->toBase64}`),
|
|
104
|
+
),
|
|
105
|
+
prefetch: async _ => Ok(),
|
|
106
|
+
openNow: (~field, json) =>
|
|
107
|
+
switch json->Reventless.Pii.unwrap->Option.flatMap(Reventless.Pii.parseStored) {
|
|
108
|
+
| Some(Cipher(_)) if erased.contents => Ok(Reventless.Pii.wrap(Reventless.Pii.erasedMarker))
|
|
109
|
+
| Some(Cipher({bytes})) => Ok(bytes->fromBase64->JSON.parseOrThrow)
|
|
110
|
+
| _ => Error(Reventless.Pii.UnknownForm({field: field}))
|
|
111
|
+
},
|
|
112
|
+
release: async (~subject as _, ~ref as _) => Ok(),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let keyed: Reventless.Capabilities.t = {
|
|
116
|
+
...Reventless.Capabilities.none,
|
|
117
|
+
secrets: Reventless.Secrets.fixed(
|
|
118
|
+
~token="t",
|
|
119
|
+
~hash=s => s,
|
|
120
|
+
~fingerprint=(~purpose, input) =>
|
|
121
|
+
`fp(${purpose})#${input->toBase64->String.length->Int.toString}`,
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let joined = Source.MemberJoined({memberId: "m-1", email: "ada@example.org"})
|
|
126
|
+
let greeted = Source.MemberGreeted({memberId: "m-1", email: "ada@example.org"})
|
|
127
|
+
let rows = () => Callback.todoItems->Dict.toArray
|
|
128
|
+
let published: ref<array<Reventless.Message.commandJson>> = ref([])
|
|
129
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async msgs => {
|
|
130
|
+
published := published.contents->Array.concat(msgs)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
describe("an automation's work item with personal data", () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
Callback.todoItems->Dict.keysToArray->Array.forEach(k => Callback.todoItems->Dict.delete(k))
|
|
136
|
+
processed := []
|
|
137
|
+
published := []
|
|
138
|
+
erased := false
|
|
139
|
+
Reventless.Pii.Provider.install(provider)
|
|
140
|
+
})
|
|
141
|
+
afterEach(() => Reventless.Pii.Provider.install(Reventless.Pii.plain))
|
|
142
|
+
|
|
143
|
+
testPromise("is stored sealed, under a key that holds no address", async () => {
|
|
144
|
+
await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
|
|
145
|
+
expect(rows()->JSON.stringifyAny->Option.getOr("")->String.includes("ada@example.org"))->toBe(
|
|
146
|
+
false,
|
|
147
|
+
)
|
|
148
|
+
expect(
|
|
149
|
+
rows()->Array.every(((id, _)) => id->String.startsWith("fp(automationWorkItem:GreetMember)")),
|
|
150
|
+
)->toBe(true)
|
|
151
|
+
expect(rows()->Array.length)->toBe(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testPromise("is processed with the key and the value it was collected with", async () => {
|
|
155
|
+
await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
|
|
156
|
+
await Callback.phase2(publish)
|
|
157
|
+
expect(processed.contents)->toEqual([("m-1:ada@example.org", "ada@example.org")])
|
|
158
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
testPromise("is resolved by the key the mapping chose", async () => {
|
|
162
|
+
await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
|
|
163
|
+
await Callback.phase1([envelope(greeted)], context, ~capabilities=keyed)
|
|
164
|
+
expect(rows()->Array.map(((_, row)) => row.status))->toEqual([
|
|
165
|
+
ReventlessCore.AutomationSlice_Callback.Completed,
|
|
166
|
+
])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
testPromise("is completed without processing once its subject is erased", async () => {
|
|
170
|
+
await Callback.phase1([envelope(joined)], context, ~capabilities=keyed)
|
|
171
|
+
erased := true
|
|
172
|
+
await Callback.phase2(publish)
|
|
173
|
+
expect(processed.contents)->toEqual([])
|
|
174
|
+
expect(rows()->Array.map(((_, row)) => row.status))->toEqual([
|
|
175
|
+
ReventlessCore.AutomationSlice_Callback.Completed,
|
|
176
|
+
])
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
|
+
import * as Nodebuffer from "node:buffer";
|
|
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 Pii$Reventless from "@reventlessdev/reventless-spec/src/components/Pii.res.mjs";
|
|
9
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
|
+
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
11
|
+
import * as Secrets$Reventless from "@reventlessdev/reventless-spec/src/semantic/Secrets.res.mjs";
|
|
12
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
13
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
14
|
+
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
15
|
+
import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
|
|
16
|
+
import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
|
|
17
|
+
|
|
18
|
+
let name = "GreetDcbEventLog";
|
|
19
|
+
|
|
20
|
+
let eventSchema = Sury.union([
|
|
21
|
+
Sury.$schema(s => ({
|
|
22
|
+
TAG: "MemberJoined",
|
|
23
|
+
memberId: s.m(DcbTag$Reventless.string),
|
|
24
|
+
email: s.m(Sury.string)
|
|
25
|
+
})),
|
|
26
|
+
Sury.$schema(s => ({
|
|
27
|
+
TAG: "MemberGreeted",
|
|
28
|
+
memberId: s.m(DcbTag$Reventless.string),
|
|
29
|
+
email: s.m(Sury.string)
|
|
30
|
+
}))
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
let Source = {
|
|
34
|
+
Id: undefined,
|
|
35
|
+
name: name,
|
|
36
|
+
eventSchema: eventSchema
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let moduleUrl = import.meta.url;
|
|
40
|
+
|
|
41
|
+
let todoItemSchema = Sury.$schema(s => ({
|
|
42
|
+
memberId: s.m(Sury.string),
|
|
43
|
+
email: s.m(Pii$Reventless.mark("memberId", undefined, Sury.string))
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
let commandSchema = Sury.$schema(s => ({
|
|
47
|
+
TAG: "SendGreeting",
|
|
48
|
+
memberId: s.m(DcbTag$Reventless.string),
|
|
49
|
+
key: s.m(Sury.string)
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
function authorizationOf(param) {
|
|
53
|
+
return "AllowAuthenticated";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function commandTransition(param) {
|
|
57
|
+
return "Undeclared";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let traits = [];
|
|
61
|
+
|
|
62
|
+
let Spec = {
|
|
63
|
+
name: "GreetMember",
|
|
64
|
+
moduleUrl: moduleUrl,
|
|
65
|
+
todoItemSchema: todoItemSchema,
|
|
66
|
+
commandSchema: commandSchema,
|
|
67
|
+
maxRetries: 3,
|
|
68
|
+
targetName: "SendGreeting",
|
|
69
|
+
schedule: undefined,
|
|
70
|
+
authorizationOf: authorizationOf,
|
|
71
|
+
commandTransition: commandTransition,
|
|
72
|
+
traits: traits
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function collect(event, param, _ctx) {
|
|
76
|
+
if (event.TAG !== "MemberJoined") {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
let email = event.email;
|
|
80
|
+
let memberId = event.memberId;
|
|
81
|
+
return [[
|
|
82
|
+
memberId + `:` + email,
|
|
83
|
+
{
|
|
84
|
+
memberId: memberId,
|
|
85
|
+
email: email
|
|
86
|
+
}
|
|
87
|
+
]];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolve(event) {
|
|
91
|
+
if (event.TAG === "MemberJoined") {
|
|
92
|
+
return;
|
|
93
|
+
} else {
|
|
94
|
+
return event.memberId + `:` + event.email;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reschedule(param) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let Mapping = AutomationSlice$Reventless.Mapping.Make({
|
|
103
|
+
Id: Id$Reventless.$$String,
|
|
104
|
+
name: name,
|
|
105
|
+
eventSchema: eventSchema
|
|
106
|
+
})(Spec)({
|
|
107
|
+
collect: collect,
|
|
108
|
+
resolve: resolve,
|
|
109
|
+
reschedule: reschedule
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let processed = {
|
|
113
|
+
contents: []
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function process(key, item, param) {
|
|
117
|
+
processed.contents = processed.contents.concat([[
|
|
118
|
+
key,
|
|
119
|
+
item.email
|
|
120
|
+
]]);
|
|
121
|
+
return [
|
|
122
|
+
item.memberId,
|
|
123
|
+
{
|
|
124
|
+
TAG: "SendGreeting",
|
|
125
|
+
memberId: item.memberId,
|
|
126
|
+
key: key
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function onExhausted(_id, _item) {
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let moduleUrl$1 = import.meta.url;
|
|
136
|
+
|
|
137
|
+
AutomationSlice$Reventless.Mappings.Make(Spec);
|
|
138
|
+
|
|
139
|
+
let mappings = [Mapping];
|
|
140
|
+
|
|
141
|
+
let Automation = {
|
|
142
|
+
process: process,
|
|
143
|
+
onExhausted: onExhausted,
|
|
144
|
+
moduleUrl: moduleUrl$1,
|
|
145
|
+
mappings: mappings
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let Callback = AutomationSlice_Callback$ReventlessCore.Make(Spec)(Automation);
|
|
149
|
+
|
|
150
|
+
let context = {
|
|
151
|
+
environment: "test",
|
|
152
|
+
platformName: "local",
|
|
153
|
+
pluginName: "TestPlugin",
|
|
154
|
+
sliceName: "GreetMember"
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function envelope(event) {
|
|
158
|
+
return Object.fromEntries([
|
|
159
|
+
[
|
|
160
|
+
"id",
|
|
161
|
+
"m-1"
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
"meta",
|
|
165
|
+
Util_Sury$Reventless.toJson(Message$ReventlessCore.generateMeta(name, undefined, undefined, undefined, undefined, undefined, undefined), Message$Reventless.metaSchema)
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
"event",
|
|
169
|
+
Util_Sury$Reventless.toJson(event, eventSchema)
|
|
170
|
+
]
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toBase64(s) {
|
|
175
|
+
return Nodebuffer.Buffer.from(s, "utf8").toString("base64");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function fromBase64(s) {
|
|
179
|
+
return Nodebuffer.Buffer.from(s, "base64").toString("utf8");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let erased = {
|
|
183
|
+
contents: false
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
async function provider_seal(subject, param, param$1, json) {
|
|
187
|
+
return {
|
|
188
|
+
TAG: "Ok",
|
|
189
|
+
_0: Pii$Reventless.wrap(`enc:v1:` + subject + `:fake:` + toBase64(JSON.stringify(json)))
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function provider_prefetch(param) {
|
|
194
|
+
return {
|
|
195
|
+
TAG: "Ok",
|
|
196
|
+
_0: undefined
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function provider_openNow(field, json) {
|
|
201
|
+
let match = Stdlib_Option.flatMap(Pii$Reventless.unwrap(json), Pii$Reventless.parseStored);
|
|
202
|
+
if (match === undefined) {
|
|
203
|
+
return {
|
|
204
|
+
TAG: "Error",
|
|
205
|
+
_0: {
|
|
206
|
+
TAG: "UnknownForm",
|
|
207
|
+
field: field
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
switch (match.TAG) {
|
|
212
|
+
case "Cipher" :
|
|
213
|
+
if (erased.contents) {
|
|
214
|
+
return {
|
|
215
|
+
TAG: "Ok",
|
|
216
|
+
_0: Pii$Reventless.wrap(Pii$Reventless.erasedMarker)
|
|
217
|
+
};
|
|
218
|
+
} else {
|
|
219
|
+
return {
|
|
220
|
+
TAG: "Ok",
|
|
221
|
+
_0: JSON.parse(fromBase64(match.bytes))
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
case "ErasedForm" :
|
|
225
|
+
case "ForgottenForm" :
|
|
226
|
+
return {
|
|
227
|
+
TAG: "Error",
|
|
228
|
+
_0: {
|
|
229
|
+
TAG: "UnknownForm",
|
|
230
|
+
field: field
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function provider_release(param, param$1) {
|
|
237
|
+
return {
|
|
238
|
+
TAG: "Ok",
|
|
239
|
+
_0: undefined
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let provider = {
|
|
244
|
+
name: "fake",
|
|
245
|
+
encrypts: true,
|
|
246
|
+
seal: provider_seal,
|
|
247
|
+
prefetch: provider_prefetch,
|
|
248
|
+
openNow: provider_openNow,
|
|
249
|
+
release: provider_release
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
let keyed_geocode = Capabilities$Reventless.none.geocode;
|
|
253
|
+
|
|
254
|
+
let keyed_messaging = Capabilities$Reventless.none.messaging;
|
|
255
|
+
|
|
256
|
+
let keyed_identityProvider = Capabilities$Reventless.none.identityProvider;
|
|
257
|
+
|
|
258
|
+
let keyed_secrets = Secrets$Reventless.fixed("t", s => s, (purpose, input) => `fp(` + purpose + `)#` + toBase64(input).length.toString());
|
|
259
|
+
|
|
260
|
+
let keyed_stash = Capabilities$Reventless.none.stash;
|
|
261
|
+
|
|
262
|
+
let keyed = {
|
|
263
|
+
geocode: keyed_geocode,
|
|
264
|
+
messaging: keyed_messaging,
|
|
265
|
+
identityProvider: keyed_identityProvider,
|
|
266
|
+
secrets: keyed_secrets,
|
|
267
|
+
stash: keyed_stash
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
let joined = {
|
|
271
|
+
TAG: "MemberJoined",
|
|
272
|
+
memberId: "m-1",
|
|
273
|
+
email: "ada@example.org"
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let greeted = {
|
|
277
|
+
TAG: "MemberGreeted",
|
|
278
|
+
memberId: "m-1",
|
|
279
|
+
email: "ada@example.org"
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
function rows() {
|
|
283
|
+
return Object.entries(Callback.todoItems);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let published = {
|
|
287
|
+
contents: []
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
async function publish(msgs) {
|
|
291
|
+
published.contents = published.contents.concat(msgs);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
globalThis.describe("an automation's work item with personal data", () => {
|
|
295
|
+
globalThis.beforeEach(() => {
|
|
296
|
+
Object.keys(Callback.todoItems).forEach(k => Stdlib_Dict.$$delete(Callback.todoItems, k));
|
|
297
|
+
processed.contents = [];
|
|
298
|
+
published.contents = [];
|
|
299
|
+
erased.contents = false;
|
|
300
|
+
Pii$Reventless.Provider.install(provider);
|
|
301
|
+
});
|
|
302
|
+
globalThis.afterEach(() => Pii$Reventless.Provider.install(Pii$Reventless.plain));
|
|
303
|
+
globalThis.test("is stored sealed, under a key that holds no address", async () => {
|
|
304
|
+
await Callback.phase1([envelope(joined)], context, keyed);
|
|
305
|
+
globalThis.expect(Stdlib_Option.getOr(JSON.stringify(Object.entries(Callback.todoItems)), "").includes("ada@example.org")).toBe(false);
|
|
306
|
+
globalThis.expect(Object.entries(Callback.todoItems).every(param => param[0].startsWith("fp(automationWorkItem:GreetMember)"))).toBe(true);
|
|
307
|
+
globalThis.expect(Object.entries(Callback.todoItems).length).toBe(1);
|
|
308
|
+
});
|
|
309
|
+
globalThis.test("is processed with the key and the value it was collected with", async () => {
|
|
310
|
+
await Callback.phase1([envelope(joined)], context, keyed);
|
|
311
|
+
await Callback.phase2(publish);
|
|
312
|
+
globalThis.expect(processed.contents).toEqual([[
|
|
313
|
+
"m-1:ada@example.org",
|
|
314
|
+
"ada@example.org"
|
|
315
|
+
]]);
|
|
316
|
+
globalThis.expect(published.contents.length).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
globalThis.test("is resolved by the key the mapping chose", async () => {
|
|
319
|
+
await Callback.phase1([envelope(joined)], context, keyed);
|
|
320
|
+
await Callback.phase1([envelope(greeted)], context, keyed);
|
|
321
|
+
globalThis.expect(Object.entries(Callback.todoItems).map(param => param[1].status)).toEqual(["Completed"]);
|
|
322
|
+
});
|
|
323
|
+
globalThis.test("is completed without processing once its subject is erased", async () => {
|
|
324
|
+
await Callback.phase1([envelope(joined)], context, keyed);
|
|
325
|
+
erased.contents = true;
|
|
326
|
+
await Callback.phase2(publish);
|
|
327
|
+
globalThis.expect(processed.contents).toEqual([]);
|
|
328
|
+
globalThis.expect(Object.entries(Callback.todoItems).map(param => param[1].status)).toEqual(["Completed"]);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
export {
|
|
333
|
+
Source,
|
|
334
|
+
Spec,
|
|
335
|
+
Mapping,
|
|
336
|
+
processed,
|
|
337
|
+
Automation,
|
|
338
|
+
Callback,
|
|
339
|
+
context,
|
|
340
|
+
envelope,
|
|
341
|
+
toBase64,
|
|
342
|
+
fromBase64,
|
|
343
|
+
erased,
|
|
344
|
+
provider,
|
|
345
|
+
keyed,
|
|
346
|
+
joined,
|
|
347
|
+
greeted,
|
|
348
|
+
rows,
|
|
349
|
+
published,
|
|
350
|
+
publish,
|
|
351
|
+
}
|
|
352
|
+
/* eventSchema Not a pure module */
|