@shirudo/ddd-kit 3.0.0-rc.8 → 3.0.0-rc.9

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/dist/testing.d.ts CHANGED
@@ -502,7 +502,8 @@ declare function createProjectionCheckpointStoreContractTests<TCtx>(harness: Pro
502
502
  interface ContractRepository<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
503
503
  findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
504
504
  add(aggregate: TAggregate): void;
505
- update(aggregate: TAggregate): void;
505
+ /** An append-only port declares no update. */
506
+ update?(aggregate: TAggregate): void;
506
507
  /** Physical removal is an optional persistence capability. */
507
508
  remove?(aggregate: TAggregate): void;
508
509
  }
@@ -541,6 +542,14 @@ interface RepositoryContractHarness<TAggregate extends Aggregate<Id<string>, TEv
541
542
  snapshotState?(aggregate: TAggregate): unknown;
542
543
  /** Opt out only for an intentionally upserting add implementation. */
543
544
  insertsAreDuplicateChecked?: boolean;
545
+ /**
546
+ * Opt out only for an append-only port, one that the definition marks
547
+ * with `appendOnly: true`. The suite then skips every update proof. The
548
+ * duplicate-add proof is the concurrency proof that remains, so it is
549
+ * mandatory. Provide `createAggregateWithId` and keep
550
+ * `insertsAreDuplicateChecked`, or the proof fails instead of skipping.
551
+ */
552
+ updatesAreSupported?: boolean;
544
553
  /** Enables physical-remove behavior and stale-remove OCC tests. */
545
554
  removesAreSupported?: boolean;
546
555
  /** The remove flush predicates on the version captured at load. */
package/dist/testing.js CHANGED
@@ -2596,10 +2596,19 @@ function createRepositoryContractTests(harness) {
2596
2596
  const mutateVersionOnly = harness.mutateVersionOnly;
2597
2597
  const mutateChildCollection = harness.mutateChildCollection;
2598
2598
  const insertsAreDuplicateChecked = harness.insertsAreDuplicateChecked !== false;
2599
+ const updatesAreSupported = harness.updatesAreSupported !== false;
2599
2600
  const removesAreSupported = harness.removesAreSupported === true;
2600
2601
  const removesAreVersionChecked = removesAreSupported && harness.removesAreVersionChecked === true;
2601
2602
  const overlappingCallsBoundMs = harness.overlappingCallsBoundMs ?? 1e3;
2602
2603
  const load = (repository, id) => loadAggregateOrFail(repository, id, "the adapter did not commit or reconstitute the aggregate");
2604
+ const update = (repository, aggregate) => {
2605
+ assert(repository.update !== void 0, "the harness keeps updatesAreSupported, but the repository has no update");
2606
+ repository.update(aggregate);
2607
+ };
2608
+ const updateGate = {
2609
+ capability: "updatesAreSupported",
2610
+ satisfiedBy: updatesAreSupported
2611
+ };
2603
2612
  async function seed(environment) {
2604
2613
  const aggregate = harness.createAggregate();
2605
2614
  harness.mutate(aggregate);
@@ -2650,7 +2659,7 @@ function createRepositoryContractTests(harness) {
2650
2659
  });
2651
2660
  })
2652
2661
  },
2653
- {
2662
+ gatedContractTest(updateGate, {
2654
2663
  name: "MANDATORY stale update: writer B conflicts after writer A commits and persists nothing",
2655
2664
  run: inEnvironment(async (environment) => {
2656
2665
  const seeded = await seed(environment);
@@ -2658,12 +2667,12 @@ function createRepositoryContractTests(harness) {
2658
2667
  const stale = await load(repository, seeded.id);
2659
2668
  await hold();
2660
2669
  harness.mutate(stale);
2661
- repository.update(stale);
2670
+ update(repository, stale);
2662
2671
  }));
2663
2672
  const committedA = await awaitOverlappingCall(() => environment.run(async ({ repository }) => {
2664
2673
  const current = await load(repository, seeded.id);
2665
2674
  harness.mutate(current);
2666
- repository.update(current);
2675
+ update(repository, current);
2667
2676
  return current;
2668
2677
  }), writerB, overlappingCallsBoundMs);
2669
2678
  const outboxAfterA = await environment.committedOutboxEvents();
@@ -2675,7 +2684,7 @@ function createRepositoryContractTests(harness) {
2675
2684
  if (snapshotState) assert(deepEqual(snapshotState.call(harness, final), snapshotState.call(harness, committedA)), "the stale writer must not replace writer A's state");
2676
2685
  assert(deepEqual(eventIds(await environment.committedOutboxEvents()), eventIds(outboxAfterA)), "a rejected stale flush must add no outbox records");
2677
2686
  })
2678
- },
2687
+ }),
2679
2688
  {
2680
2689
  name: "rollback acknowledges nothing and commits neither state nor outbox",
2681
2690
  run: inEnvironment(async (environment) => {
@@ -2720,26 +2729,26 @@ function createRepositoryContractTests(harness) {
2720
2729
  });
2721
2730
  })
2722
2731
  },
2723
- {
2732
+ gatedContractTest(updateGate, {
2724
2733
  name: "an unchanged explicit update is safe and emits no event",
2725
2734
  run: inEnvironment(async (environment) => {
2726
2735
  const seeded = await seed(environment);
2727
2736
  const before = await environment.committedOutboxEvents();
2728
2737
  await environment.run(async ({ repository }) => {
2729
2738
  const aggregate = await load(repository, seeded.id);
2730
- repository.update(aggregate);
2739
+ update(repository, aggregate);
2731
2740
  });
2732
2741
  assert(deepEqual(eventIds(await environment.committedOutboxEvents()), eventIds(before)), "an unchanged update must not manufacture an outbox event");
2733
2742
  })
2734
- }
2743
+ })
2735
2744
  ];
2736
2745
  tests.push(gatedContractTest({
2737
2746
  capability: createAggregateWithId ? "insertsAreDuplicateChecked" : "createAggregateWithId",
2738
- satisfiedBy: Boolean(createAggregateWithId) && insertsAreDuplicateChecked
2747
+ satisfiedBy: Boolean(createAggregateWithId) && insertsAreDuplicateChecked || !updatesAreSupported
2739
2748
  }, {
2740
2749
  name: "duplicate add rejects and preserves the existing aggregate",
2741
2750
  run: inEnvironment(async (environment) => {
2742
- assert(createAggregateWithId !== void 0, "capability gate");
2751
+ assert(createAggregateWithId !== void 0 && insertsAreDuplicateChecked, "an append-only harness must provide createAggregateWithId and keep insertsAreDuplicateChecked: the duplicate-add proof is its only concurrency proof");
2743
2752
  const seeded = await seed(environment);
2744
2753
  const duplicate = createAggregateWithId.call(harness, seeded.id);
2745
2754
  harness.mutate(duplicate);
@@ -2753,7 +2762,7 @@ function createRepositoryContractTests(harness) {
2753
2762
  if (snapshotState) assert(deepEqual(snapshotState.call(harness, final), snapshotState.call(harness, seeded)), "the existing row's state must be untouched by the rejected duplicate add");
2754
2763
  })
2755
2764
  }));
2756
- tests.push(gatedContractTest({
2765
+ tests.push(gatedContractTest(updateGate, gatedContractTest({
2757
2766
  capability: "mutateVersionOnly",
2758
2767
  satisfiedBy: Boolean(mutateVersionOnly)
2759
2768
  }, {
@@ -2765,14 +2774,14 @@ function createRepositoryContractTests(harness) {
2765
2774
  await environment.run(async ({ repository }) => {
2766
2775
  const aggregate = await load(repository, seeded.id);
2767
2776
  mutateVersionOnly.call(harness, aggregate);
2768
- repository.update(aggregate);
2777
+ update(repository, aggregate);
2769
2778
  });
2770
2779
  const reloaded = await reload(environment, seeded.id);
2771
2780
  assertEqual(reloaded.version, seeded.version + 1, "a version-only change (empty change set, bumped version) must still be persisted; skipping it desyncs the persisted version and produces false concurrency conflicts later");
2772
2781
  assert(deepEqual(eventIds(await environment.committedOutboxEvents()), eventIds(outboxBefore)), "state-only update must not create an outbox event");
2773
2782
  })
2774
- }));
2775
- tests.push(gatedContractTest({
2783
+ })));
2784
+ tests.push(gatedContractTest(updateGate, gatedContractTest({
2776
2785
  capability: "mutateChildCollection",
2777
2786
  satisfiedBy: Boolean(mutateChildCollection)
2778
2787
  }, {
@@ -2783,12 +2792,12 @@ function createRepositoryContractTests(harness) {
2783
2792
  await environment.run(async ({ repository }) => {
2784
2793
  const aggregate = await load(repository, seeded.id);
2785
2794
  mutateChildCollection.call(harness, aggregate);
2786
- repository.update(aggregate);
2795
+ update(repository, aggregate);
2787
2796
  });
2788
2797
  const reloaded = await reload(environment, seeded.id);
2789
2798
  assertEqual(reloaded.version, seeded.version + 1, "nested collection update must advance the persisted root version");
2790
2799
  })
2791
- }));
2800
+ })));
2792
2801
  tests.push(gatedContractTest({
2793
2802
  capability: "removesAreSupported",
2794
2803
  satisfiedBy: removesAreSupported
@@ -2805,7 +2814,7 @@ function createRepositoryContractTests(harness) {
2805
2814
  assert(await environment.run(({ repository }) => repository.findById(seeded.id)) === void 0, "remove must physically remove the aggregate after commit");
2806
2815
  })
2807
2816
  }));
2808
- tests.push(gatedContractTest({
2817
+ tests.push(gatedContractTest(updateGate, gatedContractTest({
2809
2818
  capability: "removesAreVersionChecked",
2810
2819
  satisfiedBy: removesAreVersionChecked
2811
2820
  }, {
@@ -2821,14 +2830,14 @@ function createRepositoryContractTests(harness) {
2821
2830
  await awaitOverlappingCall(() => environment.run(async ({ repository }) => {
2822
2831
  const current = await load(repository, seeded.id);
2823
2832
  harness.mutate(current);
2824
- repository.update(current);
2833
+ update(repository, current);
2825
2834
  }), staleRemove, overlappingCallsBoundMs);
2826
2835
  staleRemove.release();
2827
2836
  const rejection = await captureRejection(staleRemove.call);
2828
2837
  assertChainContainsKitError(rejection, ["CONCURRENCY_CONFLICT"], `stale remove must reject with ConcurrencyConflictError; got ${describeError(rejection)}`);
2829
2838
  assert(await reload(environment, seeded.id) !== void 0, "stale remove must not delete the concurrent winner");
2830
2839
  })
2831
- }));
2840
+ })));
2832
2841
  return tests;
2833
2842
  }
2834
2843