@cratis/chronicle 6.41.0 → 6.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/projections/captureProjectionProvenance.d.ts.map +1 -1
- package/dist/projections/captureProjectionProvenance.js +2 -0
- package/dist/projections/captureProjectionProvenance.js.map +1 -1
- package/dist/projections/declarative/FromAllBuilder.d.ts +18 -0
- package/dist/projections/declarative/FromAllBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/FromAllBuilder.js +36 -0
- package/dist/projections/declarative/FromAllBuilder.js.map +1 -0
- package/dist/projections/declarative/IFromAllBuilder.d.ts +33 -0
- package/dist/projections/declarative/IFromAllBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/IFromAllBuilder.js +4 -0
- package/dist/projections/declarative/IFromAllBuilder.js.map +1 -0
- package/dist/projections/declarative/IProjectionBuilderFor.d.ts +8 -0
- package/dist/projections/declarative/IProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts +4 -0
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.js +15 -0
- package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
- package/dist/projections/declarative/index.d.ts +2 -0
- package/dist/projections/declarative/index.d.ts.map +1 -1
- package/dist/projections/declarative/index.js +1 -0
- package/dist/projections/declarative/index.js.map +1 -1
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts +6 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts.map +1 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js +12 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +2 -1
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +2 -1
- package/dist/projections/modelBound/fromAll.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/projections/captureProjectionProvenance.ts +1 -0
- package/projections/declarative/FromAllBuilder.ts +49 -0
- package/projections/declarative/IFromAllBuilder.ts +39 -0
- package/projections/declarative/IProjectionBuilderFor.ts +9 -0
- package/projections/declarative/ProjectionBuilderFor.ts +17 -0
- package/projections/declarative/for_ProjectionBuilderFor/when_subscribing_to_all_events.spec.ts +111 -0
- package/projections/declarative/index.ts +2 -0
- package/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.ts +15 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.spec.ts +90 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts +69 -0
- package/projections/modelBound/fromAll.ts +2 -1
- 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/projections/ProjectionReadModelProcessor.fixtures.spec.ts +5 -0
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +2 -0
- package/testing/singleAppendRoute.spec.ts +19 -0
- package/testing/singleAppendRoute.ts +9 -4
|
@@ -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');
|
|
@@ -38,6 +38,11 @@ const allFixtures = readdirSync(directory).filter(name => name.endsWith('.json')
|
|
|
38
38
|
.map(name => ({ name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture }));
|
|
39
39
|
const fixtures = allFixtures.filter(({ fixture }) => fixture.kind === 'kernelSemantics');
|
|
40
40
|
const guardErrors: Record<string, { type: string; message: string }> = {
|
|
41
|
+
'from-all-only.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
42
|
+
'from-all-mixed.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
43
|
+
'from-all-exclude-children.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
44
|
+
'from-all-empty.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
45
|
+
'from-every-restricted.json': { type: 'UnsupportedProjectionOperation', message: 'fromEvery/all subscriptions require a kernel-backed test' },
|
|
41
46
|
'joins-custom-key.json': { type: 'UnsupportedProjectionOperation', message: 'join keys other than $eventSourceId' },
|
|
42
47
|
'joins-initial-state.json': { type: 'UnsupportedProjectionOperation', message: 'joins with initial model state' },
|
|
43
48
|
'arithmetic-string-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic operands must have number/double' },
|
package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts
CHANGED
|
@@ -79,6 +79,8 @@ describe('when rejecting unsupported operations before any event is seeded', ()
|
|
|
79
79
|
{ name: 'removedWithJoin', define: builder => { builder.from(Changed).removedWithJoin(Removed); }, path: 'RemovedWithJoin[capability-removed:1] (.removedWithJoin)' },
|
|
80
80
|
{ name: 'removal key', define: builder => { builder.from(Removed).removedWith(Changed, removal => removal.usingKey(event => event.name)); }, path: 'RemovedWith[capability-changed:1].Key (.removedWith)' },
|
|
81
81
|
{ name: 'fromEvery', define: builder => { builder.from(Changed).fromEvery(all => all.set(model => model.name).toEventSourceId()); }, path: 'All (.fromEvery)' },
|
|
82
|
+
{ name: 'fromAll', define: builder => { builder.fromAll(all => all.set(model => model.name).toEventSourceId()); }, path: 'SubscribesToAllEvents (.fromAll)' },
|
|
83
|
+
{ name: 'empty fromAll', define: builder => { builder.fromAll(() => {}); }, path: 'SubscribesToAllEvents (.fromAll)' },
|
|
82
84
|
{ name: 'empty fromEvery', define: builder => { builder.from(Changed).fromEvery(all => all.excludeChildProjections()); }, path: 'All (.fromEvery)' },
|
|
83
85
|
{ name: 'passive', define: builder => { builder.from(Changed).passive(); }, path: 'IsActive (.passive)' },
|
|
84
86
|
{ name: 'event sequence', define: builder => { builder.from(Changed).fromEventSequence('custom'); }, path: 'EventSequenceId (.fromEventSequence)' }
|
|
@@ -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
|
+
});
|
|
@@ -6,16 +6,21 @@ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOpe
|
|
|
6
6
|
|
|
7
7
|
/** The fixture-proven single-append route boundary, shared with read-model setup. */
|
|
8
8
|
export function singleAppendRoute(options: AppendOptions | undefined, artifact: string,
|
|
9
|
-
unsupported = (operation: string, artifact: string, reason: string) => new UnsupportedEventSequenceOperation(operation, artifact, reason)
|
|
9
|
+
unsupported = (operation: string, artifact: string, reason: string) => new UnsupportedEventSequenceOperation(operation, artifact, reason),
|
|
10
|
+
allowSubject = false
|
|
10
11
|
): { sourceType: string; streamType: string; streamId: string } {
|
|
11
12
|
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
|
|
12
13
|
throw unsupported('append.options', artifact, 'Append options must be an object.');
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const allowed = ['sourceType', 'streamType', 'streamId', ...(allowSubject ? ['subject'] : [])];
|
|
16
|
+
if (options && (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
|
|
17
|
+
options.correlationId !== undefined || (!allowSubject && options.subject !== undefined) || options.occurred !== undefined ||
|
|
16
18
|
options.eventSourceId !== undefined || options.concurrencyScope !== undefined || options.tags !== undefined ||
|
|
17
19
|
options.concurrencyScopes !== undefined)) {
|
|
18
|
-
throw unsupported('append.options', artifact,
|
|
20
|
+
throw unsupported('append.options', artifact, `Only ${allowed.join(', ')} options are supported for single append.`);
|
|
21
|
+
}
|
|
22
|
+
if (options?.subject !== undefined && options.subject !== null && typeof options.subject !== 'string') {
|
|
23
|
+
throw unsupported('append.subject', artifact, 'Subject must be a string.');
|
|
19
24
|
}
|
|
20
25
|
const route = { sourceType: options?.sourceType, streamType: options?.streamType, streamId: options?.streamId };
|
|
21
26
|
for (const [name, value] of Object.entries(route)) {
|