@reventlessdev/reventless-core 3.0.0-alpha.261 → 3.0.0-alpha.263

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 (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +9 -9
  3. package/src/adapter/Secrets/Secrets_Node_Backend.res +60 -0
  4. package/src/adapter/Secrets/Secrets_Node_Backend.res.mjs +69 -0
  5. package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
  6. package/src/components/Aggregate/Aggregate_Builder.res.mjs +1 -1
  7. package/src/components/CommandGenerator/CommandGenerator.res +4 -4
  8. package/src/components/CommandGenerator/CommandGenerator_Callback.res +13 -20
  9. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +3 -24
  10. package/src/components/Dcb/Dcb_Builder.res +45 -49
  11. package/src/components/Dcb/Dcb_Builder.res.mjs +31 -40
  12. package/src/components/StateChangeSlice/StateChangeSlice.res +2 -0
  13. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +6 -0
  14. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs +4 -4
  15. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +51 -22
  16. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +47 -25
  17. package/src/plugin/component/Plugin_Structure.res +101 -4
  18. package/src/plugin/component/Plugin_Structure.res.mjs +82 -9
  19. package/tests/Secrets_Node_BackendTest.res +141 -0
  20. package/tests/Secrets_Node_BackendTest.res.mjs +152 -0
  21. package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +1 -1
  22. package/tests/admin/Platform_Admin_StructureTest.res +12 -3
  23. package/tests/admin/Platform_Admin_StructureTest.res.mjs +4 -3
  24. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +1 -1
  25. package/tests/commandgenerator/CommandGeneratorCallbackTest.res.mjs +1 -1
  26. package/tests/commandgenerator/CommandGeneratorFixtures.res +2 -0
  27. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +17 -3
  28. package/tests/commandgenerator/OwnerStampingTest.res.mjs +2 -2
  29. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +1 -1
  30. package/tests/dcb/DcbFixtures.res +18 -1
  31. package/tests/dcb/DcbFixtures.res.mjs +14 -0
  32. package/tests/dcb/DcbScopeInferenceTest.res +234 -0
  33. package/tests/dcb/DcbScopeInferenceTest.res.mjs +425 -13
  34. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +22 -22
  35. package/tests/dcb/DcbTagTest.res +81 -68
  36. package/tests/dcb/DcbTagTest.res.mjs +88 -89
  37. package/tests/plugin/PluginStructureAccessTest.res +1 -0
  38. package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
  39. package/tests/plugin/PluginStructureTest.res +47 -0
  40. package/tests/plugin/PluginStructureTest.res.mjs +50 -10
@@ -201,7 +201,7 @@ globalThis.beforeEach(() => {
201
201
  globalThis.describe("StateChangeSlice_Callback:", () => {
202
202
  globalThis.describe("handleCommands - happy path", () => {
203
203
  globalThis.test("CreateItem on empty log succeeds", async () => {
204
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
204
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
205
205
  TAG: "CreateItem",
206
206
  itemId: "item-1",
207
207
  name: "Test"
@@ -220,7 +220,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
220
220
  ]);
221
221
  });
222
222
  globalThis.test("stored event carries only its entity tags (no framework provenance tag)", async () => {
223
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
223
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
224
224
  TAG: "CreateItem",
225
225
  itemId: "item-1",
226
226
  name: "Test"
@@ -234,7 +234,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
234
234
  });
235
235
  globalThis.describe("handleCommands - decide returns Ok([])", () => {
236
236
  globalThis.test("NoOp returns Ok without storing events", async () => {
237
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-noop", "NoOp")])));
237
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-noop", "NoOp")])));
238
238
  globalThis.expect([
239
239
  results,
240
240
  mock.getEvents().length
@@ -249,12 +249,12 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
249
249
  });
250
250
  globalThis.describe("handleCommands - decide returns Error", () => {
251
251
  globalThis.test("CreateItem when item exists returns Ok (business-rule violations ACK, not NACK)", async () => {
252
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
252
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
253
253
  TAG: "CreateItem",
254
254
  itemId: "item-1",
255
255
  name: "Test"
256
256
  })])));
257
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-2", {
257
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-2", {
258
258
  TAG: "CreateItem",
259
259
  itemId: "item-1",
260
260
  name: "Duplicate"
@@ -268,7 +268,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
268
268
  globalThis.describe("handleCommands - retry on conflict", () => {
269
269
  globalThis.test("retries and succeeds after 1 append failure", async () => {
270
270
  mock.failNextAppends.contents = 1;
271
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
271
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
272
272
  TAG: "CreateItem",
273
273
  itemId: "item-1",
274
274
  name: "Test"
@@ -286,7 +286,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
286
286
  });
287
287
  globalThis.test("returns Error after retries exhausted (4 failures)", async () => {
288
288
  mock.failNextAppends.contents = 4;
289
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
289
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
290
290
  TAG: "CreateItem",
291
291
  itemId: "item-1",
292
292
  name: "Test"
@@ -305,7 +305,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
305
305
  });
306
306
  globalThis.describe("handleCommands - read consistency (eventual-first, strong-on-retry)", () => {
307
307
  globalThis.test("a conflict-free command reads eventually-consistent (no strong read)", async () => {
308
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
308
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
309
309
  TAG: "CreateItem",
310
310
  itemId: "item-1",
311
311
  name: "Test"
@@ -314,7 +314,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
314
314
  });
315
315
  globalThis.test("escalates to strong consistency on retry after a conflict", async () => {
316
316
  mock.failNextAppends.contents = 1;
317
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
317
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
318
318
  TAG: "CreateItem",
319
319
  itemId: "item-1",
320
320
  name: "Test"
@@ -326,7 +326,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
326
326
  });
327
327
  globalThis.test("every retry after the first is strong (2 failures)", async () => {
328
328
  mock.failNextAppends.contents = 2;
329
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
329
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
330
330
  TAG: "CreateItem",
331
331
  itemId: "item-1",
332
332
  name: "Test"
@@ -340,7 +340,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
340
340
  });
341
341
  globalThis.describe("handleCommands - read consistency (per-slice override)", () => {
342
342
  globalThis.test("AlwaysStrong reads strong on the first attempt", async () => {
343
- await Effect.runPromise(StrongHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
343
+ await Effect.runPromise(StrongHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
344
344
  TAG: "CreateItem",
345
345
  itemId: "item-1",
346
346
  name: "Test"
@@ -349,7 +349,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
349
349
  });
350
350
  globalThis.test("AlwaysStrong stays strong across retries (2 failures)", async () => {
351
351
  mock.failNextAppends.contents = 2;
352
- await Effect.runPromise(StrongHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
352
+ await Effect.runPromise(StrongHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
353
353
  TAG: "CreateItem",
354
354
  itemId: "item-1",
355
355
  name: "Test"
@@ -362,7 +362,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
362
362
  });
363
363
  globalThis.test("AlwaysEventual never reads strong, even on retry (2 failures)", async () => {
364
364
  mock.failNextAppends.contents = 2;
365
- await Effect.runPromise(EventualHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
365
+ await Effect.runPromise(EventualHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
366
366
  TAG: "CreateItem",
367
367
  itemId: "item-1",
368
368
  name: "Test"
@@ -376,12 +376,12 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
376
376
  });
377
377
  globalThis.describe("handleCommands - conditional append", () => {
378
378
  globalThis.test("RenameItem after CreateItem uses headPosition in condition", async () => {
379
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
379
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-1", {
380
380
  TAG: "CreateItem",
381
381
  itemId: "item-1",
382
382
  name: "Test"
383
383
  })])));
384
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-2", {
384
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-2", {
385
385
  TAG: "RenameItem",
386
386
  itemId: "item-1",
387
387
  newName: "Updated"
@@ -400,7 +400,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
400
400
  });
401
401
  globalThis.describe("handleCommands - batch", () => {
402
402
  globalThis.test("multiple successful commands", async () => {
403
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([
403
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([
404
404
  makeTopicItem("ref-1", {
405
405
  TAG: "CreateItem",
406
406
  itemId: "item-1",
@@ -424,12 +424,12 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
424
424
  ]);
425
425
  });
426
426
  globalThis.test("mixed: new item succeeds, duplicate item ACKs as Ok (business-rule violation)", async () => {
427
- await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-0", {
427
+ await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem("ref-0", {
428
428
  TAG: "CreateItem",
429
429
  itemId: "item-1",
430
430
  name: "Existing"
431
431
  })])));
432
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([
432
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([
433
433
  makeTopicItem("ref-1", {
434
434
  TAG: "CreateItem",
435
435
  itemId: "item-2",
@@ -453,13 +453,13 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
453
453
  ]);
454
454
  });
455
455
  globalThis.test("empty batch returns empty array", async () => {
456
- let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([])));
456
+ let results = await Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([])));
457
457
  globalThis.expect(results).toEqual([]);
458
458
  });
459
459
  });
460
460
  globalThis.describe("handleCommands - projection cache", () => {
461
461
  globalThis.test("a third same-entity command reads only the delta", async () => {
462
- let run = (reference, command) => Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem(reference, command)])));
462
+ let run = (reference, command) => Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem(reference, command)])));
463
463
  await run("ref-1", {
464
464
  TAG: "CreateItem",
465
465
  itemId: "item-1",
@@ -493,7 +493,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
493
493
  ]);
494
494
  });
495
495
  globalThis.test("a different entity does not hit another entity's cache (full read)", async () => {
496
- let run = (reference, command) => Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem(reference, command)])));
496
+ let run = (reference, command) => Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem(reference, command)])));
497
497
  await run("ref-1", {
498
498
  TAG: "CreateItem",
499
499
  itemId: "item-1",
@@ -510,7 +510,7 @@ globalThis.describe("StateChangeSlice_Callback:", () => {
510
510
  ]);
511
511
  });
512
512
  globalThis.test("conflict re-seeds the cache so the retry reads the delta", async () => {
513
- let run = (reference, command) => Effect.runPromise(TestHandler.handleCommands(undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem(reference, command)])));
513
+ let run = (reference, command) => Effect.runPromise(TestHandler.handleCommands(undefined, undefined, undefined, testDcbEventLog, Stream.fromIterable([makeTopicItem(reference, command)])));
514
514
  await run("ref-1", {
515
515
  TAG: "CreateItem",
516
516
  itemId: "item-1",
@@ -534,107 +534,120 @@ describe("DcbTag:", () => {
534
534
  )
535
535
  })
536
536
 
537
- describe("derivePartitionTag", () => {
538
- let u = S.castToUnknown
537
+ describe("deriveBoundaryPartition", () => {
538
+ let slice = (name, eventSchema): Reventless.DcbTag.sliceSchemas => {
539
+ name,
540
+ commandSchema: S.unknown,
541
+ consumedEventSchema: S.unknown,
542
+ eventSchema: eventSchema->S.castToUnknown,
543
+ }
544
+ let thrownMessage = f =>
545
+ try {
546
+ let _ = f()
547
+ None
548
+ } catch {
549
+ | JsExn(err) => JsExn.message(err)
550
+ }
539
551
 
540
552
  testSync(
541
553
  "returns Composite for schema with >= 2 composite fields",
542
- () => {
543
- let result = Reventless.DcbTag.derivePartitionTag([
544
- ("Sync", "test.res", DcbFixtures.compositeEventSchema->u),
545
- ])
546
- switch result {
554
+ () =>
555
+ switch Reventless.DcbTag.deriveBoundaryPartition([
556
+ slice("Sync", DcbFixtures.compositeEventSchema),
557
+ ]).partitionTag {
547
558
  | Composite(spec) =>
548
- expect(spec.keys)->toEqual(["environment", "platformName", "pluginName"])->ignore
559
+ expect(spec.keys)->toEqual(["environment", "platformName", "pluginName"])
549
560
  expect(spec.seps)->toEqual(["/", "/"])
550
- | Simple(_) => fail("Expected Composite")
551
- }
552
- },
561
+ | _ => fail("Expected Composite")
562
+ },
553
563
  )
554
564
 
555
565
  testSync(
556
566
  "returns Composite with custom separators",
557
- () => {
558
- let result = Reventless.DcbTag.derivePartitionTag([
559
- ("Config", "test.res", DcbFixtures.compositeEventCustomSepSchema->u),
560
- ])
561
- switch result {
567
+ () =>
568
+ switch Reventless.DcbTag.deriveBoundaryPartition([
569
+ slice("Config", DcbFixtures.compositeEventCustomSepSchema),
570
+ ]).partitionTag {
562
571
  | Composite(spec) =>
563
- expect(spec.keys)->toEqual(["tenantId", "region", "service"])->ignore
572
+ expect(spec.keys)->toEqual(["tenantId", "region", "service"])
564
573
  expect(spec.seps)->toEqual([":", "/"])
565
- | Simple(_) => fail("Expected Composite")
566
- }
567
- },
574
+ | _ => fail("Expected Composite")
575
+ },
568
576
  )
569
577
 
570
578
  testSync(
571
- "returns Simple for schema with only @partitionTag",
579
+ "files each event type under its own slice's key — two entities, two annotated keys",
572
580
  () => {
573
- let result = Reventless.DcbTag.derivePartitionTag([
574
- ("Order", "test.res", DcbFixtures.simplePartitionEventSchema->u),
581
+ let bp = Reventless.DcbTag.deriveBoundaryPartition([
582
+ slice("PlaceOrder", DcbFixtures.simplePartitionEventSchema),
583
+ slice("LinkAddress", DcbFixtures.addressLinkedEventSchema),
575
584
  ])
576
- switch result {
577
- | Simple(pt) => expect(pt.key)->toBe("orderId")
578
- | Composite(_) => fail("Expected Simple")
585
+ switch bp.partitionTag {
586
+ | ByEventType(keys) =>
587
+ expect(keys->Dict.get("OrderPlaced"))->toEqual(Some("orderId"))
588
+ expect(keys->Dict.get("AddressLinked"))->toEqual(Some("customerId"))
589
+ | _ => fail("Expected ByEventType")
579
590
  }
591
+ expect(bp->Reventless.DcbTag.slicePartitionTag("LinkAddress"))->toEqual(
592
+ Some(Reventless.DcbTag.Simple({key: "customerId"})),
593
+ )
580
594
  },
581
595
  )
582
596
 
583
597
  testSync(
584
- "returns Simple for schema with single tagged field",
598
+ "a single tagged field is the partition",
599
+ () =>
600
+ expect(
601
+ Reventless.DcbTag.deriveBoundaryPartition([
602
+ slice("Item", DcbFixtures.singleTagCommandSchema),
603
+ ]).partitionBySlice->Dict.get("Item"),
604
+ )->toEqual(Some("itemId")),
605
+ )
606
+
607
+ testSync(
608
+ "an unannotated multi-id slice throws naming it, though a sibling is annotated",
585
609
  () => {
586
- let result = Reventless.DcbTag.derivePartitionTag([
587
- ("Item", "test.res", DcbFixtures.singleTagCommandSchema->u),
588
- ])
589
- switch result {
590
- | Simple(pt) => expect(pt.key)->toBe("itemId")
591
- | Composite(_) => fail("Expected Simple")
592
- }
610
+ let message =
611
+ thrownMessage(
612
+ () =>
613
+ Reventless.DcbTag.deriveBoundaryPartition([
614
+ slice("PlaceOrder", DcbFixtures.simplePartitionEventSchema),
615
+ slice("LinkCustomerAddress", DcbFixtures.unannotatedLinkEventSchema),
616
+ ]),
617
+ )->Option.getOr("")
618
+ expect(message->String.includes("LinkCustomerAddress"))->toBe(true)
619
+ expect(message->String.includes("@partitionTag"))->toBe(true)
593
620
  },
594
621
  )
595
622
 
596
623
  testSync(
597
624
  "throws on mixed composite and simple partition strategy",
598
- () => {
599
- let threw = ref(false)
600
- try {
601
- let _ = Reventless.DcbTag.derivePartitionTag([
602
- ("Mixed", "test.res", DcbFixtures.mixedStrategyEventSchema->u),
603
- ])
604
- } catch {
605
- | JsExn(err) =>
606
- threw := true
607
- expect(
608
- JsExn.message(err)
609
- ->Option.getOr("")
610
- ->String.includes("mixes @compositePartitionTag and @partitionTag"),
625
+ () =>
626
+ expect(
627
+ thrownMessage(
628
+ () =>
629
+ Reventless.DcbTag.deriveBoundaryPartition([
630
+ slice("Mixed", DcbFixtures.mixedStrategyEventSchema),
631
+ ]),
611
632
  )
612
- ->toBe(true)
613
- ->ignore
614
- }
615
- expect(threw.contents)->toBe(true)
616
- },
633
+ ->Option.getOr("")
634
+ ->String.includes("mixes @compositePartitionTag and @partitionTag"),
635
+ )->toBe(true),
617
636
  )
618
637
 
619
638
  testSync(
620
639
  "throws on single composite field (< 2)",
621
- () => {
622
- let threw = ref(false)
623
- try {
624
- let _ = Reventless.DcbTag.derivePartitionTag([
625
- ("Single", "test.res", DcbFixtures.singleCompositeEventSchema->u),
626
- ])
627
- } catch {
628
- | JsExn(err) =>
629
- threw := true
630
- expect(
631
- JsExn.message(err)->Option.getOr("")->String.includes("at least 2 annotated fields"),
640
+ () =>
641
+ expect(
642
+ thrownMessage(
643
+ () =>
644
+ Reventless.DcbTag.deriveBoundaryPartition([
645
+ slice("Single", DcbFixtures.singleCompositeEventSchema),
646
+ ]),
632
647
  )
633
- ->toBe(true)
634
- ->ignore
635
- }
636
- expect(threw.contents)->toBe(true)
637
- },
648
+ ->Option.getOr("")
649
+ ->String.includes("at least 2 annotated fields"),
650
+ )->toBe(true),
638
651
  )
639
652
  })
640
653
 
@@ -418,106 +418,105 @@ globalThis.describe("DcbTag:", () => {
418
418
  globalThis.expect(DcbTag$Reventless.getCompositePartitionKeyValue(tags, spec)).toBe("1/2/3");
419
419
  });
420
420
  });
421
- globalThis.describe("derivePartitionTag", () => {
421
+ globalThis.describe("deriveBoundaryPartition", () => {
422
+ let slice = (name, eventSchema) => ({
423
+ name: name,
424
+ commandSchema: Sury.unknown,
425
+ consumedEventSchema: Sury.unknown,
426
+ eventSchema: eventSchema
427
+ });
428
+ let thrownMessage = f => {
429
+ try {
430
+ f();
431
+ return;
432
+ } catch (raw_err) {
433
+ let err = Primitive_exceptions.internalToException(raw_err);
434
+ if (err.RE_EXN_ID === "JsExn") {
435
+ return Stdlib_JsExn.message(err._1);
436
+ }
437
+ throw err;
438
+ }
439
+ };
422
440
  globalThis.test("returns Composite for schema with >= 2 composite fields", () => {
423
- let result = DcbTag$Reventless.derivePartitionTag([[
424
- "Sync",
425
- "test.res",
426
- DcbFixtures$ReventlessCore.compositeEventSchema
427
- ]]);
428
- if (result.TAG === "Simple") {
429
- return JestGlobals.fail("Expected Composite");
441
+ let spec = DcbTag$Reventless.deriveBoundaryPartition([slice("Sync", DcbFixtures$ReventlessCore.compositeEventSchema)]).partitionTag;
442
+ switch (spec.TAG) {
443
+ case "Composite" :
444
+ let spec$1 = spec._0;
445
+ globalThis.expect(spec$1.keys).toEqual([
446
+ "environment",
447
+ "platformName",
448
+ "pluginName"
449
+ ]);
450
+ globalThis.expect(spec$1.seps).toEqual([
451
+ "/",
452
+ "/"
453
+ ]);
454
+ return;
455
+ case "Simple" :
456
+ case "ByEventType" :
457
+ return JestGlobals.fail("Expected Composite");
430
458
  }
431
- let spec = result._0;
432
- globalThis.expect(spec.keys).toEqual([
433
- "environment",
434
- "platformName",
435
- "pluginName"
436
- ]);
437
- globalThis.expect(spec.seps).toEqual([
438
- "/",
439
- "/"
440
- ]);
441
459
  });
442
460
  globalThis.test("returns Composite with custom separators", () => {
443
- let result = DcbTag$Reventless.derivePartitionTag([[
444
- "Config",
445
- "test.res",
446
- DcbFixtures$ReventlessCore.compositeEventCustomSepSchema
447
- ]]);
448
- if (result.TAG === "Simple") {
449
- return JestGlobals.fail("Expected Composite");
461
+ let spec = DcbTag$Reventless.deriveBoundaryPartition([slice("Config", DcbFixtures$ReventlessCore.compositeEventCustomSepSchema)]).partitionTag;
462
+ switch (spec.TAG) {
463
+ case "Composite" :
464
+ let spec$1 = spec._0;
465
+ globalThis.expect(spec$1.keys).toEqual([
466
+ "tenantId",
467
+ "region",
468
+ "service"
469
+ ]);
470
+ globalThis.expect(spec$1.seps).toEqual([
471
+ ":",
472
+ "/"
473
+ ]);
474
+ return;
475
+ case "Simple" :
476
+ case "ByEventType" :
477
+ return JestGlobals.fail("Expected Composite");
450
478
  }
451
- let spec = result._0;
452
- globalThis.expect(spec.keys).toEqual([
453
- "tenantId",
454
- "region",
455
- "service"
456
- ]);
457
- globalThis.expect(spec.seps).toEqual([
458
- ":",
459
- "/"
460
- ]);
461
479
  });
462
- globalThis.test("returns Simple for schema with only @partitionTag", () => {
463
- let result = DcbTag$Reventless.derivePartitionTag([[
464
- "Order",
465
- "test.res",
466
- DcbFixtures$ReventlessCore.simplePartitionEventSchema
467
- ]]);
468
- if (result.TAG !== "Simple") {
469
- return JestGlobals.fail("Expected Simple");
470
- }
471
- globalThis.expect(result._0.key).toBe("orderId");
472
- });
473
- globalThis.test("returns Simple for schema with single tagged field", () => {
474
- let result = DcbTag$Reventless.derivePartitionTag([[
475
- "Item",
476
- "test.res",
477
- DcbFixtures$ReventlessCore.singleTagCommandSchema
478
- ]]);
479
- if (result.TAG !== "Simple") {
480
- return JestGlobals.fail("Expected Simple");
480
+ globalThis.test("files each event type under its own slice's key — two entities, two annotated keys", () => {
481
+ let bp = DcbTag$Reventless.deriveBoundaryPartition([
482
+ slice("PlaceOrder", DcbFixtures$ReventlessCore.simplePartitionEventSchema),
483
+ slice("LinkAddress", DcbFixtures$ReventlessCore.addressLinkedEventSchema)
484
+ ]);
485
+ let keys = bp.partitionTag;
486
+ switch (keys.TAG) {
487
+ case "Simple" :
488
+ case "Composite" :
489
+ JestGlobals.fail("Expected ByEventType");
490
+ break;
491
+ case "ByEventType" :
492
+ let keys$1 = keys._0;
493
+ globalThis.expect(keys$1["OrderPlaced"]).toEqual("orderId");
494
+ globalThis.expect(keys$1["AddressLinked"]).toEqual("customerId");
495
+ break;
481
496
  }
482
- globalThis.expect(result._0.key).toBe("itemId");
497
+ globalThis.expect(DcbTag$Reventless.slicePartitionTag(bp, "LinkAddress")).toEqual({
498
+ TAG: "Simple",
499
+ _0: {
500
+ key: "customerId"
501
+ }
502
+ });
503
+ });
504
+ globalThis.test("a single tagged field is the partition", () => {
505
+ globalThis.expect(DcbTag$Reventless.deriveBoundaryPartition([slice("Item", DcbFixtures$ReventlessCore.singleTagCommandSchema)]).partitionBySlice["Item"]).toEqual("itemId");
506
+ });
507
+ globalThis.test("an unannotated multi-id slice throws naming it, though a sibling is annotated", () => {
508
+ let message = Stdlib_Option.getOr(thrownMessage(() => DcbTag$Reventless.deriveBoundaryPartition([
509
+ slice("PlaceOrder", DcbFixtures$ReventlessCore.simplePartitionEventSchema),
510
+ slice("LinkCustomerAddress", DcbFixtures$ReventlessCore.unannotatedLinkEventSchema)
511
+ ])), "");
512
+ globalThis.expect(message.includes("LinkCustomerAddress")).toBe(true);
513
+ globalThis.expect(message.includes("@partitionTag")).toBe(true);
483
514
  });
484
515
  globalThis.test("throws on mixed composite and simple partition strategy", () => {
485
- let threw = false;
486
- try {
487
- DcbTag$Reventless.derivePartitionTag([[
488
- "Mixed",
489
- "test.res",
490
- DcbFixtures$ReventlessCore.mixedStrategyEventSchema
491
- ]]);
492
- } catch (raw_err) {
493
- let err = Primitive_exceptions.internalToException(raw_err);
494
- if (err.RE_EXN_ID === "JsExn") {
495
- threw = true;
496
- globalThis.expect(Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "").includes("mixes @compositePartitionTag and @partitionTag")).toBe(true);
497
- } else {
498
- throw err;
499
- }
500
- }
501
- globalThis.expect(threw).toBe(true);
516
+ globalThis.expect(Stdlib_Option.getOr(thrownMessage(() => DcbTag$Reventless.deriveBoundaryPartition([slice("Mixed", DcbFixtures$ReventlessCore.mixedStrategyEventSchema)])), "").includes("mixes @compositePartitionTag and @partitionTag")).toBe(true);
502
517
  });
503
518
  globalThis.test("throws on single composite field (< 2)", () => {
504
- let threw = false;
505
- try {
506
- DcbTag$Reventless.derivePartitionTag([[
507
- "Single",
508
- "test.res",
509
- DcbFixtures$ReventlessCore.singleCompositeEventSchema
510
- ]]);
511
- } catch (raw_err) {
512
- let err = Primitive_exceptions.internalToException(raw_err);
513
- if (err.RE_EXN_ID === "JsExn") {
514
- threw = true;
515
- globalThis.expect(Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "").includes("at least 2 annotated fields")).toBe(true);
516
- } else {
517
- throw err;
518
- }
519
- }
520
- globalThis.expect(threw).toBe(true);
519
+ globalThis.expect(Stdlib_Option.getOr(thrownMessage(() => DcbTag$Reventless.deriveBoundaryPartition([slice("Single", DcbFixtures$ReventlessCore.singleCompositeEventSchema)])), "").includes("at least 2 annotated fields")).toBe(true);
521
520
  });
522
521
  });
523
522
  globalThis.describe("isCompositePartitionMember", () => {
@@ -32,6 +32,7 @@ module PsGatedCommandsSlice: ReventlessInfra.StateChangeSlice.T = {
32
32
  ~publishJsons as _,
33
33
  ~tagKeysByEventType as _=?,
34
34
  ~crossPartitionTagKeys as _=?,
35
+ ~partitionTag as _=?,
35
36
  ~runtime as _=?,
36
37
  ~opts as _=?,
37
38
  ): component => Obj.magic(0)
@@ -15,7 +15,7 @@ let Behavior = {
15
15
  moduleUrl: PsGatedCommands$ReventlessCore.moduleUrl
16
16
  };
17
17
 
18
- function make(param, param$1, param$2, param$3, param$4, param$5) {
18
+ function make(param, param$1, param$2, param$3, param$4, param$5, param$6) {
19
19
  return 0;
20
20
  }
21
21