@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/chunks/kit-errors.d.ts +7 -1
- package/dist/chunks/kit-errors.js +6 -2
- package/dist/chunks/kit-errors.js.map +1 -1
- package/dist/index.d.ts +91 -14
- package/dist/index.js +32 -25
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +10 -1
- package/dist/testing.js +27 -18
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|