@cratis/chronicle 6.40.0 → 6.41.1

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 (100) hide show
  1. package/compliance/for_PIIManager/against_a_kernel/when_erasing_a_subject.integration.spec.ts +4 -4
  2. package/compliance/resolveEventSubject.ts +19 -0
  3. package/compliance/subject.ts +2 -2
  4. package/dist/compliance/resolveEventSubject.d.ts +3 -0
  5. package/dist/compliance/resolveEventSubject.d.ts.map +1 -0
  6. package/dist/compliance/resolveEventSubject.js +21 -0
  7. package/dist/compliance/resolveEventSubject.js.map +1 -0
  8. package/dist/compliance/subject.d.ts +2 -2
  9. package/dist/compliance/subject.js +2 -2
  10. package/dist/eventSequences/AppendOptions.d.ts +1 -1
  11. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  12. package/dist/eventSequences/EventForEventSourceId.d.ts +1 -1
  13. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  14. package/dist/eventSequences/EventSequence.js +2 -2
  15. package/dist/eventSequences/EventSequence.js.map +1 -1
  16. package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
  17. package/dist/eventSequences/prepareBatchAppend.js +2 -1
  18. package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
  19. package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
  20. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
  21. package/dist/eventSequences/prepareSingleAppend.js +3 -1
  22. package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
  23. package/dist/testing/EventScenario.js +1 -1
  24. package/dist/testing/EventScenario.js.map +1 -1
  25. package/dist/testing/EventScenarioGiven.d.ts +3 -1
  26. package/dist/testing/EventScenarioGiven.d.ts.map +1 -1
  27. package/dist/testing/EventScenarioGiven.js +4 -2
  28. package/dist/testing/EventScenarioGiven.js.map +1 -1
  29. package/dist/testing/EventScenarioGivenBuilder.d.ts +2 -1
  30. package/dist/testing/EventScenarioGivenBuilder.d.ts.map +1 -1
  31. package/dist/testing/EventScenarioGivenBuilder.js +1 -1
  32. package/dist/testing/EventScenarioGivenBuilder.js.map +1 -1
  33. package/dist/testing/EventScenarioSeed.d.ts +3 -2
  34. package/dist/testing/EventScenarioSeed.d.ts.map +1 -1
  35. package/dist/testing/EventScenarioSeed.js +2 -2
  36. package/dist/testing/EventScenarioSeed.js.map +1 -1
  37. package/dist/testing/EventScenarioWhen.d.ts +3 -1
  38. package/dist/testing/EventScenarioWhen.d.ts.map +1 -1
  39. package/dist/testing/EventScenarioWhen.js +5 -3
  40. package/dist/testing/EventScenarioWhen.js.map +1 -1
  41. package/dist/testing/EventScenarioWhenBuilder.d.ts +2 -1
  42. package/dist/testing/EventScenarioWhenBuilder.d.ts.map +1 -1
  43. package/dist/testing/EventScenarioWhenBuilder.js +1 -1
  44. package/dist/testing/EventScenarioWhenBuilder.js.map +1 -1
  45. package/dist/testing/InProcessEventSequence.d.ts +1 -2
  46. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  47. package/dist/testing/InProcessEventSequence.js +19 -27
  48. package/dist/testing/InProcessEventSequence.js.map +1 -1
  49. package/dist/testing/ReactorScenario.d.ts +3 -2
  50. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  51. package/dist/testing/ReactorScenario.js +5 -5
  52. package/dist/testing/ReactorScenario.js.map +1 -1
  53. package/dist/testing/ReadModelScenario.d.ts +2 -1
  54. package/dist/testing/ReadModelScenario.d.ts.map +1 -1
  55. package/dist/testing/ReadModelScenario.js +10 -1
  56. package/dist/testing/ReadModelScenario.js.map +1 -1
  57. package/dist/testing/ReadModelScenarioGiven.d.ts +3 -1
  58. package/dist/testing/ReadModelScenarioGiven.d.ts.map +1 -1
  59. package/dist/testing/ReadModelScenarioGiven.js +4 -2
  60. package/dist/testing/ReadModelScenarioGiven.js.map +1 -1
  61. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts +2 -1
  62. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts.map +1 -1
  63. package/dist/testing/ReadModelScenarioGivenBuilder.js +2 -2
  64. package/dist/testing/ReadModelScenarioGivenBuilder.js.map +1 -1
  65. package/dist/testing/singleAppendRoute.d.ts +9 -0
  66. package/dist/testing/singleAppendRoute.d.ts.map +1 -0
  67. package/dist/testing/singleAppendRoute.js +28 -0
  68. package/dist/testing/singleAppendRoute.js.map +1 -0
  69. package/dist/tsconfig.tsbuildinfo +1 -1
  70. package/eventSequences/AppendOptions.ts +1 -1
  71. package/eventSequences/EventForEventSourceId.ts +1 -1
  72. package/eventSequences/EventSequence.ts +2 -2
  73. package/eventSequences/for_EventSequence/when_appending/subject_behaviors.fixture.ts +239 -0
  74. package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.spec.ts +34 -0
  75. package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts +38 -0
  76. package/eventSequences/prepareBatchAppend.ts +2 -1
  77. package/eventSequences/prepareSingleAppend.ts +3 -1
  78. package/package.json +2 -2
  79. package/testing/EventScenario.batches.spec.ts +2 -2
  80. package/testing/EventScenario.fixtures.spec.ts +2 -2
  81. package/testing/EventScenario.rejections.spec.ts +2 -2
  82. package/testing/EventScenario.ts +1 -1
  83. package/testing/EventScenarioGiven.ts +3 -2
  84. package/testing/EventScenarioGivenBuilder.ts +2 -1
  85. package/testing/EventScenarioSeed.ts +5 -4
  86. package/testing/EventScenarioWhen.ts +4 -3
  87. package/testing/EventScenarioWhenBuilder.ts +2 -1
  88. package/testing/InProcessEventSequence.ts +20 -28
  89. package/testing/ReactorScenario.ts +7 -7
  90. package/testing/ReadModelScenario.spec.ts +31 -0
  91. package/testing/ReadModelScenario.ts +12 -1
  92. package/testing/ReadModelScenarioGiven.ts +4 -2
  93. package/testing/ReadModelScenarioGivenBuilder.ts +3 -2
  94. package/testing/for_EventScenario/given/routed_scenario.fixture.ts +39 -0
  95. package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts +1 -1
  96. package/testing/for_EventScenario/when_using_routed_builders/with_routing_options.spec.ts +116 -0
  97. package/testing/for_ReactorScenario/when_using_routed_builders/with_routing_options.spec.ts +88 -0
  98. package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts +52 -0
  99. package/testing/singleAppendRoute.spec.ts +19 -0
  100. package/testing/singleAppendRoute.ts +33 -0
@@ -0,0 +1,239 @@
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 { resolveEventSubject } from '../../../compliance/resolveEventSubject.js';
5
+ import { Guid, type Constructor } from '@cratis/fundamentals';
6
+ import { beforeEach, chai, describe, it, vi, type Assertion } from 'vitest';
7
+ import type { ChronicleConnection } from '../../../connection/index.js';
8
+ import { EventSequence } from '../../EventSequence.js';
9
+ import { EventSequenceId } from '../../EventSequenceId.js';
10
+ import { EventScenario } from '../../../testing/EventScenario.js';
11
+ import { UnitOfWork } from '../../../transactions/UnitOfWork.js';
12
+ import type { IUnitOfWorkManager } from '../../../transactions/IUnitOfWorkManager.js';
13
+ import type { IEventStore } from '../../../IEventStore.js';
14
+
15
+ const should = chai.should();
16
+ function check(value: unknown): Assertion { return (value as { should: Assertion }).should; }
17
+
18
+ export function subjectBehaviors(fixtures: {
19
+ types: Constructor[];
20
+ annotated: (value: string) => object;
21
+ inherited: (value: string) => object;
22
+ concept: (value: string) => object;
23
+ unannotated: () => object;
24
+ }): void {
25
+ function connected() {
26
+ const append = vi.fn().mockResolvedValue({ Response: { SequenceNumber: 0n } });
27
+ const appendManyForEventSources = vi.fn().mockImplementation(async (request: { Events: object[] }) => ({
28
+ Response: { SequenceNumbers: request.Events.map((_, index) => BigInt(index)) }
29
+ }));
30
+ const manager = {} as IUnitOfWorkManager;
31
+ const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'Default',
32
+ { eventSequences: { append, appendManyForEventSources } } as unknown as ChronicleConnection, manager);
33
+ const unitOfWork = new UnitOfWork(Guid.create(), () => {},
34
+ { getEventSequence: () => sequence } as unknown as IEventStore);
35
+ Object.assign(manager, { current: unitOfWork });
36
+ return { sequence, unitOfWork, append, appendManyForEventSources };
37
+ }
38
+ function scenario() {
39
+ return new EventScenario({ artifacts: { eventTypes: fixtures.types, constraints: [] }, constraints: 'disabled' });
40
+ }
41
+
42
+ const cases = [
43
+ { name: 'an annotated subject', event: () => fixtures.annotated('person'), expected: 'person', stored: 'person' },
44
+ { name: 'an inherited annotated subject', event: () => fixtures.inherited('person'), expected: 'person', stored: 'person' },
45
+ { name: 'a concept subject', event: () => fixtures.concept('person'), expected: 'person', stored: 'person' },
46
+ { name: 'an empty annotated string', event: () => fixtures.annotated(''), expected: '', stored: 'source' },
47
+ { name: 'a whitespace annotated string', event: () => fixtures.annotated(' '), expected: ' ', stored: 'source' },
48
+ { name: 'an empty concept subject', event: () => fixtures.concept(''), expected: 'source', stored: 'source' },
49
+ { name: 'no annotation despite an id property', event: fixtures.unannotated, expected: 'source', stored: 'source' }
50
+ ];
51
+ for (const testCase of cases) {
52
+ describe(`when appending with ${testCase.name}`, () => {
53
+ let wireSubjects: string[];
54
+ let scenarioSubjects: (string | undefined)[];
55
+ beforeEach(async () => {
56
+ const client = connected();
57
+ const memory = scenario();
58
+ await client.sequence.append('source', testCase.event());
59
+ await memory.append('source', testCase.event());
60
+ await client.sequence.appendMany('source', [testCase.event()]);
61
+ await memory.appendMany('source', [testCase.event()]);
62
+ await client.sequence.appendMany([{ eventSourceId: 'source', event: testCase.event() }]);
63
+ await memory.appendMany([{ eventSourceId: 'source', event: testCase.event() }]);
64
+ wireSubjects = [client.append.mock.calls[0][0].Subject,
65
+ ...client.appendManyForEventSources.mock.calls.map(call => call[0].Events[0].Subject)];
66
+ scenarioSubjects = memory.appendedEvents.map(event => event.context.subject);
67
+ });
68
+ it('should select the wire subject using .NET value semantics in every append overload', () => {
69
+ check(wireSubjects).deep.equal([testCase.expected, testCase.expected, testCase.expected]);
70
+ });
71
+ it('should record the kernel subject in every scenario append overload', () => {
72
+ check(scenarioSubjects).deep.equal([testCase.stored, testCase.stored, testCase.stored]);
73
+ });
74
+ });
75
+ }
76
+ for (const testCase of [
77
+ { value: undefined, expected: 'source' },
78
+ { value: 0, expected: '0' },
79
+ { value: false, expected: 'false' },
80
+ { value: Guid.as('00000000-0000-0000-0000-000000000001'), expected: '00000000-0000-0000-0000-000000000001' },
81
+ { value: { toString: () => '' }, expected: 'source' },
82
+ { value: { toString: () => null }, expected: 'source' }
83
+ ]) {
84
+ describe(`when an annotated property contains ${String(testCase.value)}`, () => {
85
+ let subjects: string[];
86
+ beforeEach(async () => {
87
+ const client = connected();
88
+ const event = fixtures.annotated('person');
89
+ Reflect.set(event, 'personId', testCase.value);
90
+ await client.sequence.append('source', event);
91
+ await client.sequence.appendMany('source', [event]);
92
+ await client.sequence.appendMany([{ eventSourceId: 'source', event }]);
93
+ subjects = [client.append.mock.calls[0][0].Subject,
94
+ ...client.appendManyForEventSources.mock.calls.map(call => call[0].Events[0].Subject)];
95
+ });
96
+ it('should preserve .NET conversion and null fallback in every append overload', () => {
97
+ check(subjects).deep.equal([testCase.expected, testCase.expected, testCase.expected]);
98
+ });
99
+ });
100
+ }
101
+ describe('when resolving a null annotated property before serialization', () => {
102
+ let selected: string | undefined;
103
+ beforeEach(() => {
104
+ const event = fixtures.annotated('person');
105
+ Reflect.set(event, 'personId', null);
106
+ selected = resolveEventSubject(event);
107
+ });
108
+ it('should leave the subject unresolved for source fallback', () => should.equal(selected, undefined));
109
+ });
110
+ for (const { subject, stored } of [
111
+ { subject: 'explicit', stored: 'explicit' },
112
+ { subject: '', stored: 'source' },
113
+ { subject: ' ', stored: 'source' },
114
+ { subject: '\t\r\n', stored: 'source' },
115
+ { subject: '\u0085\u00a0', stored: 'source' },
116
+ { subject: '\ufeff', stored: '\ufeff' },
117
+ { subject: ' person ', stored: ' person ' }
118
+ ]) {
119
+ describe(`when overriding the annotation with ${JSON.stringify(subject)}`, () => {
120
+ let wireSubject: string;
121
+ let scenarioSubject: string | undefined;
122
+ beforeEach(async () => {
123
+ const client = connected();
124
+ const memory = scenario();
125
+ await client.sequence.append('source', fixtures.annotated('person'), { subject });
126
+ await memory.append('source', fixtures.annotated('person'), { subject });
127
+ wireSubject = client.append.mock.calls[0][0].Subject;
128
+ scenarioSubject = memory.appendedEvents[0].context.subject;
129
+ });
130
+ it('should prefer the explicit subject', () => should.equal(wireSubject, subject));
131
+ it('should record the kernel subject in the scenario', () => should.equal(scenarioSubject, stored));
132
+ });
133
+ }
134
+ for (const overload of ['single source', 'multiple sources'] as const) {
135
+ for (const subject of [undefined, 'shared', '', ' ']) {
136
+ describe(`when appending a ${overload} batch with shared subject ${JSON.stringify(subject)}`, () => {
137
+ let wireSubjects: string[];
138
+ let scenarioSubjects: (string | undefined)[];
139
+ beforeEach(async () => {
140
+ const client = connected();
141
+ const memory = scenario();
142
+ const events = [fixtures.annotated('person'), fixtures.concept('other'), fixtures.unannotated()];
143
+ if (overload === 'single source') {
144
+ await client.sequence.appendMany('source', events, { subject });
145
+ await memory.appendMany('source', events, { subject });
146
+ } else {
147
+ const entries = events.map((event, index) => ({ eventSourceId: 'source', event, subject: index === 0 ? 'entry' : undefined }));
148
+ await client.sequence.appendMany(entries, { subject });
149
+ await memory.appendMany(entries, { subject });
150
+ }
151
+ wireSubjects = client.appendManyForEventSources.mock.calls[0][0].Events.map((entry: { Subject: string }) => entry.Subject);
152
+ scenarioSubjects = memory.appendedEvents.map(event => event.context.subject);
153
+ });
154
+ it('should prefer entry then shared then annotation then source', () => {
155
+ check(wireSubjects).deep.equal([overload === 'multiple sources' ? 'entry' : subject ?? 'person', subject ?? 'other', subject ?? 'source']);
156
+ });
157
+ it('should record the kernel subjects in the scenario', () => {
158
+ const stored = subject === '' || subject === ' ' ? 'source' : subject;
159
+ check(scenarioSubjects).deep.equal([overload === 'multiple sources' ? 'entry' : stored ?? 'person',
160
+ stored ?? 'other', stored ?? 'source']);
161
+ });
162
+ });
163
+ }
164
+ }
165
+ for (const subject of ['', ' ', '\t\r\n', '\u0085\u00a0']) {
166
+ describe(`when an entry has an explicit blank subject ${JSON.stringify(subject)}`, () => {
167
+ let wireSubjects: string[];
168
+ let scenarioSubjects: (string | undefined)[];
169
+ beforeEach(async () => {
170
+ const client = connected();
171
+ const memory = scenario();
172
+ const entries = [{ eventSourceId: 'source', event: fixtures.annotated('person'), subject }];
173
+ await client.sequence.appendMany(entries, { subject: 'shared' });
174
+ await memory.appendMany(entries, { subject: 'shared' });
175
+ wireSubjects = client.appendManyForEventSources.mock.calls[0][0].Events.map((entry: { Subject: string }) => entry.Subject);
176
+ scenarioSubjects = memory.appendedEvents.map(event => event.context.subject);
177
+ });
178
+ it('should not replace the blank entry subject on the wire', () => check(wireSubjects).deep.equal([subject]));
179
+ it('should record the event source subject in the scenario', () => check(scenarioSubjects).deep.equal(['source']));
180
+ });
181
+ }
182
+ for (const testCase of cases) {
183
+ describe(`when using routed scenario builders with ${testCase.name}`, () => {
184
+ let memory: EventScenario;
185
+ const route = { sourceType: 'people', streamType: 'personal', streamId: 'details' };
186
+ beforeEach(async () => {
187
+ memory = scenario();
188
+ await memory.given.forEventSource('source', route).events(testCase.event());
189
+ await memory.when.forEventSource('source', route).event(testCase.event());
190
+ await memory.when.forEventSource('source', route).events(testCase.event());
191
+ });
192
+ it('should record the kernel subject for seeds, single appends and batch appends', () => {
193
+ check(memory.appendedEvents.map(event => event.context.subject)).deep.equal(Array(3).fill(testCase.stored));
194
+ });
195
+ it('should retain the selected route on every append path', () => {
196
+ check(memory.appendedEvents.map(event => ({ sourceType: event.context.eventSourceType,
197
+ streamType: event.context.eventStreamType, streamId: event.context.eventStreamId }))).deep.equal(Array(3).fill(route));
198
+ });
199
+ });
200
+ }
201
+ for (const subject of ['explicit', '', ' ']) {
202
+ describe(`when routed scenario builders override an annotation with ${JSON.stringify(subject)}`, () => {
203
+ let subjects: (string | undefined)[];
204
+ beforeEach(async () => {
205
+ const memory = scenario();
206
+ const options = { sourceType: 'people', streamType: 'personal', streamId: 'details', subject };
207
+ await memory.given.forEventSource('source', options).events(fixtures.annotated('person'));
208
+ await memory.when.forEventSource('source', options).event(fixtures.annotated('person'));
209
+ await memory.when.forEventSource('source', options).events(fixtures.annotated('person'));
210
+ subjects = memory.appendedEvents.map(event => event.context.subject);
211
+ });
212
+ it('should prefer the call subject and apply the kernel fallback on every path', () => {
213
+ check(subjects).deep.equal(Array(3).fill(subject.trim() ? subject : 'source'));
214
+ });
215
+ });
216
+ }
217
+ describe('when committing transactional single and batch appends', () => {
218
+ let subjects: string[];
219
+ beforeEach(async () => {
220
+ const client = connected();
221
+ await client.sequence.transactional.append('source', fixtures.annotated('person'));
222
+ await client.sequence.transactional.appendMany('source', [fixtures.concept('other'), fixtures.unannotated()]);
223
+ await client.unitOfWork.commit();
224
+ subjects = client.appendManyForEventSources.mock.calls[0][0].Events.map((entry: { Subject: string }) => entry.Subject);
225
+ });
226
+ it('should resolve each buffered event\'s annotation when the unit of work commits', () => check(subjects).deep.equal(['person', 'other', 'source']));
227
+ });
228
+ for (const testCase of cases) {
229
+ describe(`when seeding a scenario with ${testCase.name}`, () => {
230
+ let subject: string | undefined;
231
+ beforeEach(async () => {
232
+ const memory = scenario();
233
+ await memory.given.forEventSource('source').events(testCase.event());
234
+ subject = memory.appendedEvents[0].context.subject;
235
+ });
236
+ it('should record the kernel subject through the append boundary', () => should.equal(subject, testCase.stored));
237
+ });
238
+ }
239
+ }
@@ -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 { ConceptAs, field } from '@cratis/fundamentals';
5
+ import { describe } from 'vitest';
6
+ import { subject } from '../../../compliance/subject.js';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import { subjectBehaviors } from './subject_behaviors.fixture.js';
9
+
10
+ class PersonId extends ConceptAs<string> { static readonly valueType = String; }
11
+ class PersonalData { constructor(readonly personId: string) {} }
12
+ class DerivedPersonalData extends PersonalData {}
13
+ class ConceptPersonalData { constructor(readonly personId: PersonId) {} }
14
+ class UnannotatedData { id = 'not-the-subject'; }
15
+
16
+ subject()(PersonalData.prototype, 'personId');
17
+ field(String)(PersonalData.prototype, 'personId');
18
+ eventType('subject-legacy-string')(PersonalData);
19
+ eventType('subject-legacy-derived')(DerivedPersonalData);
20
+ subject()(ConceptPersonalData.prototype, 'personId');
21
+ field(PersonId)(ConceptPersonalData.prototype, 'personId');
22
+ eventType('subject-legacy-concept')(ConceptPersonalData);
23
+ field(String)(UnannotatedData.prototype, 'id');
24
+ eventType('subject-legacy-unannotated')(UnannotatedData);
25
+
26
+ describe('when appending subject annotations with legacy decorators', () => {
27
+ subjectBehaviors({
28
+ types: [PersonalData, DerivedPersonalData, ConceptPersonalData, UnannotatedData],
29
+ annotated: value => new PersonalData(value),
30
+ inherited: value => new DerivedPersonalData(value),
31
+ concept: value => new ConceptPersonalData(new PersonId(value)),
32
+ unannotated: () => new UnannotatedData()
33
+ });
34
+ });
@@ -0,0 +1,38 @@
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 { ConceptAs, field } from '@cratis/fundamentals';
5
+ import { describe } from 'vitest';
6
+ import { subject } from '../../../compliance/subject.js';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import { subjectBehaviors } from './subject_behaviors.fixture.js';
9
+
10
+ class PersonId extends ConceptAs<string> { static readonly valueType = String; }
11
+
12
+ @eventType('subject-standard-string')
13
+ class PersonalData {
14
+ @subject() @field(String) personId: string;
15
+ constructor(personId: string) { this.personId = personId; }
16
+ }
17
+
18
+ @eventType('subject-standard-derived')
19
+ class DerivedPersonalData extends PersonalData {}
20
+
21
+ @eventType('subject-standard-concept')
22
+ class ConceptPersonalData {
23
+ @subject() @field(PersonId) personId: PersonId;
24
+ constructor(personId: string) { this.personId = new PersonId(personId); }
25
+ }
26
+
27
+ @eventType('subject-standard-unannotated')
28
+ class UnannotatedData { @field(String) id = 'not-the-subject'; }
29
+
30
+ describe('when appending subject annotations with standard decorators', () => {
31
+ subjectBehaviors({
32
+ types: [PersonalData, DerivedPersonalData, ConceptPersonalData, UnannotatedData],
33
+ annotated: value => new PersonalData(value),
34
+ inherited: value => new DerivedPersonalData(value),
35
+ concept: value => new ConceptPersonalData(value),
36
+ unannotated: () => new UnannotatedData()
37
+ });
38
+ });
@@ -8,6 +8,7 @@ import { getEventTypeFor } from '../events/eventTypeDecorator.js';
8
8
  import { getTagsFor } from '../events/tagDecorator.js';
9
9
  import { mergeTags } from '../events/mergeTags.js';
10
10
  import { identityProvider } from '../identity/index.js';
11
+ import { resolveEventSubject } from '../compliance/resolveEventSubject.js';
11
12
  import type { AppendOptions } from './AppendOptions.js';
12
13
  import type { EventForEventSourceId } from './EventForEventSourceId.js';
13
14
  import type { ConcurrencyScope } from './ConcurrencyScope.js';
@@ -56,7 +57,7 @@ export function prepareBatchAppend(
56
57
  EventType: { Id: eventType.id.value, Generation: eventType.generation.value, Tombstone: eventType.tombstone },
57
58
  Content: JsonSerializer.serialize(event), Tags: tags,
58
59
  Occurred: occurrenceTime === undefined ? undefined : { Value: occurrenceTime.toISOString() },
59
- Subject: subject ?? appendOptions?.subject ?? eventSourceId
60
+ Subject: subject ?? appendOptions?.subject ?? resolveEventSubject(event) ?? eventSourceId
60
61
  };
61
62
  });
62
63
  return { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend };
@@ -8,6 +8,7 @@ import { mergeTags } from '../events/mergeTags.js';
8
8
  import { identityProvider } from '../identity/index.js';
9
9
  import { causationManager, CausationType } from '../auditing/index.js';
10
10
  import { correlationIdManager } from '../correlation/index.js';
11
+ import { resolveEventSubject } from '../compliance/resolveEventSubject.js';
11
12
  import type { AppendOptions } from './AppendOptions.js';
12
13
 
13
14
  /** The production client and in-process scenario share the single-append serialization boundary. */
@@ -16,10 +17,11 @@ export function prepareSingleAppend(event: object, options?: AppendOptions) {
16
17
  const correlationId = options?.correlationId === undefined
17
18
  ? Guid.as(correlationIdManager.current.value)
18
19
  : Guid.as(options.correlationId);
20
+ const subject = options?.subject ?? resolveEventSubject(event);
19
21
  const content = JsonSerializer.serialize(event);
20
22
  const tags = mergeTags(getTagsFor(event.constructor as Function), options?.tags);
21
23
  const causationChain = causationManager.run(CausationType.appendEvent, { eventType: eventType.id.value },
22
24
  () => causationManager.getCurrentChain());
23
25
  const identity = identityProvider.getCurrent();
24
- return { eventType, correlationId, content, tags, causationChain, identity };
26
+ return { eventType, correlationId, content, tags, causationChain, identity, subject };
25
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.40.0",
3
+ "version": "6.41.1",
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,7 +173,7 @@
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/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",
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/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",
177
177
  "test:native": "node ./scripts/check-native-esm.mjs"
178
178
  },
179
179
  "dependencies": {
@@ -265,7 +265,7 @@ describe('when appending batches against committed kernel fixtures', () => {
265
265
  error => { (error instanceof UnsupportedEventSequenceOperation).should.be.true; }
266
266
  );
267
267
  await first;
268
- for (const options of [{ subject: 'bad subject' }, { tags: ['invalid|tag'] },
268
+ for (const options of [{ subject: 42 as unknown as string }, { tags: ['invalid|tag'] },
269
269
  { concurrencyScope: {} as never }]) {
270
270
  await scenario.appendMany('B', [new OracleEventRecorded('second', false)], options).then(
271
271
  () => { throw new Error('Unproven metadata accepted'); },
@@ -450,7 +450,7 @@ describe('when appending batches against committed kernel fixtures', () => {
450
450
  ['appendMany.correlationId', () => scenario.appendMany('B', [event()], { correlationId: 'bad-guid' })],
451
451
  ['appendMany.entry', () => scenario.appendMany([entry({ unknown: 'value' })])],
452
452
  ['appendMany.sourceType', () => scenario.appendMany([entry({ eventSourceType: 'bad source' })])],
453
- ['appendMany.subject', () => scenario.appendMany([entry({ subject: '' })], { subject: 'shared' })],
453
+ ['appendMany.subject', () => scenario.appendMany([entry({ subject: 42 })], { subject: 'shared' })],
454
454
  ['appendMany.streamType', () => scenario.appendMany([entry({ eventStreamType: 'bad stream' })])],
455
455
  ['appendMany.streamId', () => scenario.appendMany([entry({ eventStreamId: 'bad stream' })])],
456
456
  ['appendMany.event', () => scenario.appendMany('B', [new UnregisteredRecorded()])],
@@ -219,11 +219,11 @@ describe('when appending against committed kernel event fixtures', () => {
219
219
  it('should reject prototype getters and non-enumerable append metadata without mutation', async () => {
220
220
  const scenario = makeScenario();
221
221
  class OptionsWithGetters {
222
- get subject() { return 'different-subject'; }
222
+ get occurred() { return new Date(); }
223
223
  get streamId() { return 'different-stream'; }
224
224
  }
225
225
  await unsupportedAsync(() => scenario.append('A', new OracleEventRecorded('one', true), new OptionsWithGetters()), 'append.options');
226
- const hidden = Object.defineProperty({}, 'subject', { value: 'different-subject', enumerable: false });
226
+ const hidden = Object.defineProperty({}, 'occurred', { value: new Date(), enumerable: false });
227
227
  await unsupportedAsync(() => scenario.append('A', new OracleEventRecorded('one', true), hidden), 'append.options');
228
228
  scenario.appendedEvents.length.should.equal(0);
229
229
  scenario.results.length.should.equal(0);
@@ -233,9 +233,9 @@ describe('when the fixture-bounded event sequence encounters unproven operations
233
233
  await rejects(() => subject.append('A', new Recorded(), { correlationId: '00000000-0000-0000-0000-000000000001' }), 'append.options');
234
234
  clean(subject);
235
235
  });
236
- it('should reject plain subject options', async () => {
236
+ it('should reject non-string subject options', async () => {
237
237
  const subject = scenario();
238
- await rejects(() => subject.append('A', new Recorded(), { subject: 'other' }), 'append.options');
238
+ await rejects(() => subject.append('A', new Recorded(), { subject: 42 as unknown as string }), 'append.subject');
239
239
  clean(subject);
240
240
  });
241
241
  it('should reject empty source/type-read event types', async () => {
@@ -149,7 +149,7 @@ export class EventScenario {
149
149
  }
150
150
  const sequence = new InProcessEventSequence(options, eventTypes, constraints);
151
151
  this.eventSequence = sequence;
152
- registerScenarioSeed(this, (source, events) => sequence.seed(source, events));
152
+ registerScenarioSeed(this, (source, events, options) => sequence.seed(source, events, options));
153
153
  this.given = new EventScenarioGivenBuilder(this);
154
154
  this.when = new EventScenarioWhenBuilder(this);
155
155
  }
@@ -1,13 +1,14 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
4
5
  import type { EventScenario } from './EventScenario.js';
5
6
  import { seedScenario } from './EventScenarioSeed.js';
6
7
 
7
8
  /** Seeds events in order through individual single appends, as in the .NET given builder. */
8
9
  export class EventScenarioGiven {
9
- constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
10
+ constructor(private readonly _scenario: EventScenario, private readonly _id: string, private readonly _options?: AppendOptions) {}
10
11
  async events(...events: object[]): Promise<void> {
11
- await seedScenario(this._scenario, this._id, events);
12
+ await seedScenario(this._scenario, this._id, events, this._options);
12
13
  }
13
14
  }
@@ -1,11 +1,12 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
4
5
  import type { EventScenario } from './EventScenario.js';
5
6
  import { EventScenarioGiven } from './EventScenarioGiven.js';
6
7
 
7
8
  /** Selects the source for setup events. */
8
9
  export class EventScenarioGivenBuilder {
9
10
  constructor(private readonly _scenario: EventScenario) {}
10
- forEventSource(id: string): EventScenarioGiven { return new EventScenarioGiven(this._scenario, id); }
11
+ forEventSource(id: string, options?: AppendOptions): EventScenarioGiven { return new EventScenarioGiven(this._scenario, id, options); }
11
12
  }
@@ -1,17 +1,18 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
4
5
  import type { EventScenario } from './EventScenario.js';
5
6
 
6
- const seeders = new WeakMap<EventScenario, (source: string, events: object[]) => Promise<void>>();
7
+ const seeders = new WeakMap<EventScenario, (source: string, events: object[], options?: AppendOptions) => Promise<void>>();
7
8
 
8
9
  /** Internal builder wiring; not exported by the testing package. */
9
- export function registerScenarioSeed(scenario: EventScenario, seed: (source: string, events: object[]) => Promise<void>): void {
10
+ export function registerScenarioSeed(scenario: EventScenario, seed: (source: string, events: object[], options?: AppendOptions) => Promise<void>): void {
10
11
  seeders.set(scenario, seed);
11
12
  }
12
13
 
13
- export function seedScenario(scenario: EventScenario, source: string, events: object[]): Promise<void> {
14
+ export function seedScenario(scenario: EventScenario, source: string, events: object[], options?: AppendOptions): Promise<void> {
14
15
  const seed = seeders.get(scenario);
15
16
  if (!seed) throw new Error('EventScenario setup requires a constructed scenario.');
16
- return seed(source, events);
17
+ return seed(source, events, options);
17
18
  }
@@ -1,14 +1,15 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
4
5
  import type { AppendResult } from '../eventSequences/AppendResult.js';
5
6
  import type { EventScenario } from './EventScenario.js';
6
7
 
7
8
  /** A single act-phase append; the plural form is reserved for atomic batches. */
8
9
  export class EventScenarioWhen {
9
- constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
10
- async event(event: object): Promise<AppendResult> { return this._scenario.append(this._id, event); }
10
+ constructor(private readonly _scenario: EventScenario, private readonly _id: string, private readonly _options?: AppendOptions) {}
11
+ async event(event: object): Promise<AppendResult> { return this._scenario.append(this._id, event, this._options); }
11
12
  async events(...events: object[]): Promise<readonly AppendResult[]> {
12
- return this._scenario.appendMany(this._id, events);
13
+ return this._scenario.appendMany(this._id, events, this._options);
13
14
  }
14
15
  }
@@ -1,11 +1,12 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
4
5
  import type { EventScenario } from './EventScenario.js';
5
6
  import { EventScenarioWhen } from './EventScenarioWhen.js';
6
7
 
7
8
  /** Selects the source for an act-phase append. */
8
9
  export class EventScenarioWhenBuilder {
9
10
  constructor(private readonly _scenario: EventScenario) {}
10
- forEventSource(id: string): EventScenarioWhen { return new EventScenarioWhen(this._scenario, id); }
11
+ forEventSource(id: string, options?: AppendOptions): EventScenarioWhen { return new EventScenarioWhen(this._scenario, id, options); }
11
12
  }