@reventlessdev/reventless-local 3.0.0-alpha.281 → 3.0.0-alpha.283

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res +5 -4
  4. package/src/Platform.res.mjs +12 -12
  5. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +6 -25
  6. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +4 -20
  7. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +34 -38
  8. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +14 -6
  9. package/src/adapter/PlatformGraphQL_Server.res +1 -1
  10. package/tests/adapter/CommandAuthorizationTest.res +7 -8
  11. package/tests/adapter/CommandAuthorizationTest.res.mjs +5 -5
  12. package/tests/adapter/InboundTranslationMutationTest.res +95 -7
  13. package/tests/adapter/InboundTranslationMutationTest.res.mjs +97 -9
  14. package/tests/components/aggregate/AggregateFixtures.res.mjs +3 -3
  15. package/tests/components/aggregate/AggregateSelfMappingFixtures.res.mjs +3 -3
  16. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +4 -4
  17. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -8
  18. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +2 -2
  19. package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +3 -3
  20. package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +2 -2
  21. package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +2 -2
  22. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +3 -3
  23. package/tests/components/dcb/DcbFixtures.res.mjs +3 -3
  24. package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +5 -5
  25. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +116 -8
  26. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +160 -19
  27. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +58 -0
  28. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +145 -7
  29. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +3 -3
  30. package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +3 -3
  31. package/tests/plugin/PluginBehavior_GWT.res.mjs +2 -2
  32. package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +1 -1
@@ -224,11 +224,8 @@ describe("InboundTranslationSlice Callback", () => {
224
224
 
225
225
  type role = Reventless.Role.name
226
226
 
227
- let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
227
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> =>
228
228
  AllowAuthenticated
229
- type lifecycleState = unit
230
- let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
231
- Unrestricted
232
229
  }
233
230
 
234
231
  module MultiTranslation = {
@@ -293,11 +290,8 @@ describe("InboundTranslationSlice Callback", () => {
293
290
 
294
291
  type role = Reventless.Role.name
295
292
 
296
- let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
293
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> =>
297
294
  AllowAuthenticated
298
- type lifecycleState = unit
299
- let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
300
- Unrestricted
301
295
  }
302
296
 
303
297
  module EmptyTranslation = {
@@ -329,3 +323,117 @@ describe("InboundTranslationSlice Callback", () => {
329
323
  )
330
324
  })
331
325
  })
326
+
327
+ module CatalogFeedTranslation = {
328
+ let translate = CatalogFeedSpec.translate
329
+ let moduleUrl = CatalogFeedSpec.moduleUrl
330
+ }
331
+ module CatalogFeedCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
332
+ CatalogFeedSpec,
333
+ CatalogFeedTranslation,
334
+ )
335
+
336
+ let caller = (groups): Reventless.Identity.t => {
337
+ userId: "u-1",
338
+ username: "alice",
339
+ groups,
340
+ provider: InMemory,
341
+ }
342
+
343
+ let catalogInput = (kind): JSON.t =>
344
+ Dict.fromArray([
345
+ ("kind", JSON.Encode.string(kind)),
346
+ ("id", JSON.Encode.string("x-1")),
347
+ ])->JSON.Encode.object
348
+
349
+ describe("receive checks each command against its own rule", () => {
350
+ let published = ref([])
351
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
352
+ published := published.contents->Array.concat(cmds)
353
+ let _ = beforeEach(() => published := [])
354
+
355
+ testPromise(
356
+ "a caller lacking one command's role is refused, and nothing is published",
357
+ async () => {
358
+ let result = await CatalogFeedCallback.receive(
359
+ publish,
360
+ catalogInput("both"),
361
+ ~caller=caller(["Merchandiser"]),
362
+ )
363
+ switch result {
364
+ | Error({requestId, ?errorCode}) =>
365
+ expect(errorCode)->toEqual(Some("Forbidden"))
366
+ let row = CatalogFeedCallback.auditLog->Dict.get(requestId)
367
+ expect(row->Option.map(r => r.status))->toEqual(
368
+ Some(ReventlessCore.InboundTranslationSlice_Callback.Failure),
369
+ )
370
+ | Ok(_) => expect(true)->toBe(false)
371
+ }
372
+ expect(published.contents->Array.length)->toBe(0)
373
+ },
374
+ )
375
+
376
+ testPromise("a caller holding every command's role publishes them all", async () => {
377
+ let result = await CatalogFeedCallback.receive(
378
+ publish,
379
+ catalogInput("both"),
380
+ ~caller=caller(["Merchandiser", "Admin"]),
381
+ )
382
+ expect(result->Result.isOk)->toBe(true)
383
+ expect(published.contents->Array.length)->toBe(2)
384
+ })
385
+
386
+ testPromise("the role one command needs is enough when the input yields only it", async () => {
387
+ let result = await CatalogFeedCallback.receive(
388
+ publish,
389
+ catalogInput("product"),
390
+ ~caller=caller(["Merchandiser"]),
391
+ )
392
+ expect(result->Result.isOk)->toBe(true)
393
+ expect(published.contents->Array.length)->toBe(1)
394
+ })
395
+
396
+ testPromise("no caller is the platform itself, and publishes", async () => {
397
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"))
398
+ expect(result->Result.isOk)->toBe(true)
399
+ expect(published.contents->Array.length)->toBe(2)
400
+ })
401
+
402
+ testPromise("a refusal answers CommandRejected with Forbidden", async () => {
403
+ let result = await CatalogFeedCallback.receive(
404
+ publish,
405
+ catalogInput("both"),
406
+ ~caller=Reventless.Identity.anonymous,
407
+ )
408
+ switch result->ReventlessCore.InboundTranslationSlice_Callback.receiveResultToOutcome {
409
+ | Rejected({errorCode}) => expect(errorCode)->toBe("Forbidden")
410
+ | _ => expect(true)->toBe(false)
411
+ }
412
+ })
413
+ })
414
+
415
+ module NoteFeedTranslation = {
416
+ let translate = NoteFeedSpec.translate
417
+ let moduleUrl = NoteFeedSpec.moduleUrl
418
+ }
419
+ module NoteFeedCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
420
+ NoteFeedSpec,
421
+ NoteFeedTranslation,
422
+ )
423
+
424
+ describe("receive encodes commands with their schema", () => {
425
+ testPromise("an absent option the schema writes as null decodes at the target", async () => {
426
+ let published = ref([])
427
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds => published := cmds
428
+ let _ = await NoteFeedCallback.receive(
429
+ publish,
430
+ Dict.fromArray([("orderId", JSON.Encode.string("ord-1"))])->JSON.Encode.object,
431
+ )
432
+
433
+ let commandJson = (published.contents->Array.getUnsafe(0)).commandJson
434
+ // The target decodes with the same schema; the runtime value would omit `note`.
435
+ let decoded = commandJson->S.parseOrThrow(~to=NoteFeedSpec.commandSchema)
436
+ expect(decoded)->toEqual(NoteFeedSpec.Annotate({orderId: "ord-1", note: None}))
437
+ expect(commandJson->JSON.stringify->String.includes(`"note":null`))->toBe(true)
438
+ })
439
+ })
@@ -3,8 +3,10 @@
3
3
  import * as Sury from "sury";
4
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
8
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
9
+ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
8
10
  import * as InboundTranslationSliceFixtures$ReventlessLocal from "./InboundTranslationSliceFixtures.res.mjs";
9
11
  import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
10
12
 
@@ -24,8 +26,7 @@ let Callback = InboundTranslationSlice_Callback$ReventlessCore.Make({
24
26
  commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandSchema,
25
27
  targetName: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.targetName,
26
28
  externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalSystem,
27
- commandAuthorization: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandAuthorization,
28
- commandTransition: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandTransition
29
+ authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.authorizationOf
29
30
  })(PaymentWebhookTranslation);
30
31
 
31
32
  globalThis.describe("InboundTranslationSlice Callback", () => {
@@ -45,7 +46,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
45
46
  orderId: "ord-1",
46
47
  status: "completed"
47
48
  };
48
- let result = await Callback.receive(mockPublish, inputJson);
49
+ let result = await Callback.receive(mockPublish, inputJson, undefined);
49
50
  if (result.TAG === "Ok") {
50
51
  let match = result._0;
51
52
  globalThis.expect(match.targetIds).toEqual(["ord-1"]);
@@ -76,7 +77,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
76
77
  orderId: "ord-1",
77
78
  status: "pending"
78
79
  };
79
- let result = await Callback.receive(mockPublish, inputJson);
80
+ let result = await Callback.receive(mockPublish, inputJson, undefined);
80
81
  if (result.TAG === "Ok") {
81
82
  globalThis.expect(true).toBe(false);
82
83
  } else {
@@ -93,7 +94,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
93
94
  let inputJson = {
94
95
  unexpected: "data"
95
96
  };
96
- let result = await Callback.receive(mockPublish, inputJson);
97
+ let result = await Callback.receive(mockPublish, inputJson, undefined);
97
98
  if (result.TAG === "Ok") {
98
99
  globalThis.expect(true).toBe(false);
99
100
  } else {
@@ -111,7 +112,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
111
112
  orderId: "ord-1",
112
113
  status: "completed"
113
114
  };
114
- let result = await Callback.receive(failingPublish, inputJson);
115
+ let result = await Callback.receive(failingPublish, inputJson, undefined);
115
116
  if (result.TAG === "Ok") {
116
117
  globalThis.expect(true).toBe(false);
117
118
  } else {
@@ -130,7 +131,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
130
131
  paymentId: "pay-1",
131
132
  orderId: "ord-1",
132
133
  status: "completed"
133
- });
134
+ }, undefined);
134
135
  let firstId = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(first);
135
136
  globalThis.expect(Stdlib_Option.isSome(InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, firstId))).toBe(true);
136
137
  globalThis.expect(Object.entries(Callback.auditLog).length).toBe(0);
@@ -138,7 +139,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
138
139
  paymentId: "pay-1",
139
140
  orderId: "ord-1",
140
141
  status: "completed"
141
- });
142
+ }, undefined);
142
143
  let secondId = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(second);
143
144
  globalThis.expect(secondId === firstId).toBe(false);
144
145
  globalThis.expect(Object.entries(Callback.auditLog).length).toBe(1);
@@ -151,7 +152,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
151
152
  paymentId: "pay-1",
152
153
  orderId: "ord-1",
153
154
  status: "pending"
154
- });
155
+ }, undefined);
155
156
  let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
156
157
  let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, id);
157
158
  if (row !== undefined) {
@@ -185,8 +186,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
185
186
  }
186
187
  ])
187
188
  });
188
- let commandAuthorization = param => "AllowAuthenticated";
189
- let commandTransition = param => "Unrestricted";
189
+ let authorizationOf = param => "AllowAuthenticated";
190
190
  let MultiTranslation = {
191
191
  translate: translate,
192
192
  moduleUrl: moduleUrl
@@ -198,8 +198,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
198
198
  commandSchema: commandSchema,
199
199
  targetName: "ConfirmPayment",
200
200
  externalSystem: undefined,
201
- commandAuthorization: commandAuthorization,
202
- commandTransition: commandTransition
201
+ authorizationOf: authorizationOf
203
202
  })(MultiTranslation);
204
203
  let publishedCommands = {
205
204
  contents: []
@@ -215,7 +214,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
215
214
  "pay-3"
216
215
  ]
217
216
  };
218
- let result = await MultiCallback.receive(mockPublish, inputJson);
217
+ let result = await MultiCallback.receive(mockPublish, inputJson, undefined);
219
218
  if (result.TAG === "Ok") {
220
219
  let match = result._0;
221
220
  globalThis.expect(match.targetIds).toEqual([
@@ -249,8 +248,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
249
248
  TAG: "Ok",
250
249
  _0: []
251
250
  });
252
- let commandAuthorization = param => "AllowAuthenticated";
253
- let commandTransition = param => "Unrestricted";
251
+ let authorizationOf = param => "AllowAuthenticated";
254
252
  let EmptyTranslation = {
255
253
  translate: translate,
256
254
  moduleUrl: moduleUrl
@@ -262,8 +260,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
262
260
  commandSchema: commandSchema,
263
261
  targetName: "ConfirmPayment",
264
262
  externalSystem: undefined,
265
- commandAuthorization: commandAuthorization,
266
- commandTransition: commandTransition
263
+ authorizationOf: authorizationOf
267
264
  })(EmptyTranslation);
268
265
  let publishedCommands = {
269
266
  contents: []
@@ -274,7 +271,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
274
271
  let inputJson = {
275
272
  orderId: "ord-1"
276
273
  };
277
- let result = await EmptyCallback.receive(mockPublish, inputJson);
274
+ let result = await EmptyCallback.receive(mockPublish, inputJson, undefined);
278
275
  if (result.TAG === "Ok") {
279
276
  let match = result._0;
280
277
  globalThis.expect(match.targetIds).toEqual([]);
@@ -287,8 +284,152 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
287
284
  });
288
285
  });
289
286
 
287
+ let translate$1 = InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.translate;
288
+
289
+ let moduleUrl$1 = InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.moduleUrl;
290
+
291
+ let CatalogFeedTranslation = {
292
+ translate: translate$1,
293
+ moduleUrl: moduleUrl$1
294
+ };
295
+
296
+ let CatalogFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
297
+ name: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.name,
298
+ moduleUrl: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.moduleUrl,
299
+ externalInputSchema: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalInputSchema,
300
+ commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.commandSchema,
301
+ targetName: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.targetName,
302
+ externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalSystem,
303
+ authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.authorizationOf
304
+ })(CatalogFeedTranslation);
305
+
306
+ function caller(groups) {
307
+ return {
308
+ userId: "u-1",
309
+ username: "alice",
310
+ groups: groups,
311
+ provider: "InMemory"
312
+ };
313
+ }
314
+
315
+ function catalogInput(kind) {
316
+ return Object.fromEntries([
317
+ [
318
+ "kind",
319
+ kind
320
+ ],
321
+ [
322
+ "id",
323
+ "x-1"
324
+ ]
325
+ ]);
326
+ }
327
+
328
+ globalThis.describe("receive checks each command against its own rule", () => {
329
+ let published = {
330
+ contents: []
331
+ };
332
+ let publish = async cmds => {
333
+ published.contents = published.contents.concat(cmds);
334
+ };
335
+ globalThis.beforeEach(() => {
336
+ published.contents = [];
337
+ });
338
+ globalThis.test("a caller lacking one command's role is refused, and nothing is published", async () => {
339
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), caller(["Merchandiser"]));
340
+ if (result.TAG === "Ok") {
341
+ globalThis.expect(true).toBe(false);
342
+ } else {
343
+ let match = result._0;
344
+ globalThis.expect(match.errorCode).toEqual("Forbidden");
345
+ let row = CatalogFeedCallback.auditLog[match.requestId];
346
+ globalThis.expect(Stdlib_Option.map(row, r => r.status)).toEqual("Failure");
347
+ }
348
+ globalThis.expect(published.contents.length).toBe(0);
349
+ });
350
+ globalThis.test("a caller holding every command's role publishes them all", async () => {
351
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), caller([
352
+ "Merchandiser",
353
+ "Admin"
354
+ ]));
355
+ globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
356
+ globalThis.expect(published.contents.length).toBe(2);
357
+ });
358
+ globalThis.test("the role one command needs is enough when the input yields only it", async () => {
359
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("product"), caller(["Merchandiser"]));
360
+ globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
361
+ globalThis.expect(published.contents.length).toBe(1);
362
+ });
363
+ globalThis.test("no caller is the platform itself, and publishes", async () => {
364
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), undefined);
365
+ globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
366
+ globalThis.expect(published.contents.length).toBe(2);
367
+ });
368
+ globalThis.test("a refusal answers CommandRejected with Forbidden", async () => {
369
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Identity$Reventless.anonymous);
370
+ let match = InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result);
371
+ switch (match.TAG) {
372
+ case "Rejected" :
373
+ globalThis.expect(match.errorCode).toBe("Forbidden");
374
+ return;
375
+ case "Accepted" :
376
+ case "Pending" :
377
+ globalThis.expect(true).toBe(false);
378
+ return;
379
+ }
380
+ });
381
+ });
382
+
383
+ let translate$2 = InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.translate;
384
+
385
+ let moduleUrl$2 = InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.moduleUrl;
386
+
387
+ let NoteFeedTranslation = {
388
+ translate: translate$2,
389
+ moduleUrl: moduleUrl$2
390
+ };
391
+
392
+ let NoteFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
393
+ name: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.name,
394
+ moduleUrl: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.moduleUrl,
395
+ externalInputSchema: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.externalInputSchema,
396
+ commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.commandSchema,
397
+ targetName: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.targetName,
398
+ externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.externalSystem,
399
+ authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.authorizationOf
400
+ })(NoteFeedTranslation);
401
+
402
+ globalThis.describe("receive encodes commands with their schema", () => {
403
+ globalThis.test("an absent option the schema writes as null decodes at the target", async () => {
404
+ let published = {
405
+ contents: []
406
+ };
407
+ let publish = async cmds => {
408
+ published.contents = cmds;
409
+ };
410
+ await NoteFeedCallback.receive(publish, Object.fromEntries([[
411
+ "orderId",
412
+ "ord-1"
413
+ ]]), undefined);
414
+ let commandJson = published.contents[0].commandJson;
415
+ let decoded = Sury.parseOrThrow(commandJson, InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.commandSchema);
416
+ globalThis.expect(decoded).toEqual({
417
+ TAG: "Annotate",
418
+ orderId: "ord-1",
419
+ note: undefined
420
+ });
421
+ globalThis.expect(JSON.stringify(commandJson).includes(`"note":null`)).toBe(true);
422
+ });
423
+ });
424
+
290
425
  export {
291
426
  PaymentWebhookTranslation,
292
427
  Callback,
428
+ CatalogFeedTranslation,
429
+ CatalogFeedCallback,
430
+ caller,
431
+ catalogInput,
432
+ NoteFeedTranslation,
433
+ NoteFeedCallback,
293
434
  }
294
435
  /* Callback Not a pure module */
@@ -36,3 +36,61 @@ module PaymentWebhookSpec = {
36
36
  | status => Error("Unknown payment status: " ++ status)
37
37
  }
38
38
  }
39
+
40
+ // Two constructors under different roles; `kind` picks which the input becomes.
41
+ module CatalogFeedSpec = {
42
+ let name = "CatalogFeed"
43
+ let moduleUrl: string = %raw(`import.meta.url`)
44
+
45
+ @schema
46
+ type externalInput = {kind: string, id: string}
47
+
48
+ @schema
49
+ type command =
50
+ | AddProduct({productId: @s.matches(Reventless.DcbTag.string) string})
51
+ | AddCategory({categoryId: @s.matches(Reventless.DcbTag.string) string})
52
+
53
+ let targetName = "AddProduct"
54
+ let externalSystem = None
55
+
56
+ type role = Reventless.Role.name
57
+ let merchandiser = Reventless.Role.make("Merchandiser")
58
+ let admin = Reventless.Role.make("Admin")
59
+ let authorizationOf = (name: string): Reventless.Authorization.rule<role> =>
60
+ switch name {
61
+ | "AddProduct" => AllowRoles([merchandiser])
62
+ | "AddCategory" => AllowRoles([admin])
63
+ | _ => DenyAll
64
+ }
65
+
66
+ let translate = (input: externalInput) =>
67
+ switch input.kind {
68
+ | "product" => Ok([(input.id, AddProduct({productId: input.id}))])
69
+ | "both" =>
70
+ Ok([
71
+ (input.id, AddProduct({productId: input.id})),
72
+ (input.id, AddCategory({categoryId: input.id})),
73
+ ])
74
+ | kind => Error("Unknown kind: " ++ kind)
75
+ }
76
+ }
77
+
78
+ // A command whose schema writes an absent note as `null`, which the runtime value does not.
79
+ module NoteFeedSpec = {
80
+ let name = "NoteFeed"
81
+ let moduleUrl: string = %raw(`import.meta.url`)
82
+
83
+ @schema
84
+ type externalInput = {orderId: string}
85
+
86
+ @schema
87
+ type command =
88
+ Annotate({orderId: @s.matches(Reventless.DcbTag.string) string, note: @s.null option<string>})
89
+
90
+ let targetName = "Annotate"
91
+ let externalSystem = None
92
+
93
+ let translate = (input: externalInput) => Ok([
94
+ (input.orderId, Annotate({orderId: input.orderId, note: None})),
95
+ ])
96
+ }
@@ -52,14 +52,10 @@ function translate(input) {
52
52
  }
53
53
  }
54
54
 
55
- function commandAuthorization(param) {
55
+ function authorizationOf(param) {
56
56
  return "AllowAuthenticated";
57
57
  }
58
58
 
59
- function commandTransition(param) {
60
- return "Undeclared";
61
- }
62
-
63
59
  let traits = [];
64
60
 
65
61
  let PaymentWebhookSpec = {
@@ -70,13 +66,155 @@ let PaymentWebhookSpec = {
70
66
  targetName: "ConfirmPayment",
71
67
  externalSystem: undefined,
72
68
  translate: translate,
73
- commandAuthorization: commandAuthorization,
74
- commandTransition: commandTransition,
69
+ authorizationOf: authorizationOf,
75
70
  traits: traits
76
71
  };
77
72
 
73
+ let moduleUrl$2 = import.meta.url;
74
+
75
+ let externalInputSchema$1 = Sury.$schema(s => ({
76
+ kind: s.m(Sury.string),
77
+ id: s.m(Sury.string)
78
+ }));
79
+
80
+ let commandSchema$1 = Sury.union([
81
+ Sury.$schema(s => ({
82
+ TAG: "AddProduct",
83
+ productId: s.m(DcbTag$Reventless.string)
84
+ })),
85
+ Sury.$schema(s => ({
86
+ TAG: "AddCategory",
87
+ categoryId: s.m(DcbTag$Reventless.string)
88
+ }))
89
+ ]);
90
+
91
+ let merchandiser = "Merchandiser";
92
+
93
+ let admin = "Admin";
94
+
95
+ function authorizationOf$1(name) {
96
+ switch (name) {
97
+ case "AddCategory" :
98
+ return {
99
+ TAG: "AllowRoles",
100
+ _0: [admin]
101
+ };
102
+ case "AddProduct" :
103
+ return {
104
+ TAG: "AllowRoles",
105
+ _0: [merchandiser]
106
+ };
107
+ default:
108
+ return "DenyAll";
109
+ }
110
+ }
111
+
112
+ function translate$1(input) {
113
+ let kind = input.kind;
114
+ switch (kind) {
115
+ case "both" :
116
+ return {
117
+ TAG: "Ok",
118
+ _0: [
119
+ [
120
+ input.id,
121
+ {
122
+ TAG: "AddProduct",
123
+ productId: input.id
124
+ }
125
+ ],
126
+ [
127
+ input.id,
128
+ {
129
+ TAG: "AddCategory",
130
+ categoryId: input.id
131
+ }
132
+ ]
133
+ ]
134
+ };
135
+ case "product" :
136
+ return {
137
+ TAG: "Ok",
138
+ _0: [[
139
+ input.id,
140
+ {
141
+ TAG: "AddProduct",
142
+ productId: input.id
143
+ }
144
+ ]]
145
+ };
146
+ default:
147
+ return {
148
+ TAG: "Error",
149
+ _0: "Unknown kind: " + kind
150
+ };
151
+ }
152
+ }
153
+
154
+ let traits$1 = [];
155
+
156
+ let CatalogFeedSpec = {
157
+ name: "CatalogFeed",
158
+ moduleUrl: moduleUrl$2,
159
+ externalInputSchema: externalInputSchema$1,
160
+ commandSchema: commandSchema$1,
161
+ targetName: "AddProduct",
162
+ externalSystem: undefined,
163
+ merchandiser: merchandiser,
164
+ admin: admin,
165
+ authorizationOf: authorizationOf$1,
166
+ translate: translate$1,
167
+ traits: traits$1
168
+ };
169
+
170
+ let moduleUrl$3 = import.meta.url;
171
+
172
+ let externalInputSchema$2 = Sury.$schema(s => ({
173
+ orderId: s.m(Sury.string)
174
+ }));
175
+
176
+ let commandSchema$2 = Sury.$schema(s => ({
177
+ TAG: "Annotate",
178
+ orderId: s.m(DcbTag$Reventless.string),
179
+ note: s.m(Sury.$nullAsOption(Sury.string))
180
+ }));
181
+
182
+ function translate$2(input) {
183
+ return {
184
+ TAG: "Ok",
185
+ _0: [[
186
+ input.orderId,
187
+ {
188
+ TAG: "Annotate",
189
+ orderId: input.orderId,
190
+ note: undefined
191
+ }
192
+ ]]
193
+ };
194
+ }
195
+
196
+ function authorizationOf$2(param) {
197
+ return "AllowAuthenticated";
198
+ }
199
+
200
+ let traits$2 = [];
201
+
202
+ let NoteFeedSpec = {
203
+ name: "NoteFeed",
204
+ moduleUrl: moduleUrl$3,
205
+ externalInputSchema: externalInputSchema$2,
206
+ commandSchema: commandSchema$2,
207
+ targetName: "Annotate",
208
+ externalSystem: undefined,
209
+ translate: translate$2,
210
+ authorizationOf: authorizationOf$2,
211
+ traits: traits$2
212
+ };
213
+
78
214
  export {
79
215
  OrderEventLog,
80
216
  PaymentWebhookSpec,
217
+ CatalogFeedSpec,
218
+ NoteFeedSpec,
81
219
  }
82
220
  /* moduleUrl Not a pure module */
@@ -37,7 +37,7 @@ let commandSchema = Sury.$schema(s => ({
37
37
 
38
38
  let errorSchema = Sury.literal("AlreadyPlaced");
39
39
 
40
- function commandAuthorization(param) {
40
+ function authorizationOf(param) {
41
41
  return "AllowAuthenticated";
42
42
  }
43
43
 
@@ -55,7 +55,7 @@ let PlaceSpec = {
55
55
  consumedEventSchema: consumedEventSchema,
56
56
  errorSchema: errorSchema,
57
57
  commandSchema: commandSchema,
58
- commandAuthorization: commandAuthorization,
58
+ authorizationOf: authorizationOf,
59
59
  commandTransition: commandTransition,
60
60
  traits: traits,
61
61
  readConsistency: "EscalateOnRetry"
@@ -222,7 +222,7 @@ let PlaceMaker = StateChangeSlice_Builder$ReventlessLocal.Make({
222
222
  errorSchema: errorSchema,
223
223
  eventSchema: eventSchema,
224
224
  commandSchema: commandSchema,
225
- commandAuthorization: commandAuthorization,
225
+ authorizationOf: authorizationOf,
226
226
  commandTransition: commandTransition,
227
227
  traits: traits,
228
228
  readConsistency: "EscalateOnRetry"