@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.
- 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/EventScenario.js +1 -1
- package/dist/testing/EventScenario.js.map +1 -1
- package/dist/testing/EventScenarioGiven.d.ts +3 -1
- package/dist/testing/EventScenarioGiven.d.ts.map +1 -1
- package/dist/testing/EventScenarioGiven.js +4 -2
- package/dist/testing/EventScenarioGiven.js.map +1 -1
- package/dist/testing/EventScenarioGivenBuilder.d.ts +2 -1
- package/dist/testing/EventScenarioGivenBuilder.d.ts.map +1 -1
- package/dist/testing/EventScenarioGivenBuilder.js +1 -1
- package/dist/testing/EventScenarioGivenBuilder.js.map +1 -1
- package/dist/testing/EventScenarioSeed.d.ts +3 -2
- package/dist/testing/EventScenarioSeed.d.ts.map +1 -1
- package/dist/testing/EventScenarioSeed.js +2 -2
- package/dist/testing/EventScenarioSeed.js.map +1 -1
- package/dist/testing/EventScenarioWhen.d.ts +3 -1
- package/dist/testing/EventScenarioWhen.d.ts.map +1 -1
- package/dist/testing/EventScenarioWhen.js +5 -3
- package/dist/testing/EventScenarioWhen.js.map +1 -1
- package/dist/testing/EventScenarioWhenBuilder.d.ts +2 -1
- package/dist/testing/EventScenarioWhenBuilder.d.ts.map +1 -1
- package/dist/testing/EventScenarioWhenBuilder.js +1 -1
- package/dist/testing/EventScenarioWhenBuilder.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts +1 -2
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +19 -27
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts +3 -2
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +5 -5
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/ReadModelScenario.d.ts +2 -1
- package/dist/testing/ReadModelScenario.d.ts.map +1 -1
- package/dist/testing/ReadModelScenario.js +10 -1
- package/dist/testing/ReadModelScenario.js.map +1 -1
- package/dist/testing/ReadModelScenarioGiven.d.ts +3 -1
- package/dist/testing/ReadModelScenarioGiven.d.ts.map +1 -1
- package/dist/testing/ReadModelScenarioGiven.js +4 -2
- package/dist/testing/ReadModelScenarioGiven.js.map +1 -1
- package/dist/testing/ReadModelScenarioGivenBuilder.d.ts +2 -1
- package/dist/testing/ReadModelScenarioGivenBuilder.d.ts.map +1 -1
- package/dist/testing/ReadModelScenarioGivenBuilder.js +2 -2
- package/dist/testing/ReadModelScenarioGivenBuilder.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +9 -0
- package/dist/testing/singleAppendRoute.d.ts.map +1 -0
- package/dist/testing/singleAppendRoute.js +28 -0
- package/dist/testing/singleAppendRoute.js.map +1 -0
- 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/EventScenario.ts +1 -1
- package/testing/EventScenarioGiven.ts +3 -2
- package/testing/EventScenarioGivenBuilder.ts +2 -1
- package/testing/EventScenarioSeed.ts +5 -4
- package/testing/EventScenarioWhen.ts +4 -3
- package/testing/EventScenarioWhenBuilder.ts +2 -1
- package/testing/InProcessEventSequence.ts +20 -28
- package/testing/ReactorScenario.ts +7 -7
- package/testing/ReadModelScenario.spec.ts +31 -0
- package/testing/ReadModelScenario.ts +12 -1
- package/testing/ReadModelScenarioGiven.ts +4 -2
- package/testing/ReadModelScenarioGivenBuilder.ts +3 -2
- package/testing/for_EventScenario/given/routed_scenario.fixture.ts +39 -0
- package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts +1 -1
- package/testing/for_EventScenario/when_using_routed_builders/with_routing_options.spec.ts +116 -0
- package/testing/for_ReactorScenario/when_using_routed_builders/with_routing_options.spec.ts +88 -0
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts +52 -0
- package/testing/singleAppendRoute.spec.ts +19 -0
- 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
|
+
});
|
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.
|
|
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 () => {
|
package/testing/EventScenario.ts
CHANGED
|
@@ -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
|
}
|