@cratis/chronicle 6.31.3 → 6.32.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.
Files changed (34) hide show
  1. package/dist/testing/ConstraintScope.d.ts +9 -0
  2. package/dist/testing/ConstraintScope.d.ts.map +1 -0
  3. package/dist/testing/ConstraintScope.js +36 -0
  4. package/dist/testing/ConstraintScope.js.map +1 -0
  5. package/dist/testing/EventScenario.d.ts.map +1 -1
  6. package/dist/testing/EventScenario.js +10 -0
  7. package/dist/testing/EventScenario.js.map +1 -1
  8. package/dist/testing/InProcessConstraints.d.ts +1 -1
  9. package/dist/testing/InProcessConstraints.d.ts.map +1 -1
  10. package/dist/testing/InProcessConstraints.js +23 -13
  11. package/dist/testing/InProcessConstraints.js.map +1 -1
  12. package/dist/testing/InProcessEventSequence.d.ts +1 -0
  13. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  14. package/dist/testing/InProcessEventSequence.js +33 -10
  15. package/dist/testing/InProcessEventSequence.js.map +1 -1
  16. package/dist/tsconfig.tsbuildinfo +1 -1
  17. package/package.json +2 -2
  18. package/testing/ConstraintScope.ts +32 -0
  19. package/testing/EventScenario.batches.spec.ts +2 -5
  20. package/testing/EventScenario.constraints.spec.ts +1 -4
  21. package/testing/EventScenario.fixtures.spec.ts +1 -1
  22. package/testing/EventScenario.ts +12 -0
  23. package/testing/InProcessConstraints.ts +29 -16
  24. package/testing/InProcessEventSequence.ts +35 -10
  25. package/testing/for_EventScenario/when_appending_concept_fields/concept_scenario_behaviors.fixture.ts +178 -0
  26. package/testing/for_EventScenario/when_appending_concept_fields/with_legacy_decorators.legacy.spec.ts +74 -0
  27. package/testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts +74 -0
  28. package/testing/for_EventScenario/when_appending_scoped_constraints/scope_scenario_behaviors.fixture.ts +139 -0
  29. package/testing/for_EventScenario/when_appending_scoped_constraints/with_legacy_decorators.legacy.spec.ts +57 -0
  30. package/testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts +56 -0
  31. package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_cycles.spec.ts +48 -0
  32. package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts +119 -0
  33. package/testing/for_ReactorScenario/when_enforcing_scoped_constraints/with_default_input_routes.spec.ts +70 -0
  34. package/testing/for_ReactorScenario/when_enforcing_scoped_constraints/with_routed_service_appends.spec.ts +65 -0
@@ -0,0 +1,119 @@
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 { chai, describe, it } from 'vitest';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import { constraint } from '../../../events/constraints/constraint.js';
9
+ import { unique } from '../../../events/constraints/unique.js';
10
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
11
+ import type { AppendOptions } from '../../../eventSequences/AppendOptions.js';
12
+ import { EventScenario, UnsupportedEventSequenceOperation } from '../../index.js';
13
+
14
+ chai.should();
15
+ @eventType('ScopedGuardClaim')
16
+ class Claim { @field(String) key: string; @field(String) other = 'Other'; constructor(key: string) { this.key = key; } }
17
+ @eventType('ScopedGuardFlag')
18
+ class Flag { @field(Boolean) key = true; }
19
+ @eventType('ScopedGuardRemoval')
20
+ class Removed { @field(String) label = 'release'; }
21
+ @eventType('ScopedGuardFieldless')
22
+ class Fieldless {}
23
+
24
+ function definition(configure: (builder: IConstraintBuilder) => void, name = 'ScopedGuard'): Function {
25
+ @constraint(name)
26
+ class Definition { define(builder: IConstraintBuilder) { configure(builder); } }
27
+ return Definition;
28
+ }
29
+ const scopedConstraint = definition(builder => builder.perEventStreamId().unique(value => value.on(Claim, event => event.key).removedWith(Removed)));
30
+ const create = () => new EventScenario({ artifacts: { eventTypes: [Claim, Removed], constraints: [scopedConstraint] } });
31
+ async function unsupported(action: () => unknown, reason: string): Promise<void> {
32
+ await Promise.resolve().then(action).then(() => { throw new Error('Unsupported operation succeeded'); }, error => {
33
+ (error instanceof UnsupportedEventSequenceOperation).should.be.true;
34
+ (error as Error).message.should.include(reason);
35
+ (error as Error).message.should.include('Use a kernel-backed test.');
36
+ });
37
+ }
38
+
39
+ describe('when configuring unproven scoped property shapes', () => {
40
+ for (const [name, configure, reason] of [
41
+ ['composite', (builder: IConstraintBuilder) => builder.unique(value => value.on(Claim, event => event.key, event => event.other)), 'Scoped composite or case-insensitive property keys'],
42
+ ['casing', (builder: IConstraintBuilder) => builder.unique(value => value.on(Claim, event => event.key).ignoreCasing()), 'Scoped composite or case-insensitive property keys'],
43
+ ['boolean', (builder: IConstraintBuilder) => builder.unique(value => value.on(Flag, event => event.key)), 'Scoped property keys must be schema-backed strings'],
44
+ ['covered removal', (builder: IConstraintBuilder) => builder.unique(value => value.on(Claim, event => event.key).removedWith(Claim)), 'Scoped covered-and-removal property events'],
45
+ ['fieldless removal', (builder: IConstraintBuilder) => builder.unique(value => value.on(Claim, event => event.key).removedWith(Fieldless)), 'Scoped fieldless removal events']
46
+ ] as const) {
47
+ it(`should reject ${name} with its specific boundary rather than a compiler failure`, async () => {
48
+ const eventTypes = name === 'fieldless removal' ? [Claim, Fieldless] : [Claim, Flag, Removed];
49
+ new EventScenario({ artifacts: { eventTypes, constraints: [definition(configure)] } }).should.be.instanceOf(EventScenario);
50
+ await unsupported(() => new EventScenario({ artifacts: { eventTypes,
51
+ constraints: [definition(builder => { builder.perEventSourceType(); configure(builder); })] } }), reason);
52
+ });
53
+ }
54
+
55
+ it('should reject a scoped definition alongside another nonoverlapping definition', async () => {
56
+ const other = definition(builder => builder.unique(value => value.on(Flag, event => event.key)), 'OtherGuard');
57
+ await unsupported(() => new EventScenario({ artifacts: { eventTypes: [Claim, Flag, Removed], constraints: [scopedConstraint, other] } }),
58
+ 'Scoped constraints alongside other definitions');
59
+ });
60
+
61
+ it('should retain the production compiler conflict between scoped fluent and unscoped unique decorators', async () => {
62
+ @eventType('ScopedDecoratedConflict')
63
+ class Decorated { @field(String) @unique('Conflict') key = 'key'; }
64
+ const scoped = definition(builder => builder.perEventStreamId().unique(value => value.on(Decorated, event => event.key)), 'Conflict');
65
+ await unsupported(() => new EventScenario({ artifacts: { eventTypes: [Decorated], constraints: [scoped] } }),
66
+ "Conflicting scopes for constraint 'Conflict'");
67
+ });
68
+ });
69
+
70
+ describe('when a scoped append uses unsupported routes or metadata', () => {
71
+ it('should preserve claims, history, results, notifications and sequence after unsupported input', async () => {
72
+ const scenario = create();
73
+ (await scenario.append('A', new Claim('Alpha'), { streamId: 'West' })).isSuccess.should.be.true;
74
+ const before = scenario.appendedEvents;
75
+ const results = scenario.results;
76
+ const notifications = scenario.eventLog.appendOperations[Symbol.asyncIterator]();
77
+ const pending = notifications.next();
78
+ try {
79
+ for (const dimension of ['sourceType', 'streamType', 'streamId'] as const) {
80
+ for (const value of [' ', 'a|estt:b', '*', 'a:b', 'é', 'a\u0000b']) {
81
+ const options = { [dimension]: value };
82
+ await unsupported(() => scenario.append('B', new Claim('Beta'), options), `append.${dimension}`);
83
+ await unsupported(() => scenario.appendMany('B', [new Claim('Beta')], options), `appendMany.${dimension}`);
84
+ }
85
+ }
86
+ for (const options of [{ tags: [] }, { subject: '' }, { occurred: new Date() }, { correlationId: '00000000-0000-0000-0000-000000000001' },
87
+ { concurrencyScopes: {} }, { eventSourceId: 'other' }, { arbitrary: true }]) {
88
+ await unsupported(() => scenario.append('B', new Claim('Beta'), options as AppendOptions), 'append.options');
89
+ }
90
+ await unsupported(() => scenario.appendMany([
91
+ { eventSourceId: 'A', event: new Removed(), eventStreamId: 'West' },
92
+ { eventSourceId: 'B', event: new Claim('Alpha'), eventStreamId: 'bad|route' }
93
+ ]), 'appendMany.streamId');
94
+ scenario.appendedEvents.should.deep.equal(before);
95
+ scenario.results.should.deep.equal(results);
96
+ (await scenario.eventLog.getNextSequenceNumber()).value.should.equal(1n);
97
+ const rejected = await scenario.append('B', new Claim('Alpha'), { streamId: 'West' });
98
+ rejected.isSuccess.should.be.false;
99
+ (await pending).value![0].result.should.equal(rejected);
100
+ (await scenario.append('B', new Claim('Beta'), { streamId: 'West' })).sequenceNumber.value.should.equal(1n);
101
+ } finally { await notifications.return?.(); }
102
+ });
103
+
104
+ it('should roll back default-route setup without disturbing another scope', async () => {
105
+ const scenario = create();
106
+ await scenario.append('A', new Claim('Alpha'), { streamId: 'West' });
107
+ await scenario.given.forEventSource('A').events(new Claim('DefaultKey'));
108
+ const before = scenario.appendedEvents;
109
+ const results = scenario.results;
110
+ await scenario.given.forEventSource('B').events(new Claim('Temporary'), new Claim('DefaultKey'))
111
+ .then(() => { throw new Error('Invalid setup succeeded'); }, error => {
112
+ (error as Error).message.should.include('EventScenario given setup failed');
113
+ });
114
+ scenario.appendedEvents.should.deep.equal(before);
115
+ scenario.results.should.deep.equal(results);
116
+ (await scenario.append('C', new Claim('Temporary'))).isSuccess.should.be.true;
117
+ (await scenario.append('C', new Claim('Alpha'), { streamId: 'West' })).isSuccess.should.be.false;
118
+ });
119
+ });
@@ -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 'reflect-metadata';
5
+ import { field } from '@cratis/fundamentals';
6
+ import { chai, describe, it } from 'vitest';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import { constraint } from '../../../events/constraints/constraint.js';
9
+ import { removeConstraint } from '../../../events/constraints/removeConstraint.js';
10
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
11
+ import type { EventContext } from '../../../events/EventContext.js';
12
+ import { reactor } from '../../../reactors/reactor.js';
13
+ import { ReactorScenario } from '../../ReactorScenario.js';
14
+
15
+ chai.should();
16
+ @eventType('ScopedReactorFirst')
17
+ class First { @field(String) label = 'First'; }
18
+ @eventType('ScopedReactorSibling')
19
+ class Sibling { @field(String) label = 'Sibling'; }
20
+ @eventType('ScopedReactorRemoved')
21
+ @removeConstraint('ScopedReactorCycle')
22
+ class Removed { @field(String) label = 'Removed'; }
23
+ @eventType('ScopedReactorExpired')
24
+ @removeConstraint('ScopedReactorCycle')
25
+ class Expired { @field(String) label = 'Expired'; }
26
+ @eventType('ScopedReactorEffect')
27
+ class Effect { @field(String) label: string; constructor(label: string) { this.label = label; } }
28
+ @constraint('ScopedReactorFirstCycle')
29
+ class FirstCycle {
30
+ define(builder: IConstraintBuilder) { builder.perEventSourceType().perEventStreamType().perEventStreamId().uniqueFor(First, 'Occupied', 'ScopedReactorCycle'); }
31
+ }
32
+ @constraint('ScopedReactorSiblingCycle')
33
+ class SiblingCycle {
34
+ define(builder: IConstraintBuilder) { builder.perEventSourceType().perEventStreamType().perEventStreamId().uniqueFor(Sibling, 'Occupied', 'ScopedReactorCycle'); }
35
+ }
36
+
37
+ describe('when enforcing scoped reactor constraints on default-route input', () => {
38
+ it('should deliver only accepted cycles and retain no effects or deliveries from rejected actions', async () => {
39
+ const calls: EventContext[] = [];
40
+ @reactor('ScopedReactor')
41
+ class Reactor {
42
+ first(event: First, context: EventContext) { calls.push(context); return new Effect(event.label); }
43
+ sibling(event: Sibling, context: EventContext) { calls.push(context); return new Effect(event.label); }
44
+ }
45
+ const scenario = new ReactorScenario(Reactor, { artifacts: {
46
+ eventTypes: [First, Sibling, Removed, Expired, Effect], constraints: [FirstCycle, SiblingCycle]
47
+ } });
48
+ await scenario.given.forEventSource('A').events(new First());
49
+ await scenario.when.forEventSource('A').events(new Sibling()).then(
50
+ () => { throw new Error('Conflicting cycle delivered'); }, error => {
51
+ (error as Error).message.should.include('ReactorScenario action append failed');
52
+ (error as Error).message.should.include('ScopedReactorCycle');
53
+ });
54
+ calls.length.should.equal(1);
55
+ scenario.produced.length.should.equal(1);
56
+ scenario.results.length.should.equal(1);
57
+ scenario.appendedEvents.length.should.equal(1);
58
+ await scenario.when.forEventSource('A').events(new Removed(), new Sibling());
59
+ await scenario.when.forEventSource('A').events(new Expired(), new First(), new Sibling()).then(
60
+ () => { throw new Error('Invalid batch delivered'); }, error => {
61
+ (error as Error).message.should.include('ReactorScenario action append failed');
62
+ });
63
+ calls.length.should.equal(2);
64
+ scenario.produced.length.should.equal(2);
65
+ scenario.results.length.should.equal(2);
66
+ scenario.appendedEvents.length.should.equal(3);
67
+ calls.map(context => [context.eventSourceType, context.eventStreamType, context.eventStreamId])
68
+ .should.deep.equal([['Default', 'All', 'Default'], ['Default', 'All', 'Default']]);
69
+ });
70
+ });
@@ -0,0 +1,65 @@
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 { chai, describe, it } 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 type { EventContext } from '../../../events/EventContext.js';
11
+ import { reactor } from '../../../reactors/reactor.js';
12
+ import type { ReactorServices } from '../../../reactors/ReactorServices.js';
13
+ import { ReactorScenario } from '../../ReactorScenario.js';
14
+ import { UnsupportedEventSequenceOperation } from '../../UnsupportedEventSequenceOperation.js';
15
+
16
+ chai.should();
17
+ @eventType('ScopedServiceRequested')
18
+ class Requested { @field(String) stream: string; constructor(stream: string) { this.stream = stream; } }
19
+ @eventType('ScopedServiceClaimed')
20
+ class Claimed { @field(String) key = 'Key'; }
21
+ @eventType('ScopedServiceReturned')
22
+ class Returned { @field(String) label = 'returned'; }
23
+ @constraint('ScopedServiceKey')
24
+ class Key { define(builder: IConstraintBuilder) { builder.perEventStreamId().unique(key => key.on(Claimed, event => event.key)); } }
25
+
26
+ describe('when a reactor explicitly appends into a constraint scope', () => {
27
+ it('should preserve routed service appends without routing or recursively delivering the input', async () => {
28
+ const successes: boolean[] = [];
29
+ @reactor('ScopedServiceWriter')
30
+ class Writer {
31
+ async requested(event: Requested, context: EventContext, services: ReactorServices) {
32
+ successes.push((await services.eventStore.eventLog.append(context.eventSourceId, new Claimed(), { streamId: event.stream })).isSuccess);
33
+ }
34
+ }
35
+ const scenario = new ReactorScenario(Writer, { artifacts: { eventTypes: [Requested, Claimed], constraints: [Key] } });
36
+ await scenario.when.forEventSource('A').events(new Requested('West'));
37
+ await scenario.when.forEventSource('B').events(new Requested('East'));
38
+ await scenario.when.forEventSource('C').events(new Requested('West'));
39
+ successes.should.deep.equal([true, true, false]);
40
+ scenario.appendedEvents.map(event => event.context.eventStreamId).should.deep.equal(['Default', 'West', 'Default', 'East', 'Default']);
41
+ scenario.results.length.should.equal(3);
42
+ scenario.produced.length.should.equal(0);
43
+ });
44
+
45
+ it('should fail a delivery that swallows an unsupported route and record no returned effect', async () => {
46
+ @reactor('ScopedServiceUnsupportedWriter')
47
+ class Writer {
48
+ async requested(_event: Requested, context: EventContext, services: ReactorServices) {
49
+ try { await services.eventStore.eventLog.append(context.eventSourceId, new Claimed(), { streamId: 'bad|route' }); }
50
+ catch { /* The harness must latch unsupported appends even if the handler swallows them. */ }
51
+ return new Returned();
52
+ }
53
+ }
54
+ const scenario = new ReactorScenario(Writer, { artifacts: { eventTypes: [Requested, Claimed, Returned], constraints: [Key] } });
55
+ await scenario.when.forEventSource('A').events(new Requested('West')).then(
56
+ () => { throw new Error('Unsupported route was swallowed'); }, error => {
57
+ (error instanceof UnsupportedEventSequenceOperation).should.be.true;
58
+ (error as Error).message.should.include('append.streamId');
59
+ });
60
+ scenario.appendedEvents.length.should.equal(1);
61
+ scenario.produced.length.should.equal(0);
62
+ scenario.sideEffects.length.should.equal(0);
63
+ scenario.results[0].completed.should.be.false;
64
+ });
65
+ });