@cratis/chronicle 6.49.0 → 6.51.0
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/eventSequences/AppendOptions.d.ts +10 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +6 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +28 -8
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -5
- package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
- package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -3
- package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/TransactionalEventSequence.js +7 -4
- package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendNotification.d.ts +2 -1
- package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
- package/dist/eventSequences/createAppendNotification.js +3 -2
- package/dist/eventSequences/createAppendNotification.js.map +1 -1
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
- package/dist/eventSequences/index.d.ts +1 -0
- package/dist/eventSequences/index.d.ts.map +1 -1
- package/dist/eventSequences/index.js +1 -0
- package/dist/eventSequences/index.js.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.js +4 -1
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.js +3 -1
- package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
- package/dist/events/EventContext.d.ts +7 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/InvalidNamedTag.d.ts +8 -0
- package/dist/events/InvalidNamedTag.d.ts.map +1 -0
- package/dist/events/InvalidNamedTag.js +13 -0
- package/dist/events/InvalidNamedTag.js.map +1 -0
- package/dist/events/NamedTag.d.ts +25 -0
- package/dist/events/NamedTag.d.ts.map +1 -0
- package/dist/events/NamedTag.js +39 -0
- package/dist/events/NamedTag.js.map +1 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts +7 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.js +13 -0
- package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
- package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
- package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
- package/dist/events/constraints/ConstraintEventSequences.js +39 -0
- package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +24 -3
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
- package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueOptions.d.ts +15 -0
- package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
- package/dist/events/constraints/UniqueOptions.js +4 -0
- package/dist/events/constraints/UniqueOptions.js.map +1 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
- package/dist/events/constraints/index.d.ts +2 -0
- package/dist/events/constraints/index.d.ts.map +1 -1
- package/dist/events/constraints/index.js.map +1 -1
- package/dist/events/constraints/unique.d.ts +10 -0
- package/dist/events/constraints/unique.d.ts.map +1 -1
- package/dist/events/constraints/unique.js +11 -10
- package/dist/events/constraints/unique.js.map +1 -1
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeNamedTags.d.ts +13 -0
- package/dist/events/mergeNamedTags.d.ts.map +1 -0
- package/dist/events/mergeNamedTags.js +39 -0
- package/dist/events/mergeNamedTags.js.map +1 -0
- package/dist/events/toClientEventContext.d.ts.map +1 -1
- package/dist/events/toClientEventContext.js +3 -1
- package/dist/events/toClientEventContext.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
- package/dist/projections/eventContextPropertyExpression.js +2 -1
- package/dist/projections/eventContextPropertyExpression.js.map +1 -1
- package/dist/testing/EventScenario.d.ts.map +1 -1
- package/dist/testing/EventScenario.js +8 -3
- package/dist/testing/EventScenario.js.map +1 -1
- package/dist/testing/InProcessConstraints.d.ts +4 -1
- package/dist/testing/InProcessConstraints.d.ts.map +1 -1
- package/dist/testing/InProcessConstraints.js +5 -1
- package/dist/testing/InProcessConstraints.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +19 -8
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +6 -0
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +5 -0
- package/dist/testing/singleAppendRoute.d.ts.map +1 -1
- package/dist/testing/singleAppendRoute.js +12 -1
- package/dist/testing/singleAppendRoute.js.map +1 -1
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
- package/dist/transactions/IUnitOfWork.d.ts +5 -3
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/TransactionalAppendOptions.d.ts +35 -0
- package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
- package/dist/transactions/TransactionalAppendOptions.js +4 -0
- package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
- package/dist/transactions/UnitOfWork.d.ts +3 -2
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +32 -11
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
- package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
- package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
- package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
- package/dist/transactions/index.d.ts +2 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +1 -0
- package/dist/transactions/index.js.map +1 -1
- package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
- package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
- package/dist/transactions/toEventForEventSourceId.js +29 -0
- package/dist/transactions/toEventForEventSourceId.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +11 -0
- package/eventSequences/EventForEventSourceId.ts +7 -0
- package/eventSequences/EventSequence.ts +30 -8
- package/eventSequences/ITransactionalEventSequence.ts +5 -5
- package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
- package/eventSequences/TransactionalEventSequence.ts +8 -5
- package/eventSequences/createAppendNotification.ts +4 -2
- package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
- package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
- package/eventSequences/index.ts +1 -0
- package/eventSequences/prepareBatchAppend.ts +4 -1
- package/eventSequences/prepareSingleAppend.ts +3 -1
- package/events/EventContext.ts +8 -0
- package/events/InvalidNamedTag.ts +13 -0
- package/events/NamedTag.ts +44 -0
- package/events/constraints/ConstraintBuilder.ts +16 -0
- package/events/constraints/ConstraintEventSequences.ts +41 -0
- package/events/constraints/Constraints.ts +22 -3
- package/events/constraints/IConstraintBuilder.ts +20 -0
- package/events/constraints/UniqueOptions.ts +20 -0
- package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
- package/events/constraints/index.ts +2 -0
- package/events/constraints/unique.ts +22 -2
- package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
- package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
- package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
- package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
- package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
- package/events/index.ts +3 -0
- package/events/mergeNamedTags.ts +37 -0
- package/events/toClientEventContext.ts +3 -1
- package/package.json +6 -6
- package/projections/eventContextPropertyExpression.ts +3 -2
- package/testing/EventScenario.fixtures.spec.ts +1 -1
- package/testing/EventScenario.ts +10 -4
- package/testing/InProcessConstraints.ts +5 -1
- package/testing/InProcessEventSequence.ts +19 -8
- package/testing/ReactorScenario.ts +6 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
- package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
- package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
- package/testing/singleAppendRoute.ts +13 -1
- package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
- package/transactions/IUnitOfWork.ts +5 -3
- package/transactions/TransactionalAppendOptions.ts +45 -0
- package/transactions/UnitOfWork.ts +37 -12
- package/transactions/concurrencyScopesAreEqual.ts +23 -0
- package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
- package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
- package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
- package/transactions/index.ts +2 -0
- package/transactions/toEventForEventSourceId.ts +32 -0
- package/vitest.integration.config.ts +2 -0
- package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
- package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
- package/webhooks/for_Webhooks/when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts +43 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { ChronicleClient, ChronicleOptions, EventSequenceNumber, eventType, type AppendedEvent,
|
|
8
|
+
type IClientArtifactsProvider, type IEventStore } from '../../../index.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
12
|
+
|
|
13
|
+
@eventType()
|
|
14
|
+
class ItemAdded { @field(Number) order = 0; }
|
|
15
|
+
|
|
16
|
+
const artifacts: IClientArtifactsProvider = {
|
|
17
|
+
eventTypes: [ItemAdded], eventSources: [], readModels: [], reactors: [], reducers: [],
|
|
18
|
+
seeders: [], constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const item = (order: number) => Object.assign(new ItemAdded(), { order });
|
|
22
|
+
|
|
23
|
+
describe.skipIf(!connectionString && !process.env.CI)('when committing a unit of work with append metadata against a kernel', () => {
|
|
24
|
+
const storeName = `UnitOfWork${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
25
|
+
const occurred = new Date('2026-03-04T05:06:07.000Z');
|
|
26
|
+
let client: ChronicleClient;
|
|
27
|
+
let store: IEventStore;
|
|
28
|
+
let id: string;
|
|
29
|
+
let events: AppendedEvent[];
|
|
30
|
+
let committed: boolean;
|
|
31
|
+
let staleSucceeded: boolean;
|
|
32
|
+
let eventsAfterStale: number;
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, { discoveryPatterns: [], clientArtifactsProvider: artifacts }));
|
|
36
|
+
store = await client.getEventStore(storeName);
|
|
37
|
+
id = randomUUID();
|
|
38
|
+
const seeded = await store.eventLog.append(id, item(-1));
|
|
39
|
+
const tail = { sequenceNumber: seeded.sequenceNumber.value, eventSourceId: true };
|
|
40
|
+
|
|
41
|
+
const unitOfWork = store.unitOfWorkManager.begin();
|
|
42
|
+
await store.eventLog.transactional.append(id, item(0), {
|
|
43
|
+
eventStreamType: 'Orders', eventStreamId: 'order-1', eventSourceType: 'Customer',
|
|
44
|
+
subject: 'subject-1', occurred, tags: ['priority'], concurrencyScope: tail
|
|
45
|
+
});
|
|
46
|
+
await store.eventLog.transactional.append(id, item(1));
|
|
47
|
+
await unitOfWork.commit();
|
|
48
|
+
committed = unitOfWork.isSuccess;
|
|
49
|
+
events = (await store.eventLog.getFromSequenceNumber(EventSequenceNumber.first, id, [ItemAdded])).slice(1);
|
|
50
|
+
|
|
51
|
+
const stale = store.unitOfWorkManager.begin();
|
|
52
|
+
await store.eventLog.transactional.append(id, item(2), { concurrencyScope: tail });
|
|
53
|
+
await stale.commit();
|
|
54
|
+
staleSucceeded = stale.isSuccess;
|
|
55
|
+
eventsAfterStale = (await store.eventLog.getFromSequenceNumber(EventSequenceNumber.first, id, [ItemAdded])).length;
|
|
56
|
+
});
|
|
57
|
+
afterAll(() => client?.dispose());
|
|
58
|
+
|
|
59
|
+
it('should accept a current concurrency scope', () => committed.should.be.true);
|
|
60
|
+
it('should append both events in input order', () => events.map(_ => (_.content as { order: number }).order).should.deep.equal([0, 1]));
|
|
61
|
+
it('should keep the stream type', () => events[0].context.eventStreamType!.should.equal('Orders'));
|
|
62
|
+
it('should keep the stream id', () => events[0].context.eventStreamId!.should.equal('order-1'));
|
|
63
|
+
it('should keep the source type', () => events[0].context.eventSourceType!.should.equal('Customer'));
|
|
64
|
+
it('should keep the subject', () => events[0].context.subject!.should.equal('subject-1'));
|
|
65
|
+
it('should keep the occurrence time', () => events[0].context.occurred.toISOString().should.equal(occurred.toISOString()));
|
|
66
|
+
it('should keep the tags', () => events[0].context.tags.map(String).should.contain('priority'));
|
|
67
|
+
it('should default the subject of the event without options to its event source id', () => events[1].context.subject!.should.equal(id));
|
|
68
|
+
it('should reject a stale concurrency scope', () => staleSucceeded.should.be.false);
|
|
69
|
+
it('should not write the event rejected by the stale scope', () => eventsAfterStale.should.equal(3));
|
|
70
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { vi, type Mock } from 'vitest';
|
|
6
|
+
import type { AppendOptions } from '../../../eventSequences/AppendOptions.js';
|
|
7
|
+
import type { AppendResult } from '../../../eventSequences/AppendResult.js';
|
|
8
|
+
import type { EventForEventSourceId } from '../../../eventSequences/EventForEventSourceId.js';
|
|
9
|
+
import { EventSequenceNumber } from '../../../eventSequences/EventSequenceNumber.js';
|
|
10
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
11
|
+
import { UnitOfWork } from '../../UnitOfWork.js';
|
|
12
|
+
|
|
13
|
+
/** One recorded appendMany call. */
|
|
14
|
+
export interface RecordedAppend {
|
|
15
|
+
readonly eventSequenceId: string;
|
|
16
|
+
readonly events: EventForEventSourceId[];
|
|
17
|
+
readonly options: AppendOptions;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Creates a successful append result with a given sequence number. */
|
|
21
|
+
export function successfulAppendResult(sequenceNumber: bigint): AppendResult {
|
|
22
|
+
return { sequenceNumber: new EventSequenceNumber(sequenceNumber), constraintViolations: [], errors: [], isSuccess: true, waitForCompletion: vi.fn() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The unit of work and what its store recorded. */
|
|
26
|
+
export interface RecordingUnitOfWork {
|
|
27
|
+
readonly unitOfWork: UnitOfWork;
|
|
28
|
+
readonly appends: RecordedAppend[];
|
|
29
|
+
readonly record: Mock;
|
|
30
|
+
readonly completed: Mock;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a unit of work over a store whose event sequences record every appendMany call.
|
|
35
|
+
* @param appendMany - Optional replacement for the recording appendMany; receives the sequence id first.
|
|
36
|
+
*/
|
|
37
|
+
export function a_unit_of_work_with_a_recording_store(
|
|
38
|
+
appendMany?: (eventSequenceId: string, events: EventForEventSourceId[], options: AppendOptions) => Promise<AppendResult[]>
|
|
39
|
+
): RecordingUnitOfWork {
|
|
40
|
+
const appends: RecordedAppend[] = [];
|
|
41
|
+
let next = 0n;
|
|
42
|
+
const record: Mock = vi.fn(async (eventSequenceId: string, events: EventForEventSourceId[], options: AppendOptions) => {
|
|
43
|
+
appends.push({ eventSequenceId, events, options });
|
|
44
|
+
return appendMany ? appendMany(eventSequenceId, events, options) : events.map(() => successfulAppendResult(next++));
|
|
45
|
+
});
|
|
46
|
+
const store = {
|
|
47
|
+
getEventSequence: (id: { value: string }) => ({
|
|
48
|
+
appendMany: (events: EventForEventSourceId[], options: AppendOptions) => record(id.value, events, options)
|
|
49
|
+
})
|
|
50
|
+
} as unknown as IEventStore;
|
|
51
|
+
const completed = vi.fn();
|
|
52
|
+
const unitOfWork = new UnitOfWork(Guid.create(), completed, store);
|
|
53
|
+
return { unitOfWork, appends, record, completed };
|
|
54
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { EventType } from '../../../events/EventType.js';
|
|
7
|
+
import { a_unit_of_work_with_a_recording_store, type RecordedAppend } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
describe('when adding events with concurrency scopes and the event type filters are equivalent', () => {
|
|
12
|
+
let appends: RecordedAppend[];
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
const recording = a_unit_of_work_with_a_recording_store();
|
|
16
|
+
const orderPlaced = EventType.parse('OrderPlaced+1');
|
|
17
|
+
const orderShipped = EventType.parse('OrderShipped+2');
|
|
18
|
+
const add = (eventSourceId: string, order: number, concurrencyScope: object) =>
|
|
19
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, eventSourceId, { order }, { concurrencyScope });
|
|
20
|
+
|
|
21
|
+
add('omitted', 0, { sequenceNumber: 1n });
|
|
22
|
+
add('omitted', 1, { sequenceNumber: 1n, eventTypes: [] });
|
|
23
|
+
add('duplicates', 2, { sequenceNumber: 1n, eventTypes: [orderPlaced] });
|
|
24
|
+
add('duplicates', 3, { sequenceNumber: 1n, eventTypes: [orderPlaced, EventType.parse('OrderPlaced+1')] });
|
|
25
|
+
add('order', 4, { sequenceNumber: 1n, eventTypes: [orderPlaced, orderShipped] });
|
|
26
|
+
add('order', 5, { sequenceNumber: 1n, eventTypes: [orderShipped, orderPlaced] });
|
|
27
|
+
await recording.unitOfWork.commit();
|
|
28
|
+
appends = recording.appends;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should append every event', () => appends[0].events.should.have.lengthOf(6));
|
|
32
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { EventType } from '../../../events/EventType.js';
|
|
7
|
+
import { ConflictingConcurrencyScopesInUnitOfWork } from '../../ConflictingConcurrencyScopesInUnitOfWork.js';
|
|
8
|
+
import { a_unit_of_work_with_a_recording_store } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
9
|
+
import type { UnitOfWork } from '../../UnitOfWork.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
|
|
13
|
+
describe('when adding events with concurrency scopes and the event type filters differ', () => {
|
|
14
|
+
let unitOfWork: UnitOfWork;
|
|
15
|
+
let errors: unknown[];
|
|
16
|
+
|
|
17
|
+
const tryAdd = (eventSourceId: string, eventTypes: EventType[]) => {
|
|
18
|
+
try {
|
|
19
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, eventSourceId, { eventSourceId }, { concurrencyScope: { sequenceNumber: 1n, eventTypes } });
|
|
20
|
+
} catch (caught) {
|
|
21
|
+
errors.push(caught);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
({ unitOfWork } = a_unit_of_work_with_a_recording_store());
|
|
27
|
+
errors = [];
|
|
28
|
+
tryAdd('different-type', [EventType.parse('OrderPlaced+1')]);
|
|
29
|
+
tryAdd('different-type', [EventType.parse('OrderShipped+1')]);
|
|
30
|
+
tryAdd('different-generation', [EventType.parse('OrderPlaced+1')]);
|
|
31
|
+
tryAdd('different-generation', [EventType.parse('OrderPlaced+2')]);
|
|
32
|
+
tryAdd('empty-versus-filled', []);
|
|
33
|
+
tryAdd('empty-versus-filled', [EventType.parse('OrderPlaced+1')]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should reject each conflicting event', () => {
|
|
37
|
+
errors.should.have.lengthOf(3);
|
|
38
|
+
errors.every(_ => _ instanceof ConflictingConcurrencyScopesInUnitOfWork).should.be.true;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { a_unit_of_work_with_a_recording_store, type RecordedAppend } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when adding events with concurrency scopes and the scopes are equal or omitted', () => {
|
|
11
|
+
let appends: RecordedAppend[];
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
const recording = a_unit_of_work_with_a_recording_store();
|
|
15
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 0 }, { concurrencyScope: { sequenceNumber: 3n, eventStreamType: 'Orders' } });
|
|
16
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 1 });
|
|
17
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 2 }, { concurrencyScope: { eventStreamType: 'Orders', sequenceNumber: 3n } });
|
|
18
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, 'b-1', { order: 3 }, { concurrencyScope: { sequenceNumber: 9n } });
|
|
19
|
+
await recording.unitOfWork.commit();
|
|
20
|
+
appends = recording.appends;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should append every event', () => appends[0].events.should.have.lengthOf(4));
|
|
24
|
+
it('should pass one scope per event source', () => appends[0].options.concurrencyScopes!.should.deep.equal({
|
|
25
|
+
'a-1': { sequenceNumber: 3n, eventStreamType: 'Orders' },
|
|
26
|
+
'b-1': { sequenceNumber: 9n }
|
|
27
|
+
}));
|
|
28
|
+
it('should keep the correlation of the unit of work', () => appends[0].options.should.have.property('correlationId'));
|
|
29
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { ConflictingConcurrencyScopesInUnitOfWork } from '../../ConflictingConcurrencyScopesInUnitOfWork.js';
|
|
7
|
+
import { a_unit_of_work_with_a_recording_store } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
8
|
+
import type { UnitOfWork } from '../../UnitOfWork.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
describe('when adding events with concurrency scopes and the scopes differ for one event source', () => {
|
|
13
|
+
let unitOfWork: UnitOfWork;
|
|
14
|
+
let error: unknown;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
({ unitOfWork } = a_unit_of_work_with_a_recording_store());
|
|
18
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 0 }, { concurrencyScope: { sequenceNumber: 1n } });
|
|
19
|
+
try {
|
|
20
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 1 }, { concurrencyScope: { sequenceNumber: 2n } });
|
|
21
|
+
} catch (caught) {
|
|
22
|
+
error = caught;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reject the conflicting event', () => error!.should.be.instanceOf(ConflictingConcurrencyScopesInUnitOfWork));
|
|
27
|
+
it('should not name the event source in the message', () => (error as Error).message.should.not.contain('a-1'));
|
|
28
|
+
it('should keep only the first event', () => unitOfWork.getEvents().should.deep.equal([{ order: 0 }]));
|
|
29
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { a_unit_of_work_with_a_recording_store, successfulAppendResult, type RecordedAppend } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
7
|
+
import type { UnitOfWork } from '../../UnitOfWork.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
describe('when committing events with append metadata and a later event sequence fails', () => {
|
|
12
|
+
let unitOfWork: UnitOfWork;
|
|
13
|
+
let appends: RecordedAppend[];
|
|
14
|
+
let error: unknown;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
const recording = a_unit_of_work_with_a_recording_store(async eventSequenceId => {
|
|
18
|
+
if (eventSequenceId === 'outbox') throw new Error('Kernel unavailable');
|
|
19
|
+
return [successfulAppendResult(0n)];
|
|
20
|
+
});
|
|
21
|
+
({ unitOfWork, appends } = recording);
|
|
22
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 0 }, { subject: 'first' });
|
|
23
|
+
unitOfWork.addEvent(new EventSequenceId('outbox'), 'a-1', { order: 1 }, { subject: 'second' });
|
|
24
|
+
error = await unitOfWork.commit().catch(caught => caught);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should reject the commit', () => (error as Error).message.should.equal('Kernel unavailable'));
|
|
28
|
+
it('should have written the earlier event sequence, since appends to separate sequences are not atomic', () => appends[0].events[0].subject!.should.equal('first'));
|
|
29
|
+
it('should not mark the unit of work completed', () => unitOfWork.isCompleted.should.be.false);
|
|
30
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import type { AppendResult } from '../../../eventSequences/AppendResult.js';
|
|
7
|
+
import { a_unit_of_work_with_a_recording_store, successfulAppendResult, type RecordedAppend } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
describe('when committing events with append metadata and events span event sequences', () => {
|
|
12
|
+
const outbox = new EventSequenceId('outbox');
|
|
13
|
+
const scope = { sequenceNumber: 4n, eventSourceId: true };
|
|
14
|
+
let appends: RecordedAppend[];
|
|
15
|
+
let results: ReadonlyArray<AppendResult>;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
const recording = a_unit_of_work_with_a_recording_store(async (eventSequenceId, events) =>
|
|
19
|
+
events.map((_, index) => successfulAppendResult(BigInt(eventSequenceId === 'outbox' ? 100 + index : index))));
|
|
20
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 0 }, { subject: 'log-subject', concurrencyScope: scope });
|
|
21
|
+
recording.unitOfWork.addEvent(outbox, 'a-1', { order: 1 }, { subject: 'outbox-subject' });
|
|
22
|
+
recording.unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 2 });
|
|
23
|
+
await recording.unitOfWork.commit();
|
|
24
|
+
appends = recording.appends;
|
|
25
|
+
results = recording.unitOfWork.getAppendResults();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should append once per event sequence', () => appends.map(_ => _.eventSequenceId).should.deep.equal([EventSequenceId.eventLog.value, 'outbox']));
|
|
29
|
+
it('should keep the subject in the event log append', () => appends[0].events[0].subject!.should.equal('log-subject'));
|
|
30
|
+
it('should keep the subject in the outbox append', () => appends[1].events[0].subject!.should.equal('outbox-subject'));
|
|
31
|
+
it('should pass the scope to the event log append', () => appends[0].options.concurrencyScopes!.should.deep.equal({ 'a-1': scope }));
|
|
32
|
+
it('should not pass the scope to the outbox append', () => appends[1].options.should.not.have.property('concurrencyScopes'));
|
|
33
|
+
it('should report results in input order', () => results.map(_ => _.sequenceNumber.value).should.deep.equal([0n, 100n, 1n]));
|
|
34
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { Tag } from '../../../events/Tag.js';
|
|
6
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
7
|
+
import type { EventForEventSourceId } from '../../../eventSequences/EventForEventSourceId.js';
|
|
8
|
+
import { a_unit_of_work_with_a_recording_store } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
9
|
+
|
|
10
|
+
const should = chai.should();
|
|
11
|
+
|
|
12
|
+
describe('when committing events with append metadata and options differ per event', () => {
|
|
13
|
+
const occurred = new Date('2026-01-02T03:04:05.000Z');
|
|
14
|
+
const first = { name: 'first' };
|
|
15
|
+
const second = { name: 'second' };
|
|
16
|
+
const third = { name: 'third' };
|
|
17
|
+
let batch: EventForEventSourceId[];
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const { unitOfWork, appends } = a_unit_of_work_with_a_recording_store();
|
|
21
|
+
const tags = ['priority'];
|
|
22
|
+
const occurredInput = new Date(occurred.getTime());
|
|
23
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', first, {
|
|
24
|
+
eventStreamType: 'Orders', eventStreamId: 'order-1', eventSourceType: 'Customer',
|
|
25
|
+
subject: 'subject-1', occurred: occurredInput, tags
|
|
26
|
+
});
|
|
27
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'b-1', second, { tags: [new Tag('audit')] });
|
|
28
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', third);
|
|
29
|
+
tags.push('mutated-after-enrollment');
|
|
30
|
+
occurredInput.setUTCFullYear(1999);
|
|
31
|
+
await unitOfWork.commit();
|
|
32
|
+
batch = appends[0].events;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should append the events in input order', () => batch.map(_ => _.event).should.deep.equal([first, second, third]));
|
|
36
|
+
it('should keep the stream type of the first event', () => batch[0].eventStreamType!.should.equal('Orders'));
|
|
37
|
+
it('should keep the stream id of the first event', () => batch[0].eventStreamId!.should.equal('order-1'));
|
|
38
|
+
it('should keep the source type of the first event', () => batch[0].eventSourceType!.should.equal('Customer'));
|
|
39
|
+
it('should keep the subject of the first event', () => batch[0].subject!.should.equal('subject-1'));
|
|
40
|
+
it('should keep the occurrence time given at enrollment', () => batch[0].occurred!.toISOString().should.equal(occurred.toISOString()));
|
|
41
|
+
it('should keep the tags given at enrollment', () => batch[0].tags!.should.deep.equal(['priority']));
|
|
42
|
+
it('should keep the tags of the second event', () => batch[1].tags!.map(String).should.deep.equal(['audit']));
|
|
43
|
+
it('should not give the third event any metadata', () => Object.keys(batch[2]).sort().should.deep.equal(['event', 'eventSourceId']));
|
|
44
|
+
it('should not leak the subject of the first event to the second', () => should.not.exist(batch[1].subject));
|
|
45
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { EventSequenceNumber } from '../../../eventSequences/EventSequenceNumber.js';
|
|
7
|
+
import { a_unit_of_work_with_a_recording_store, successfulAppendResult } from '../given/a_unit_of_work_with_a_recording_store.js';
|
|
8
|
+
import type { UnitOfWork } from '../../UnitOfWork.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
describe('when committing events with append metadata and the append is rejected', () => {
|
|
13
|
+
const violation = { eventSourceId: 'a-1', expectedSequenceNumber: new EventSequenceNumber(1n), actualSequenceNumber: new EventSequenceNumber(5n) };
|
|
14
|
+
let unitOfWork: UnitOfWork;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
({ unitOfWork } = a_unit_of_work_with_a_recording_store(async () => [
|
|
18
|
+
{ ...successfulAppendResult(0n), isSuccess: false, concurrencyViolation: violation }
|
|
19
|
+
]));
|
|
20
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 0 }, { concurrencyScope: { sequenceNumber: 1n } });
|
|
21
|
+
await unitOfWork.commit();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should not report success', () => unitOfWork.isSuccess.should.be.false);
|
|
25
|
+
it('should surface the concurrency violation', () => unitOfWork.getConcurrencyViolations().should.deep.equal([violation]));
|
|
26
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { beforeEach, chai, describe, it, vi } from 'vitest';
|
|
6
|
+
import { InvalidNamedTag, NamedTag } from '../../events/index.js';
|
|
7
|
+
import { EventSequenceId, NamedTagsWithRegisteredEventSourceNotSupported, type EventForEventSourceId } from '../../eventSequences/index.js';
|
|
8
|
+
import { ConcurrencyDimensions, type IEventSources } from '../../eventSources/index.js';
|
|
9
|
+
import type { IEventStore } from '../../IEventStore.js';
|
|
10
|
+
import { UnitOfWork } from '../UnitOfWork.js';
|
|
11
|
+
|
|
12
|
+
const should = chai.should();
|
|
13
|
+
const success = { isSuccess: true, constraintViolations: [], errors: [] };
|
|
14
|
+
|
|
15
|
+
class Account {}
|
|
16
|
+
const eventSources = {
|
|
17
|
+
getFor: () => ({ type: Account, name: 'Account', description: '', concurrency: ConcurrencyDimensions.none, streams: [] })
|
|
18
|
+
} as unknown as IEventSources;
|
|
19
|
+
|
|
20
|
+
function a_unit_of_work() {
|
|
21
|
+
const appendMany = vi.fn().mockImplementation(async (events: EventForEventSourceId[]) => events.map(() => success));
|
|
22
|
+
const completed = vi.fn();
|
|
23
|
+
const unitOfWork = new UnitOfWork(Guid.create(), completed, { eventSources, getEventSequence: () => ({ appendMany }) } as unknown as IEventStore);
|
|
24
|
+
return { unitOfWork, appendMany, completed };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('when committing events with named tags in a unit of work', () => {
|
|
28
|
+
let batch: EventForEventSourceId[];
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
const { unitOfWork, appendMany } = a_unit_of_work();
|
|
31
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { first: 1 },
|
|
32
|
+
{ namedTags: [new NamedTag('batch', 'b-1'), new NamedTag('batch', 'b-1')] });
|
|
33
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-2', { second: 2 }, { namedTags: [] });
|
|
34
|
+
await unitOfWork.commit();
|
|
35
|
+
batch = appendMany.mock.calls[0][0];
|
|
36
|
+
});
|
|
37
|
+
it('should carry the distinct named tags on the event they were added with', () =>
|
|
38
|
+
batch[0].namedTags!.map(tag => `${tag.name}=${tag.value}`).should.deep.equal(['batch=b-1']));
|
|
39
|
+
it('should not add named tags to an event added with none', () => should.not.exist(batch[1].namedTags));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('when adding an event with an invalid named tag to a unit of work', () => {
|
|
43
|
+
let error: unknown;
|
|
44
|
+
let unitOfWork: UnitOfWork;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
({ unitOfWork } = a_unit_of_work());
|
|
47
|
+
try {
|
|
48
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', {}, { namedTags: [{ name: ' ', value: 'x' } as NamedTag] });
|
|
49
|
+
} catch (caught) {
|
|
50
|
+
error = caught;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
|
|
54
|
+
it('should not buffer the event', () => unitOfWork.getEvents().should.have.lengthOf(0));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
for (const separateSequences of [false, true]) {
|
|
58
|
+
describe(`when committing named tags and a registered source in ${separateSequences ? 'different event sequences' : 'one batch'}`, () => {
|
|
59
|
+
let calls: ReturnType<typeof a_unit_of_work>;
|
|
60
|
+
let error: unknown;
|
|
61
|
+
beforeEach(async () => {
|
|
62
|
+
calls = a_unit_of_work();
|
|
63
|
+
// An otherwise valid group must not be written before a later unsupported combination is found.
|
|
64
|
+
calls.unitOfWork.addEvent(EventSequenceId.eventLog, 'plain', { first: 1 });
|
|
65
|
+
const sequenceId = separateSequences ? new EventSequenceId('other') : EventSequenceId.eventLog;
|
|
66
|
+
calls.unitOfWork.addEvent(sequenceId, 'tagged', { second: 2 }, { namedTags: [new NamedTag('batch', 'b-1')] });
|
|
67
|
+
calls.unitOfWork.addEvent(sequenceId, 'registered', { third: 3 }, { eventSource: Account });
|
|
68
|
+
error = await calls.unitOfWork.commit().catch(caught => caught);
|
|
69
|
+
});
|
|
70
|
+
it('should reject with the typed unsupported combination error', () =>
|
|
71
|
+
(error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
|
|
72
|
+
it('should not write any group', () => calls.appendMany.mock.calls.should.have.lengthOf(0));
|
|
73
|
+
it('should leave the unit of work uncompleted', () => calls.unitOfWork.isCompleted.should.be.false);
|
|
74
|
+
it('should not invoke completion callbacks', () => calls.completed.mock.calls.should.have.lengthOf(0));
|
|
75
|
+
it('should retain the buffered events for rollback', () => calls.unitOfWork.getEvents().should.have.lengthOf(3));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('when committing tags and registered sources on different event sequences', () => {
|
|
80
|
+
let calls: ReturnType<typeof a_unit_of_work>;
|
|
81
|
+
let error: unknown;
|
|
82
|
+
beforeEach(async () => {
|
|
83
|
+
calls = a_unit_of_work();
|
|
84
|
+
calls.unitOfWork.addEvent(EventSequenceId.eventLog, 'tagged', {}, { namedTags: [new NamedTag('batch', 'b-1')] });
|
|
85
|
+
calls.unitOfWork.addEvent(new EventSequenceId('other'), 'registered', {}, { eventSource: 'Account' });
|
|
86
|
+
error = await calls.unitOfWork.commit().catch(caught => caught);
|
|
87
|
+
});
|
|
88
|
+
it('should reject the entire commit', () => (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
|
|
89
|
+
it('should not append the first event sequence', () => calls.appendMany.mock.calls.should.have.lengthOf(0));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('when committing a registered source and named tags on one event', () => {
|
|
93
|
+
let error: unknown;
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
const { unitOfWork } = a_unit_of_work();
|
|
96
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'registered', {}, { eventSource: 'Account', namedTags: [new NamedTag('batch', 'b-1')] });
|
|
97
|
+
error = await unitOfWork.commit().catch(caught => caught);
|
|
98
|
+
});
|
|
99
|
+
it('should reject with the typed unsupported combination error', () =>
|
|
100
|
+
(error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('when committing a registered source with an empty named tag array', () => {
|
|
104
|
+
let calls: ReturnType<typeof a_unit_of_work>;
|
|
105
|
+
beforeEach(async () => {
|
|
106
|
+
calls = a_unit_of_work();
|
|
107
|
+
calls.unitOfWork.addEvent(EventSequenceId.eventLog, 'registered', {}, { eventSource: 'Account', namedTags: [] });
|
|
108
|
+
await calls.unitOfWork.commit();
|
|
109
|
+
});
|
|
110
|
+
it('should append the registered source', () => calls.appendMany.mock.calls[0][0][0].eventSource.should.equal('Account'));
|
|
111
|
+
it('should complete the unit of work', () => calls.unitOfWork.isCompleted.should.be.true);
|
|
112
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it, type Mock } from 'vitest';
|
|
5
|
+
import { EventSequenceId } from '../../eventSequences/EventSequenceId.js';
|
|
6
|
+
import { a_unit_of_work_with_a_recording_store } from './given/a_unit_of_work_with_a_recording_store.js';
|
|
7
|
+
import type { UnitOfWork } from '../UnitOfWork.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
describe('when rolling back events with append metadata', () => {
|
|
12
|
+
let unitOfWork: UnitOfWork;
|
|
13
|
+
let record: Mock;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
({ unitOfWork, record } = a_unit_of_work_with_a_recording_store());
|
|
17
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { order: 0 }, { subject: 'subject-1', concurrencyScope: { sequenceNumber: 1n } });
|
|
18
|
+
await unitOfWork.rollback();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should drop the buffered events', () => unitOfWork.getEvents().should.be.empty);
|
|
22
|
+
it('should not append anything', () => record.mock.calls.should.be.empty);
|
|
23
|
+
it('should reject a later commit', async () => (await unitOfWork.commit().catch(caught => caught)).should.be.instanceOf(Error));
|
|
24
|
+
});
|
package/transactions/index.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { NoUnitOfWorkHasBeenStarted } from './NoUnitOfWorkHasBeenStarted.js';
|
|
|
7
7
|
export { UnitOfWork } from './UnitOfWork.js';
|
|
8
8
|
export { UnitOfWorkManager } from './UnitOfWorkManager.js';
|
|
9
9
|
export type { TransactionalEventRouting } from './TransactionalEventRouting.js';
|
|
10
|
+
export type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
|
|
11
|
+
export { ConflictingConcurrencyScopesInUnitOfWork } from './ConflictingConcurrencyScopesInUnitOfWork.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
|
|
5
|
+
import type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
|
|
6
|
+
import { mergeNamedTags } from '../events/mergeNamedTags.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Captures an event and its transactional append metadata as the batch entry appended on commit.
|
|
10
|
+
* Only supplied values become keys, and tags and occurrence time are copied so later caller mutations do not leak in.
|
|
11
|
+
* The concurrency scope is not part of the entry; the unit of work passes it per event source identifier.
|
|
12
|
+
* @param eventSourceId - The event source identifier.
|
|
13
|
+
* @param event - The event payload.
|
|
14
|
+
* @param options - Optional transactional append metadata.
|
|
15
|
+
* @returns The batch entry.
|
|
16
|
+
*/
|
|
17
|
+
export function toEventForEventSourceId(eventSourceId: string, event: object, options?: TransactionalAppendOptions): EventForEventSourceId {
|
|
18
|
+
const namedTags = mergeNamedTags(options?.namedTags);
|
|
19
|
+
return {
|
|
20
|
+
eventSourceId,
|
|
21
|
+
event,
|
|
22
|
+
...(options?.eventSource !== undefined ? { eventSource: options.eventSource } : {}),
|
|
23
|
+
...(options?.eventStream !== undefined ? { eventStream: options.eventStream } : {}),
|
|
24
|
+
...(options?.eventStreamType !== undefined ? { eventStreamType: options.eventStreamType } : {}),
|
|
25
|
+
...(options?.eventStreamId !== undefined ? { eventStreamId: options.eventStreamId } : {}),
|
|
26
|
+
...(options?.eventSourceType !== undefined ? { eventSourceType: options.eventSourceType } : {}),
|
|
27
|
+
...(options?.occurred !== undefined ? { occurred: new Date(options.occurred.getTime()) } : {}),
|
|
28
|
+
...(options?.subject !== undefined ? { subject: options.subject } : {}),
|
|
29
|
+
...(options?.tags !== undefined ? { tags: [...options.tags] } : {}),
|
|
30
|
+
...(namedTags.length > 0 ? { namedTags } : {})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -7,6 +7,8 @@ import baseConfig from './vitest.config.js';
|
|
|
7
7
|
// Kernel-backed specifications. They need a running Chronicle kernel and are skipped unless
|
|
8
8
|
// CHRONICLE_INTEGRATION_CONNECTION_STRING points at one, for example
|
|
9
9
|
// chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000
|
|
10
|
+
// The webhook delivery specifications also need the kernel to reach this process over HTTP, as
|
|
11
|
+
// host.docker.internal by default; set CHRONICLE_INTEGRATION_WEBHOOK_HOST when it reaches it differently.
|
|
10
12
|
export default defineConfig({
|
|
11
13
|
...baseConfig,
|
|
12
14
|
test: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A request the kernel delivered to a {@link WebhookReceiver}.
|
|
8
|
+
*/
|
|
9
|
+
export interface ReceivedWebhookRequest {
|
|
10
|
+
/** The HTTP method. */
|
|
11
|
+
readonly method: string;
|
|
12
|
+
|
|
13
|
+
/** The request path, including any query string. */
|
|
14
|
+
readonly path: string;
|
|
15
|
+
|
|
16
|
+
/** The request headers, with lowercase names. */
|
|
17
|
+
readonly headers: IncomingHttpHeaders;
|
|
18
|
+
|
|
19
|
+
/** The raw request body. */
|
|
20
|
+
readonly body: string;
|
|
21
|
+
}
|