@reventlessdev/reventless-local 3.0.0-alpha.311 → 3.0.0-alpha.313

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.
@@ -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 */