@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,17 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { InvalidNamedTag } from '../../InvalidNamedTag.js';
|
|
6
|
+
import { NamedTag } from '../../NamedTag.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when constructing a named tag with invalid values', () => {
|
|
11
|
+
it('should reject an empty name', () => (() => new NamedTag('', 'value')).should.throw(InvalidNamedTag));
|
|
12
|
+
it('should reject a whitespace name', () => (() => new NamedTag(' \t', 'value')).should.throw(InvalidNamedTag));
|
|
13
|
+
it('should reject a name of only next-line characters like .NET', () => (() => new NamedTag('\u0085', 'value')).should.throw(InvalidNamedTag));
|
|
14
|
+
it('should reject a missing name', () => (() => new NamedTag(undefined as unknown as string, 'value')).should.throw(InvalidNamedTag));
|
|
15
|
+
it('should reject a null value', () => (() => new NamedTag('name', null as unknown as string)).should.throw(InvalidNamedTag));
|
|
16
|
+
it('should reject a non-string value', () => (() => new NamedTag('name', 42 as unknown as string)).should.throw(InvalidNamedTag));
|
|
17
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { NamedTag } from '../../NamedTag.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when constructing a named tag with a name and an empty value', () => {
|
|
10
|
+
const tag = new NamedTag('import-batch', '');
|
|
11
|
+
it('should keep the name exactly', () => tag.name.should.equal('import-batch'));
|
|
12
|
+
it('should keep the empty value', () => tag.value.should.equal(''));
|
|
13
|
+
it('should be immutable', () => Object.isFrozen(tag).should.be.true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('when comparing named tags', () => {
|
|
17
|
+
it('should treat the same name and value as equal', () => new NamedTag('a', 'B').equals(new NamedTag('a', 'B')).should.be.true);
|
|
18
|
+
it('should compare values case-sensitively', () => new NamedTag('a', 'B').equals(new NamedTag('a', 'b')).should.be.false);
|
|
19
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { mergeNamedTags } from '../../mergeNamedTags.js';
|
|
6
|
+
import { NamedTag } from '../../NamedTag.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when merging event and call named tags', () => {
|
|
11
|
+
let result: NamedTag[];
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
result = mergeNamedTags(
|
|
14
|
+
[new NamedTag('side', 'debit'), new NamedTag('transfer', 't-1')],
|
|
15
|
+
[new NamedTag('transfer', 't-1'), new NamedTag('transfer', 'T-1'), new NamedTag('side', 'credit')]);
|
|
16
|
+
});
|
|
17
|
+
it('should keep the first occurrence of each exact pair in order, including several values per name', () =>
|
|
18
|
+
result.map(tag => [tag.name, tag.value]).should.deep.equal([
|
|
19
|
+
['side', 'debit'], ['transfer', 't-1'], ['transfer', 'T-1'], ['side', 'credit']
|
|
20
|
+
]));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('when merging plain named tag objects', () => {
|
|
24
|
+
let result: NamedTag[];
|
|
25
|
+
beforeEach(() => { result = mergeNamedTags([{ name: 'channel', value: 'web' } as NamedTag]); });
|
|
26
|
+
it('should return validated named tag instances', () => result[0].should.be.instanceOf(NamedTag));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when merging no named tags', () => {
|
|
30
|
+
it('should return an empty list', () => mergeNamedTags(undefined, null, []).should.deep.equal([]));
|
|
31
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { InvalidNamedTag } from '../../InvalidNamedTag.js';
|
|
6
|
+
import { mergeNamedTags } from '../../mergeNamedTags.js';
|
|
7
|
+
import type { NamedTag } from '../../NamedTag.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
describe('when merging named tags with invalid entries', () => {
|
|
12
|
+
it('should reject a null element', () => (() => mergeNamedTags([null as unknown as NamedTag])).should.throw(InvalidNamedTag));
|
|
13
|
+
it('should reject a plain object with a blank name', () =>
|
|
14
|
+
(() => mergeNamedTags([{ name: ' ', value: 'x' } as NamedTag])).should.throw(InvalidNamedTag));
|
|
15
|
+
it('should reject a string instead of a collection', () =>
|
|
16
|
+
(() => mergeNamedTags('name' as unknown as NamedTag[])).should.throw(InvalidNamedTag));
|
|
17
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
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 type { EventContext } from '../EventContext.js';
|
|
6
|
+
import { NamedTag } from '../NamedTag.js';
|
|
7
|
+
import { toClientEventContext } from '../toClientEventContext.js';
|
|
8
|
+
import type { WireEventContext } from '../WireEventContext.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
function wireContext(namedTags?: { Name: string; Value: string }[]): WireEventContext {
|
|
13
|
+
return {
|
|
14
|
+
EventSourceId: 'source', SequenceNumber: 1n, EventSourceType: 'Default', EventStreamType: 'All', EventStreamId: 'Default',
|
|
15
|
+
EventType: { Id: 'type', Generation: 1, Tombstone: false }, Occurred: { Value: '2025-01-01T00:00:00.000Z' },
|
|
16
|
+
CorrelationId: undefined, Causation: [], CausedBy: undefined, Tags: [], Hash: '', ObservationState: 1, Subject: 'source',
|
|
17
|
+
NamedTags: namedTags
|
|
18
|
+
} as unknown as WireEventContext;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('when converting a delivered context with named tags', () => {
|
|
22
|
+
let context: EventContext;
|
|
23
|
+
beforeEach(() => { context = toClientEventContext(wireContext([{ Name: 'batch', Value: 'b-1' }, { Name: 'batch', Value: '' }])); });
|
|
24
|
+
it('should expose every named tag in delivered order', () =>
|
|
25
|
+
context.namedTags!.map(tag => [tag.name, tag.value]).should.deep.equal([['batch', 'b-1'], ['batch', '']]));
|
|
26
|
+
it('should expose named tag instances', () => context.namedTags![0].should.be.instanceOf(NamedTag));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when converting a delivered context without named tags', () => {
|
|
30
|
+
it('should expose an empty list', () => toClientEventContext(wireContext()).namedTags!.should.deep.equal([]));
|
|
31
|
+
});
|
package/events/index.ts
CHANGED
|
@@ -21,5 +21,8 @@ export { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorato
|
|
|
21
21
|
export { eventSourceType, getEventSourceTypeFor } from './eventSourceTypeDecorator.js';
|
|
22
22
|
export { eventStreamType, getEventStreamTypeFor } from './eventStreamTypeDecorator.js';
|
|
23
23
|
export { mergeTags } from './mergeTags.js';
|
|
24
|
+
export { NamedTag } from './NamedTag.js';
|
|
25
|
+
export { InvalidNamedTag } from './InvalidNamedTag.js';
|
|
26
|
+
export { mergeNamedTags } from './mergeNamedTags.js';
|
|
24
27
|
export * from './constraints/index.js';
|
|
25
28
|
export * from './migrations/index.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { InvalidNamedTag } from './InvalidNamedTag.js';
|
|
5
|
+
import { NamedTag } from './NamedTag.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates and merges structured named tags - for example the tags carried by an individual event and
|
|
9
|
+
* the tags supplied for a whole append call - into one distinct list. The first occurrence of each exact
|
|
10
|
+
* (ordinal, case-sensitive) name and value pair is kept, in order. Mirrors the .NET client's
|
|
11
|
+
* `NamedTagConverters.Merge`.
|
|
12
|
+
* @param sources - The named tag sources to merge, in precedence order. An undefined or null source contributes nothing.
|
|
13
|
+
* @returns The distinct, validated named tags.
|
|
14
|
+
* @throws {@link InvalidNamedTag} when a source is not iterable, contains an entry that is not an object,
|
|
15
|
+
* or contains a tag with a blank name or a non-string value.
|
|
16
|
+
*/
|
|
17
|
+
export function mergeNamedTags(...sources: ReadonlyArray<Iterable<NamedTag> | undefined | null>): NamedTag[] {
|
|
18
|
+
const seen = new Set<string>();
|
|
19
|
+
const merged: NamedTag[] = [];
|
|
20
|
+
for (const source of sources) {
|
|
21
|
+
if (source === undefined || source === null) continue;
|
|
22
|
+
if (typeof source !== 'object' || typeof (source as Iterable<NamedTag>)[Symbol.iterator] !== 'function') {
|
|
23
|
+
throw new InvalidNamedTag();
|
|
24
|
+
}
|
|
25
|
+
for (const entry of source as Iterable<unknown>) {
|
|
26
|
+
if (entry === null || typeof entry !== 'object') throw new InvalidNamedTag();
|
|
27
|
+
// Reconstruct so plain { name, value } objects are validated exactly like NamedTag instances.
|
|
28
|
+
const tag = entry instanceof NamedTag ? entry
|
|
29
|
+
: new NamedTag(Reflect.get(entry, 'name') as string, Reflect.get(entry, 'value') as string);
|
|
30
|
+
const key = JSON.stringify([tag.name, tag.value]);
|
|
31
|
+
if (seen.has(key)) continue;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
merged.push(tag);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return merged;
|
|
37
|
+
}
|
|
@@ -8,6 +8,7 @@ import { EventType } from './EventType.js';
|
|
|
8
8
|
import { EventTypeGeneration } from './EventTypeGeneration.js';
|
|
9
9
|
import { EventTypeId } from './EventTypeId.js';
|
|
10
10
|
import { Tag } from './Tag.js';
|
|
11
|
+
import { NamedTag } from './NamedTag.js';
|
|
11
12
|
import type { WireEventContext } from './WireEventContext.js';
|
|
12
13
|
|
|
13
14
|
/** Converts kernel metadata identically for reads, reactors, and reducers. */
|
|
@@ -37,7 +38,8 @@ export function toClientEventContext(context: WireEventContext): EventContext {
|
|
|
37
38
|
occurred: causation.Occurred ? new Date(causation.Occurred.Value) : undefined,
|
|
38
39
|
properties: { ...causation.Properties }
|
|
39
40
|
})),
|
|
40
|
-
tags: (context.Tags ?? []).map(value => new Tag(value))
|
|
41
|
+
tags: (context.Tags ?? []).map(value => new Tag(value)),
|
|
42
|
+
namedTags: (context.NamedTags ?? []).map(tag => new NamedTag(tag.Name, tag.Value))
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.51.0",
|
|
4
4
|
"description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
|
|
5
5
|
"author": "Cratis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -173,12 +173,12 @@
|
|
|
173
173
|
"clean": "rm -rf dist",
|
|
174
174
|
"test": "vitest run",
|
|
175
175
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
176
|
-
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts events/for_observerFilters/when_registering/with_standard_decorators.standard.spec.ts reactors/for_Reactors/against_a_kernel/when_filtering_source_and_stream_types.integration.spec.ts testing/for_observerScenarios/when_selecting_filters/with_source_and_stream_types.spec.ts readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts for_EventStore/when_implementing_a_legacy_event_store.spec.ts",
|
|
176
|
+
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts events/for_observerFilters/when_registering/with_standard_decorators.standard.spec.ts reactors/for_Reactors/against_a_kernel/when_filtering_source_and_stream_types.integration.spec.ts testing/for_observerScenarios/when_selecting_filters/with_source_and_stream_types.spec.ts readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts for_EventStore/when_implementing_a_legacy_event_store.spec.ts",
|
|
177
177
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
178
178
|
},
|
|
179
179
|
"dependencies": {
|
|
180
180
|
"@bufbuild/protobuf": "^2.16.0",
|
|
181
|
-
"@cratis/chronicle.contracts": "19.
|
|
181
|
+
"@cratis/chronicle.contracts": "19.31.10",
|
|
182
182
|
"@grpc/grpc-js": "^1.14.5",
|
|
183
183
|
"@opentelemetry/api": "^1.9.1",
|
|
184
184
|
"glob": "^13.0.6",
|
|
@@ -188,14 +188,14 @@
|
|
|
188
188
|
"undici": "^8.11.2"
|
|
189
189
|
},
|
|
190
190
|
"devDependencies": {
|
|
191
|
-
"@cratis/fundamentals": "7.
|
|
191
|
+
"@cratis/fundamentals": "7.22.4",
|
|
192
192
|
"@opentelemetry/context-async-hooks": "2.11.0",
|
|
193
193
|
"@opentelemetry/core": "2.11.0",
|
|
194
194
|
"@opentelemetry/instrumentation-grpc": "0.222.0",
|
|
195
195
|
"@opentelemetry/instrumentation-http": "0.222.0",
|
|
196
196
|
"@opentelemetry/sdk-metrics": "2.11.0",
|
|
197
197
|
"@opentelemetry/sdk-trace-base": "2.11.0",
|
|
198
|
-
"@types/node": "^26.6.
|
|
198
|
+
"@types/node": "^26.6.4",
|
|
199
199
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
200
200
|
"esbuild": "^0.28.2",
|
|
201
201
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
@@ -203,6 +203,6 @@
|
|
|
203
203
|
"vitest": "^5.0.3"
|
|
204
204
|
},
|
|
205
205
|
"peerDependencies": {
|
|
206
|
-
"@cratis/fundamentals": "^7.
|
|
206
|
+
"@cratis/fundamentals": "^7.22.4"
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -23,8 +23,9 @@ const contextProperties = {
|
|
|
23
23
|
correlationId: true,
|
|
24
24
|
causation: true,
|
|
25
25
|
tags: true
|
|
26
|
-
// eventSource
|
|
27
|
-
|
|
26
|
+
// eventSource and namedTags are deliberately not projectable: their kernel resolution is not part of the
|
|
27
|
+
// supported expression set.
|
|
28
|
+
} satisfies Record<Exclude<keyof EventContext, 'eventSource' | 'namedTags'>, true>;
|
|
28
29
|
|
|
29
30
|
// Mirror the kernel's DerivedPropertyFunctions registry: Week() applies to dates only.
|
|
30
31
|
const derivedFunctions: Record<string, string> = { week: 'Week' };
|
|
@@ -59,7 +59,7 @@ const fixtures = readdirSync(directory).filter(name => name.endsWith('.json') &&
|
|
|
59
59
|
!['batches.json', 'batch-omitted-routes.json', 'batch-rollback.json', 'builders.json', 'constraints.json', 'constraints-isolation.json', 'constraints-key-domain.json',
|
|
60
60
|
'constraints-property-lifecycle.json', 'constraints-property-covered-removal.json', 'constraints-event-type-cycles.json',
|
|
61
61
|
'constraints-event-type-siblings.json', 'constraints-composite.json',
|
|
62
|
-
'constraints-ignore-casing.json', 'constraints-scopes.json', 'constraints-field-types.json'].includes(name)).map(name => ({
|
|
62
|
+
'constraints-ignore-casing.json', 'constraints-scopes.json', 'constraints-field-types.json', 'constraints-event-sequences.json'].includes(name)).map(name => ({
|
|
63
63
|
name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture
|
|
64
64
|
}));
|
|
65
65
|
const artifacts = { eventTypes: [OracleEventRecorded, AlternateRecorded, BoundaryRecorded], constraints: [] };
|
package/testing/EventScenario.ts
CHANGED
|
@@ -85,9 +85,15 @@ export class EventScenario {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
// The scenario appends to the event log only. A definition scoped to other event sequences is neither
|
|
89
|
+
// validated nor indexed there by the kernel (constraints-event-sequences.json), so it takes no part.
|
|
90
|
+
const eventLog = EventSequenceId.eventLog.value;
|
|
91
|
+
const applicable = new Map([...definitions].filter(([, capture]) =>
|
|
92
|
+
!capture.eventSequences?.length || capture.eventSequences.includes(eventLog)));
|
|
93
|
+
const inapplicable = new Set([...definitions.keys()].filter(name => !applicable.has(name)));
|
|
94
|
+
constraints = new InProcessConstraints(applicable, new Map(eventTypes.map(type =>
|
|
95
|
+
[getEventTypeFor(type).id.value, getEventTypeMetadata(type)!.schema])), inapplicable);
|
|
96
|
+
for (const [name, capture] of applicable) {
|
|
91
97
|
const ids = capture.uniqueConstraint?.eventDefinitions.map(entry => entry.eventTypeId) ??
|
|
92
98
|
capture.uniqueEventType?.eventTypeIds ?? [capture.uniqueEventType?.eventTypeId];
|
|
93
99
|
const removedWith = [...(capture.uniqueConstraint?.removedWithEventTypeIds ?? []),
|
|
@@ -121,7 +127,7 @@ export class EventScenario {
|
|
|
121
127
|
`Unique keys with schema ${schema?.type ?? 'missing'}/${schema?.format ?? 'unformatted'} are not fixture-backed; date, object and other numeric formats require a kernel-backed test.`);
|
|
122
128
|
}
|
|
123
129
|
const extendedKey = schemas.some(schema => schema?.type === 'number' || schema?.format === 'guid');
|
|
124
|
-
if (extendedKey && (
|
|
130
|
+
if (extendedKey && (applicable.size !== 1 || capture.uniqueConstraint!.eventDefinitions.length !== 1 ||
|
|
125
131
|
removedWith.some(id => id !== undefined))) {
|
|
126
132
|
throw new UnsupportedEventSequenceOperation('artifacts.constraints', name,
|
|
127
133
|
'Guid and numeric keys require one isolated definition covering one event type without removers; shared definitions and removal combinations are not fixture-backed.');
|
|
@@ -69,7 +69,8 @@ export class InProcessConstraints {
|
|
|
69
69
|
private readonly _propertyRemovalTypes = new Set<string>();
|
|
70
70
|
|
|
71
71
|
constructor(private readonly _definitions: ReadonlyMap<string, ConstraintCapture>,
|
|
72
|
-
private readonly _schemas: ReadonlyMap<string, JsonSchema> = new Map()
|
|
72
|
+
private readonly _schemas: ReadonlyMap<string, JsonSchema> = new Map(),
|
|
73
|
+
private readonly _inapplicable: ReadonlySet<string> = new Set()) {
|
|
73
74
|
const coveredTypes = new Map<string, string>();
|
|
74
75
|
const removalOwners = new Map<string, string>();
|
|
75
76
|
for (const [name, capture] of _definitions) {
|
|
@@ -161,6 +162,9 @@ export class InProcessConstraints {
|
|
|
161
162
|
|
|
162
163
|
hasRemovalType(eventTypeId: string): boolean { return this._removalTypes.has(eventTypeId); }
|
|
163
164
|
|
|
165
|
+
/** Whether a named definition is scoped to event sequences other than the scenario's, so it takes no part. */
|
|
166
|
+
isInapplicable(name: string): boolean { return this._inapplicable.has(name); }
|
|
167
|
+
|
|
164
168
|
isConstrainedProperty(eventTypeId: string, property: string): boolean {
|
|
165
169
|
return this._constrainedProperties.get(eventTypeId)?.has(property) ?? false;
|
|
166
170
|
}
|
|
@@ -17,7 +17,7 @@ import { EventTypeId } from '../events/EventTypeId.js';
|
|
|
17
17
|
import { EventTypeGeneration } from '../events/EventTypeGeneration.js';
|
|
18
18
|
import { Identity } from '../identity/Identity.js';
|
|
19
19
|
import type { AppendOptions } from '../eventSequences/AppendOptions.js';
|
|
20
|
-
import { singleAppendRoute } from './singleAppendRoute.js';
|
|
20
|
+
import { hasNamedTags, singleAppendRoute } from './singleAppendRoute.js';
|
|
21
21
|
import type { AppendResult } from '../eventSequences/AppendResult.js';
|
|
22
22
|
import type { AppendedEventWithResult } from '../eventSequences/AppendedEventWithResult.js';
|
|
23
23
|
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
|
|
@@ -110,7 +110,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
110
110
|
if (eventType.generation.value !== 1 || eventType.tombstone) {
|
|
111
111
|
throw this.unsupported('artifacts.eventTypes', type.name, 'Only generation 1, non-tombstone events are proven.');
|
|
112
112
|
}
|
|
113
|
-
if (options.constraints !== 'disabled' &&
|
|
113
|
+
if (options.constraints !== 'disabled' &&
|
|
114
|
+
getRemovedConstraintNames(type).some(name => !this._constraints?.isInapplicable(name)) &&
|
|
114
115
|
!this._constraints?.hasRemovalType(eventType.id.value)) {
|
|
115
116
|
throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint removal is not fixture-backed.');
|
|
116
117
|
}
|
|
@@ -215,7 +216,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
215
216
|
subject: resolveStoredSubject(prepared.subject, eventSourceId), hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
|
|
216
217
|
eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
|
|
217
218
|
causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
218
|
-
tags: prepared.tags.map(tag => new Tag(tag))
|
|
219
|
+
tags: prepared.tags.map(tag => new Tag(tag)),
|
|
220
|
+
namedTags: []
|
|
219
221
|
}
|
|
220
222
|
};
|
|
221
223
|
const violations = this.validateConstraints([...this._history, ...(this._stagedSeeds ?? [])], [stored]);
|
|
@@ -254,10 +256,13 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
254
256
|
for (const entry of entries) {
|
|
255
257
|
if (typeof sourceOrEvents === 'string') break;
|
|
256
258
|
if (!entry || typeof entry !== 'object' || Reflect.ownKeys(entry).some(key =>
|
|
257
|
-
!['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags'].includes(String(key)))) {
|
|
259
|
+
!['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags', 'namedTags'].includes(String(key)))) {
|
|
258
260
|
throw this.unsupported('appendMany.entry', this.id.value, 'Unrecognized per-entry metadata.');
|
|
259
261
|
}
|
|
260
262
|
const individual = entry as EventForEventSourceId;
|
|
263
|
+
if (hasNamedTags(individual.namedTags)) {
|
|
264
|
+
throw this.unsupported('appendMany.namedTags', this.id.value, 'Named tags are not fixture-backed.');
|
|
265
|
+
}
|
|
261
266
|
if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
|
|
262
267
|
if (individual.subject !== undefined && individual.subject !== null && typeof individual.subject !== 'string') {
|
|
263
268
|
throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
|
|
@@ -271,7 +276,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
271
276
|
try {
|
|
272
277
|
const batchOptions: AppendOptions | undefined = shared?.correlationId === undefined && this._correlationId
|
|
273
278
|
? { correlationId: this._correlationId(), sourceType: shared?.sourceType, streamType: shared?.streamType,
|
|
274
|
-
streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags
|
|
279
|
+
streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags,
|
|
280
|
+
namedTags: shared?.namedTags }
|
|
275
281
|
: shared;
|
|
276
282
|
if (batchOptions?.correlationId !== undefined) this.validateGuid(batchOptions.correlationId, 'appendMany.correlationId');
|
|
277
283
|
prepared = typeof sourceOrEvents === 'string'
|
|
@@ -318,7 +324,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
318
324
|
eventStreamId: wire.EventStreamId || 'Default', subject: resolveStoredSubject(wire.Subject, eventSourceId), hash, causedBy: identity,
|
|
319
325
|
observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
|
|
320
326
|
causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
321
|
-
tags: wire.Tags.map(tag => new Tag(tag))
|
|
327
|
+
tags: wire.Tags.map(tag => new Tag(tag)),
|
|
328
|
+
namedTags: []
|
|
322
329
|
} };
|
|
323
330
|
return { stored, result: this.success(sequenceNumber, event.constructor.name) };
|
|
324
331
|
});
|
|
@@ -411,7 +418,10 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
411
418
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
412
419
|
throw this.unsupported('appendMany.options', this.id.value, 'Append options must be an object.');
|
|
413
420
|
}
|
|
414
|
-
|
|
421
|
+
if (hasNamedTags(options.namedTags)) {
|
|
422
|
+
throw this.unsupported('appendMany.namedTags', this.id.value, 'Named tags are not fixture-backed.');
|
|
423
|
+
}
|
|
424
|
+
const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags', 'namedTags'];
|
|
415
425
|
if (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
|
|
416
426
|
options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
|
|
417
427
|
throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
|
|
@@ -492,7 +502,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
492
502
|
context: { ...event.context, eventType, occurred: new Date(event.context.occurred),
|
|
493
503
|
causedBy: causedBy && new Identity(causedBy.subject, causedBy.name, causedBy.userName, causedBy.onBehalfOf),
|
|
494
504
|
causation: event.context.causation.map(item => ({ ...item, occurred: item.occurred && new Date(item.occurred), properties: { ...item.properties } })),
|
|
495
|
-
tags: event.context.tags.map(item => new Tag(item.value))
|
|
505
|
+
tags: event.context.tags.map(item => new Tag(item.value)),
|
|
506
|
+
namedTags: [...(event.context.namedTags ?? [])] }
|
|
496
507
|
};
|
|
497
508
|
}
|
|
498
509
|
|
|
@@ -13,11 +13,13 @@ import type { AppendedEvent } from '../events/AppendedEvent.js';
|
|
|
13
13
|
import type { EventContext } from '../events/EventContext.js';
|
|
14
14
|
import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
15
15
|
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator.js';
|
|
16
|
+
import { mergeNamedTags } from '../events/mergeNamedTags.js';
|
|
16
17
|
import { getEventSourceTypeFor } from '../events/eventSourceTypeDecorator.js';
|
|
17
18
|
import { getDeclaredEventStreamTypeFor, getEventStreamTypeFor } from '../events/eventStreamTypeDecorator.js';
|
|
18
19
|
import type { IEventLog } from '../eventSequences/IEventLog.js';
|
|
19
20
|
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
|
|
20
21
|
import type { AppendOptions } from '../eventSequences/AppendOptions.js';
|
|
22
|
+
import { ensureNamedTagBatchIsSupported } from '../eventSequences/ensureNamedTagBatchIsSupported.js';
|
|
21
23
|
import { EventStoreName } from '../EventStoreName.js';
|
|
22
24
|
import { EventStoreNamespaceName } from '../EventStoreNamespaceName.js';
|
|
23
25
|
import type { IEventStore } from '../IEventStore.js';
|
|
@@ -297,6 +299,10 @@ export class ReactorScenario {
|
|
|
297
299
|
throw new UnsupportedReactorOperation('handler.return', this._reactor.name,
|
|
298
300
|
'Only registered events and production EventForEventSourceId wrappers may be recorded; commands and unknown values are unsupported.');
|
|
299
301
|
}
|
|
302
|
+
// Production validates named tags before appending, failing the delivery on an invalid tag; so does recording.
|
|
303
|
+
for (const target of events) mergeNamedTags(target.namedTags);
|
|
304
|
+
ensureNamedTagBatchIsSupported(events.some(event => mergeNamedTags(event.namedTags).length > 0),
|
|
305
|
+
events.some(event => event.eventSource !== undefined));
|
|
300
306
|
for (const target of events) {
|
|
301
307
|
this._effects.push(Object.freeze({ kind: 'event', value: target.event, target, triggeringContext: context,
|
|
302
308
|
handler, deliveryIndex }));
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
5
|
+
import { chai, describe, it, vi, type Assertion } from 'vitest';
|
|
6
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
7
|
+
import { getEventTypeMetadata } from '../../../events/eventTypeDecorator.js';
|
|
8
|
+
import { compileConstraints } from '../../../events/constraints/Constraints.js';
|
|
9
|
+
import { EventScenario } from '../../index.js';
|
|
10
|
+
import { InProcessConstraints } from '../../InProcessConstraints.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
|
|
14
|
+
|
|
15
|
+
type Entry = { source: string; type: string; value: string };
|
|
16
|
+
type Definition = { kind: string; name: string; events?: Array<{ type: string; properties: string[] }>; eventTypes?: string[];
|
|
17
|
+
ignoreCasing?: boolean; removedWithEventTypeIds: string[]; message: string; eventSequences: string[] };
|
|
18
|
+
type History = { sequence: string; source: string; sourceType?: string; streamType?: string; streamId?: string;
|
|
19
|
+
type: string; content: Record<string, unknown>; hash?: string };
|
|
20
|
+
type Outcome = { success: boolean; sequences: string[]; errors: string[];
|
|
21
|
+
wireViolations: Array<{ EventTypeId: string; SequenceNumber: string; ConstraintType: number; ConstraintName: string; Message: string; Details: Record<string, string> }>;
|
|
22
|
+
violations: Array<{ id: string; message: string; details: Record<string, string> }>; history: History[]; next: string };
|
|
23
|
+
type SequenceCase = { name: string; kind: string; eventSchemas: Record<string, { eventTypeId: string; properties: Record<string, string> }>;
|
|
24
|
+
constraintDefinitions: Definition[]; isolatedConstraintOperations: Array<{ mode: string; events: Entry[] }> };
|
|
25
|
+
type Fixture = { eventSequenceCases: SequenceCase[]; expected: { cases: Array<{ name: string; result: { outcomes: Outcome[] } }> } };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Replays constraints-event-sequences.json: a definition applying to the event log behaves as unscoped, and one
|
|
29
|
+
* applying only to other event sequences is neither validated nor indexed on the scenario's event log.
|
|
30
|
+
* @param types - Event constructors by fixture alias.
|
|
31
|
+
* @param constraints - Selects the constraint classes for a case name.
|
|
32
|
+
*/
|
|
33
|
+
export function eventSequenceScenarioBehaviors(
|
|
34
|
+
types: Record<string, new (value: string) => object>,
|
|
35
|
+
constraints: (caseName: string) => Constructor[]
|
|
36
|
+
): void {
|
|
37
|
+
const fixture = JSON.parse(readFileSync(new URL('../../fixtures/constraints-event-sequences.json', import.meta.url), 'utf8')) as Fixture;
|
|
38
|
+
const historyOf = (scenario: EventScenario): History[] => scenario.appendedEvents.map(event => ({
|
|
39
|
+
sequence: event.context.sequenceNumber.toString(), source: event.context.eventSourceId,
|
|
40
|
+
sourceType: event.context.eventSourceType, streamType: event.context.eventStreamType, streamId: event.context.eventStreamId,
|
|
41
|
+
type: event.eventType.id.value, content: event.content, hash: event.context.hash
|
|
42
|
+
}));
|
|
43
|
+
const aliases = Object.fromEntries(Object.entries(types).map(([alias, type]) => [getEventTypeMetadata(type)!.eventType.id.value, alias]));
|
|
44
|
+
|
|
45
|
+
it('should retain every event sequence case', () => {
|
|
46
|
+
should(fixture.eventSequenceCases.map(test => test.name)).deep.equal([
|
|
47
|
+
'property-event-log', 'property-event-log-and-outbox', 'property-outbox',
|
|
48
|
+
'cycle-event-log', 'cycle-outbox', 'once-event-log', 'once-outbox'
|
|
49
|
+
]);
|
|
50
|
+
should(fixture.expected.cases.map(test => test.name)).deep.equal(fixture.eventSequenceCases.map(test => test.name));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const [caseIndex, test] of fixture.eventSequenceCases.entries()) {
|
|
54
|
+
describe(`with ${test.name}`, () => {
|
|
55
|
+
it('should match the installed definitions and kernel violations, history and next sequence', async () => {
|
|
56
|
+
const selected = Object.keys(test.eventSchemas).map(alias => types[alias]);
|
|
57
|
+
const selectedConstraints = constraints(test.name);
|
|
58
|
+
const compiled = compileConstraints({ eventTypes: selected, constraints: selectedConstraints });
|
|
59
|
+
should([...compiled].map(([name, capture]) => ({
|
|
60
|
+
kind: capture.uniqueConstraint ? 'uniqueProperty' : 'uniqueEventType', name,
|
|
61
|
+
...(capture.uniqueConstraint ? {
|
|
62
|
+
events: capture.uniqueConstraint.eventDefinitions.map(entry => ({ type: aliases[entry.eventTypeId], properties: entry.properties })),
|
|
63
|
+
ignoreCasing: capture.uniqueConstraint.ignoreCasing
|
|
64
|
+
} : { eventTypes: (capture.uniqueEventType!.eventTypeIds ?? [capture.uniqueEventType!.eventTypeId]).map(id => aliases[id]) }),
|
|
65
|
+
removedWithEventTypeIds: (capture.uniqueConstraint?.removedWithEventTypeIds ?? capture.uniqueEventType?.removedWithEventTypeIds ?? []).map(id => aliases[id]),
|
|
66
|
+
message: capture.uniqueConstraint?.message ?? capture.uniqueEventType?.message ?? '',
|
|
67
|
+
eventSequences: capture.eventSequences ?? []
|
|
68
|
+
}))).deep.equal(test.constraintDefinitions);
|
|
69
|
+
const scenario = new EventScenario({ artifacts: { eventTypes: selected, constraints: selectedConstraints } });
|
|
70
|
+
const validate = vi.spyOn(InProcessConstraints.prototype, 'validate');
|
|
71
|
+
const expected = fixture.expected.cases[caseIndex].result.outcomes;
|
|
72
|
+
should(expected.length).equal(test.isolatedConstraintOperations.length);
|
|
73
|
+
try {
|
|
74
|
+
for (const [index, operation] of test.isolatedConstraintOperations.entries()) {
|
|
75
|
+
const before = historyOf(scenario);
|
|
76
|
+
const entries = operation.events.map(entry => ({ eventSourceId: entry.source, event: new types[entry.type](entry.value) }));
|
|
77
|
+
const results = operation.mode === 'single'
|
|
78
|
+
? [await scenario.append(entries[0].eventSourceId, entries[0].event)]
|
|
79
|
+
: await scenario.appendMany(entries);
|
|
80
|
+
const outcome = expected[index];
|
|
81
|
+
const raw = validate.mock.results.at(-1)!.value as ReturnType<InProcessConstraints['validate']>;
|
|
82
|
+
should(raw.map(({ SequenceNumber, ...violation }) => ({ ...violation, SequenceNumber: SequenceNumber.toString() })))
|
|
83
|
+
.deep.equal(outcome.wireViolations);
|
|
84
|
+
for (const [position, result] of results.entries()) {
|
|
85
|
+
should(result.isSuccess).equal(outcome.success);
|
|
86
|
+
should(result.sequenceNumber.value.toString()).equal(outcome.success ? outcome.sequences[position] : '0');
|
|
87
|
+
should(result.constraintViolations.map(violation => ({ id: violation.constraintId, message: violation.message, details: violation.details })))
|
|
88
|
+
.deep.equal(outcome.violations);
|
|
89
|
+
should(result.errors.map(error => error.message)).deep.equal(outcome.errors);
|
|
90
|
+
}
|
|
91
|
+
if (!outcome.success) should(historyOf(scenario)).deep.equal(before);
|
|
92
|
+
should(historyOf(scenario)).deep.equal(outcome.history);
|
|
93
|
+
should((await scenario.eventLog.getNextSequenceNumber()).value.toString()).equal(outcome.next);
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
validate.mockRestore();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 'reflect-metadata';
|
|
5
|
+
import { field } from '@cratis/fundamentals';
|
|
6
|
+
import { describe } from 'vitest';
|
|
7
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
8
|
+
import { constraint } from '../../../events/constraints/constraint.js';
|
|
9
|
+
import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
|
|
10
|
+
import { otherSequenceExtendedKeyBehaviors } from './other_sequence_extended_key_behaviors.fixture.js';
|
|
11
|
+
|
|
12
|
+
@eventType('OracleOtherSequenceNumber')
|
|
13
|
+
class NumberClaimed { @field(Number) key = 1; }
|
|
14
|
+
@eventType('OracleOtherSequenceOutbox')
|
|
15
|
+
class OutboxClaimed { @field(String) label = 'x'; }
|
|
16
|
+
|
|
17
|
+
@constraint('OracleOtherSequenceNumberKey')
|
|
18
|
+
class NumberKey {
|
|
19
|
+
define(builder: IConstraintBuilder) { builder.forEventLog().unique(key => key.on(NumberClaimed, event => event.key)); }
|
|
20
|
+
}
|
|
21
|
+
@constraint('OracleOtherSequenceOutboxKey')
|
|
22
|
+
class OutboxKey {
|
|
23
|
+
define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').unique(key => key.on(OutboxClaimed, event => event.label)); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('when appending extended keys beside other event sequence constraints with decorators', () => {
|
|
27
|
+
otherSequenceExtendedKeyBehaviors(NumberClaimed, OutboxClaimed, [NumberKey, OutboxKey]);
|
|
28
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { field } from '@cratis/fundamentals';
|
|
5
|
+
import { describe } from 'vitest';
|
|
6
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
7
|
+
import { constraint } from '../../../events/constraints/constraint.js';
|
|
8
|
+
import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
|
|
9
|
+
import { otherSequenceExtendedKeyBehaviors } from './other_sequence_extended_key_behaviors.fixture.js';
|
|
10
|
+
|
|
11
|
+
@eventType('OracleOtherSequenceNumber')
|
|
12
|
+
class NumberClaimed { @field(Number) key = 1; }
|
|
13
|
+
@eventType('OracleOtherSequenceOutbox')
|
|
14
|
+
class OutboxClaimed { @field(String) label = 'x'; }
|
|
15
|
+
|
|
16
|
+
@constraint('OracleOtherSequenceNumberKey')
|
|
17
|
+
class NumberKey {
|
|
18
|
+
define(builder: IConstraintBuilder) { builder.forEventLog().unique(key => key.on(NumberClaimed, event => event.key)); }
|
|
19
|
+
}
|
|
20
|
+
@constraint('OracleOtherSequenceOutboxKey')
|
|
21
|
+
class OutboxKey {
|
|
22
|
+
define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').unique(key => key.on(OutboxClaimed, event => event.label)); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('when appending extended keys beside other event sequence constraints with decorators', () => {
|
|
26
|
+
otherSequenceExtendedKeyBehaviors(NumberClaimed, OutboxClaimed, [NumberKey, OutboxKey]);
|
|
27
|
+
});
|