@cratis/chronicle 6.41.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.
- package/compliance/for_PIIManager/against_a_kernel/when_erasing_a_subject.integration.spec.ts +4 -4
- package/compliance/resolveEventSubject.ts +19 -0
- package/compliance/subject.ts +2 -2
- package/dist/compliance/resolveEventSubject.d.ts +3 -0
- package/dist/compliance/resolveEventSubject.d.ts.map +1 -0
- package/dist/compliance/resolveEventSubject.js +21 -0
- package/dist/compliance/resolveEventSubject.js.map +1 -0
- package/dist/compliance/subject.d.ts +2 -2
- package/dist/compliance/subject.js +2 -2
- package/dist/eventSequences/AppendOptions.d.ts +1 -1
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +2 -2
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.js +2 -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/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +16 -6
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReadModelScenario.d.ts.map +1 -1
- package/dist/testing/ReadModelScenario.js +4 -0
- package/dist/testing/ReadModelScenario.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +1 -1
- package/dist/testing/singleAppendRoute.d.ts.map +1 -1
- package/dist/testing/singleAppendRoute.js +8 -4
- package/dist/testing/singleAppendRoute.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +1 -1
- package/eventSequences/EventForEventSourceId.ts +1 -1
- package/eventSequences/EventSequence.ts +2 -2
- package/eventSequences/for_EventSequence/when_appending/subject_behaviors.fixture.ts +239 -0
- package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.spec.ts +34 -0
- package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts +38 -0
- package/eventSequences/prepareBatchAppend.ts +2 -1
- package/eventSequences/prepareSingleAppend.ts +3 -1
- package/package.json +2 -2
- package/testing/EventScenario.batches.spec.ts +2 -2
- package/testing/EventScenario.fixtures.spec.ts +2 -2
- package/testing/EventScenario.rejections.spec.ts +2 -2
- package/testing/InProcessEventSequence.ts +17 -6
- package/testing/ReadModelScenario.spec.ts +31 -0
- package/testing/ReadModelScenario.ts +4 -0
- package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts +1 -1
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts +11 -1
- package/testing/singleAppendRoute.spec.ts +19 -0
- package/testing/singleAppendRoute.ts +9 -4
|
@@ -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
|
+
});
|
package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts
ADDED
|
@@ -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.41.
|
|
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:
|
|
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:
|
|
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
|
|
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({}, '
|
|
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
|
|
236
|
+
it('should reject non-string subject options', async () => {
|
|
237
237
|
const subject = scenario();
|
|
238
|
-
await rejects(() => subject.append('A', new Recorded(), { subject:
|
|
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 () => {
|
|
@@ -37,6 +37,11 @@ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOpe
|
|
|
37
37
|
// JS trim() omits U+0085, which the kernel trims. Reject unproven non-ASCII whitespace and controls.
|
|
38
38
|
const unprovenFilterCharacters = /[\u007f-\u009f]|(?=[^\x00-\x7f])\p{White_Space}/u;
|
|
39
39
|
|
|
40
|
+
function resolveStoredSubject(subject: string | undefined, eventSourceId: string): string {
|
|
41
|
+
// Match the kernel's string.IsNullOrWhiteSpace, including U+0085 but not U+FEFF.
|
|
42
|
+
return subject && !/^\p{White_Space}*$/u.test(subject) ? subject : eventSourceId;
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
function matchesConstrainedValue(value: unknown, content: unknown): boolean {
|
|
41
46
|
// Use production serialization for concepts, not their wrapper object's runtime type.
|
|
42
47
|
// Keep rejecting other unproven conversions, such as a Date assigned to a string field.
|
|
@@ -168,13 +173,13 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
168
173
|
try {
|
|
169
174
|
if (!event || typeof event !== 'object') throw this.unsupported('append.event', this.id.value, 'Only registered event instances are supported.');
|
|
170
175
|
if (!/^[A-Za-z0-9_-]+$/.test(eventSourceId)) throw this.unsupported('append.source', eventSourceId, 'Only simple source identifiers are fixture-backed.');
|
|
171
|
-
const route = singleAppendRoute(options, event.constructor.name, (operation, artifact, reason) => this.unsupported(operation, artifact, reason));
|
|
176
|
+
const route = singleAppendRoute(options, event.constructor.name, (operation, artifact, reason) => this.unsupported(operation, artifact, reason), true);
|
|
172
177
|
const metadata = this._catalog.get(event.constructor);
|
|
173
178
|
if (!metadata) throw this.unsupported('append.event', event.constructor.name, 'Event is not in the selected, validated catalog.');
|
|
174
179
|
let prepared: ReturnType<typeof prepareSingleAppend>;
|
|
175
180
|
let content: Record<string, unknown>;
|
|
176
181
|
try {
|
|
177
|
-
prepared = prepareSingleAppend(event,
|
|
182
|
+
prepared = prepareSingleAppend(event, { subject: options?.subject, correlationId: this._correlationId?.() });
|
|
178
183
|
content = this.checkedContent(event, prepared.content, 'append.content',
|
|
179
184
|
'Content differs from the event schema; null, missing and extra values are not proven.');
|
|
180
185
|
} catch (error) {
|
|
@@ -207,7 +212,7 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
207
212
|
eventStore: this._store, namespace: this._namespace,
|
|
208
213
|
sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
209
214
|
eventSourceType: route.sourceType, eventStreamType: route.streamType, eventStreamId: route.streamId,
|
|
210
|
-
subject: eventSourceId, hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
|
|
215
|
+
subject: resolveStoredSubject(prepared.subject, eventSourceId), hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
|
|
211
216
|
eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
|
|
212
217
|
causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
213
218
|
tags: prepared.tags.map(tag => new Tag(tag))
|
|
@@ -254,6 +259,9 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
254
259
|
}
|
|
255
260
|
const individual = entry as EventForEventSourceId;
|
|
256
261
|
if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
|
|
262
|
+
if (individual.subject !== undefined && individual.subject !== null && typeof individual.subject !== 'string') {
|
|
263
|
+
throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
|
|
264
|
+
}
|
|
257
265
|
const tags = individual.tags;
|
|
258
266
|
if (tags && (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string'))) {
|
|
259
267
|
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
@@ -288,8 +296,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
288
296
|
this.validateSource(eventSourceId, 'appendMany.source');
|
|
289
297
|
const wire = eventsToAppend[index];
|
|
290
298
|
for (const [name, value] of Object.entries({ sourceType: wire.EventSourceType, streamType: wire.EventStreamType,
|
|
291
|
-
streamId: wire.EventStreamId
|
|
292
|
-
const identifiers =
|
|
299
|
+
streamId: wire.EventStreamId })) {
|
|
300
|
+
const identifiers = /^[A-Za-z0-9_-]*$/;
|
|
293
301
|
if (value !== undefined && (typeof value !== 'string' || !identifiers.test(value))) {
|
|
294
302
|
throw this.unsupported(`appendMany.${name}`, String(value), 'Only simple metadata identifiers are fixture-backed.');
|
|
295
303
|
}
|
|
@@ -307,7 +315,7 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
307
315
|
const stored: AppendedEvent = { eventType, content, context: {
|
|
308
316
|
eventStore: this._store, namespace: this._namespace, sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
309
317
|
eventSourceType: wire.EventSourceType || 'Default', eventStreamType: wire.EventStreamType || 'All',
|
|
310
|
-
eventStreamId: wire.EventStreamId || 'Default', subject: wire.Subject, hash, causedBy: identity,
|
|
318
|
+
eventStreamId: wire.EventStreamId || 'Default', subject: resolveStoredSubject(wire.Subject, eventSourceId), hash, causedBy: identity,
|
|
311
319
|
observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
|
|
312
320
|
causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
313
321
|
tags: wire.Tags.map(tag => new Tag(tag))
|
|
@@ -408,6 +416,9 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
408
416
|
options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
|
|
409
417
|
throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
|
|
410
418
|
}
|
|
419
|
+
if (options.subject !== undefined && options.subject !== null && typeof options.subject !== 'string') {
|
|
420
|
+
throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
|
|
421
|
+
}
|
|
411
422
|
if (options.tags && (!Array.isArray(options.tags) || options.tags.some(tag => typeof tag !== 'string'))) {
|
|
412
423
|
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
413
424
|
}
|
|
@@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
5
5
|
import { ConceptAs, field, Guid } from '@cratis/fundamentals';
|
|
6
6
|
import type { Constructor } from '@cratis/fundamentals';
|
|
7
7
|
import { pii } from '../compliance/pii.js';
|
|
8
|
+
import { subject } from '../compliance/subject.js';
|
|
8
9
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
9
10
|
import type { EventContext } from '../events/EventContext.js';
|
|
10
11
|
import { fromEvent } from '../projections/modelBound/fromEvent.js';
|
|
@@ -219,6 +220,36 @@ describe('ReadModelScenario', () => {
|
|
|
219
220
|
expect(await scenario.instance).toMatchObject({ subject: 'source-a' });
|
|
220
221
|
});
|
|
221
222
|
|
|
223
|
+
it('maps a seeded event subject annotation to the projection Subject context', async () => {
|
|
224
|
+
class PersonalData {
|
|
225
|
+
@field(String) personId = 'person-a';
|
|
226
|
+
}
|
|
227
|
+
subject()(PersonalData.prototype, 'personId');
|
|
228
|
+
eventType('scenario-personal-data')(PersonalData);
|
|
229
|
+
class SubjectState { @field(String) id = ''; @field(String) subject = ''; }
|
|
230
|
+
fromEvent(PersonalData)(SubjectState);
|
|
231
|
+
setFromContext(PersonalData, 'subject')(SubjectState.prototype, 'subject');
|
|
232
|
+
const scenario = new ReadModelScenario(SubjectState, { ...artifacts, eventTypes: [PersonalData] });
|
|
233
|
+
scenario.given.forEventSource('source-a').events(new PersonalData());
|
|
234
|
+
expect(await scenario.instance).toMatchObject({ subject: 'person-a' });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it.each([undefined, null, '', ' \t\n', '\u0085\u2003'])('defaults an annotated subject %j to the event source', async value => {
|
|
238
|
+
class PersonalData {
|
|
239
|
+
// Keep null out of serialized content while exercising subject resolution.
|
|
240
|
+
get personId(): string | null | undefined { return value; }
|
|
241
|
+
}
|
|
242
|
+
field(String)(PersonalData.prototype, 'personId');
|
|
243
|
+
subject()(PersonalData.prototype, 'personId');
|
|
244
|
+
eventType('scenario-empty-subject')(PersonalData);
|
|
245
|
+
class SubjectState { @field(String) id = ''; @field(String) subject = ''; }
|
|
246
|
+
fromEvent(PersonalData)(SubjectState);
|
|
247
|
+
setFromContext(PersonalData, 'subject')(SubjectState.prototype, 'subject');
|
|
248
|
+
const scenario = new ReadModelScenario(SubjectState, { ...artifacts, eventTypes: [PersonalData] });
|
|
249
|
+
scenario.given.forEventSource('source-a').events(new PersonalData());
|
|
250
|
+
expect(await scenario.instance).toMatchObject({ subject: 'source-a' });
|
|
251
|
+
});
|
|
252
|
+
|
|
222
253
|
it('defaults a projection Hash and CausedBy mapping to kernel sentinels when the seed omits them', async () => {
|
|
223
254
|
class ContextDefaults { @field(String) id = ''; @field(String) hash = ''; @field(String) causedByName = ''; }
|
|
224
255
|
setFromContext(ItemAdded, 'hash')(ContextDefaults.prototype, 'hash');
|
|
@@ -7,6 +7,7 @@ import type { Constructor } from '@cratis/fundamentals';
|
|
|
7
7
|
import { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
|
|
8
8
|
import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
|
|
9
9
|
import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
10
|
+
import { resolveEventSubject } from '../compliance/resolveEventSubject.js';
|
|
10
11
|
import { getFromEventMetadata } from '../projections/modelBound/fromEvent.js';
|
|
11
12
|
import { getRemovedWithClassMetadata } from '../projections/modelBound/removedWith.js';
|
|
12
13
|
import { getRemovedWithJoinClassMetadata } from '../projections/modelBound/removedWithJoin.js';
|
|
@@ -138,11 +139,14 @@ export class ReadModelScenario<TReadModel extends object> {
|
|
|
138
139
|
if (!metadata) throw new Error(`Event '${event.constructor.name}' has no @eventType metadata.`);
|
|
139
140
|
// The kernel delivers JSON objects, not instances of decorated event classes.
|
|
140
141
|
const content: unknown = JSON.parse(JsonSerializer.serialize(event));
|
|
142
|
+
const subject = resolveEventSubject(event);
|
|
141
143
|
this._events.push({
|
|
142
144
|
sourceId: id,
|
|
143
145
|
content,
|
|
144
146
|
context: {
|
|
145
147
|
eventSourceId: id,
|
|
148
|
+
// Match the kernel's string.IsNullOrWhiteSpace, including U+0085 but not U+FEFF.
|
|
149
|
+
subject: subject && !/^\p{White_Space}*$/u.test(subject) ? subject : id,
|
|
146
150
|
eventSourceType: 'Default',
|
|
147
151
|
eventStreamType: 'All',
|
|
148
152
|
eventStreamId: 'Default',
|
package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts
CHANGED
|
@@ -83,7 +83,7 @@ describe('when a scoped append uses unsupported routes or metadata', () => {
|
|
|
83
83
|
await unsupported(() => scenario.appendMany('B', [new Claim('Beta')], options), `appendMany.${dimension}`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
for (const options of [{ tags: [] }, {
|
|
86
|
+
for (const options of [{ tags: [] }, { occurred: new Date() }, { correlationId: '00000000-0000-0000-0000-000000000001' },
|
|
87
87
|
{ concurrencyScopes: {} }, { eventSourceId: 'other' }, { arbitrary: true }]) {
|
|
88
88
|
await unsupported(() => scenario.append('B', new Claim('Beta'), options as AppendOptions), 'append.options');
|
|
89
89
|
}
|
package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts
CHANGED
|
@@ -25,7 +25,7 @@ describe('when read-model setup receives route options', () => {
|
|
|
25
25
|
it('should reuse single-append rejection messages without collecting unsupported input', async () => {
|
|
26
26
|
const scenario = create();
|
|
27
27
|
const events = createEvents();
|
|
28
|
-
for (const value of [{ sourceType: ' ' }, { streamType: null }, { streamId: 1 },
|
|
28
|
+
for (const value of [{ sourceType: ' ' }, { streamType: null }, { streamId: 1 }, null, []]) {
|
|
29
29
|
const options = value as AppendOptions;
|
|
30
30
|
const error = await rejection(() => events.append('A', new Claim('Other'), options));
|
|
31
31
|
(await rejection(() => scenario.given.forEventSource('A', options).events(new Claim('Other')))).message.should.equal(error.message);
|
|
@@ -33,6 +33,16 @@ describe('when read-model setup receives route options', () => {
|
|
|
33
33
|
((await scenario.instance) === null).should.be.true;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('should list only supported seed options without collecting unsupported input', async () => {
|
|
37
|
+
const scenario = create();
|
|
38
|
+
for (const value of [{ tags: [] }, { arbitrary: true }, { subject: 'person-a' }]) {
|
|
39
|
+
const options = value as AppendOptions;
|
|
40
|
+
(await rejection(() => scenario.given.forEventSource('A', options).events(new Claim('Other')))).message.should.equal(
|
|
41
|
+
'UnsupportedEventSequenceOperation: append.options (Claim): Only sourceType, streamType, streamId options are supported for single append. Use a kernel-backed test.');
|
|
42
|
+
}
|
|
43
|
+
((await scenario.instance) === null).should.be.true;
|
|
44
|
+
});
|
|
45
|
+
|
|
36
46
|
it('should retain the unproven routed read-model boundary instead of simulating projection routing', async () => {
|
|
37
47
|
const scenario = create();
|
|
38
48
|
(() => scenario.given.forEventSource('A', route).events(new Claim('Other'))).should.throw(UnsupportedProjectionOperation, 'routed events');
|
|
@@ -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 { singleAppendRoute } from './singleAppendRoute.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when single-append routing rejects unsupported options', () => {
|
|
10
|
+
it('should list only routing options when subject is not allowed', () => {
|
|
11
|
+
(() => singleAppendRoute({ subject: 'person-a' }, 'SeededEvent'))
|
|
12
|
+
.should.throw('Only sourceType, streamType, streamId options are supported for single append.');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should include subject in the allowed options for single append', () => {
|
|
16
|
+
(() => singleAppendRoute({ tags: [] }, 'AppendedEvent', undefined, true))
|
|
17
|
+
.should.throw('Only sourceType, streamType, streamId, subject options are supported for single append.');
|
|
18
|
+
});
|
|
19
|
+
});
|