@cratis/chronicle 3.1.0 → 4.0.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/ChronicleClient.ts +4 -3
- package/EventStore.ts +4 -3
- package/README.md +40 -28
- package/compliance/ComplianceContracts.ts +59 -0
- package/compliance/IPIIManager.ts +12 -0
- package/compliance/PIIManager.spec.ts +50 -0
- package/compliance/PIIManager.ts +9 -0
- package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
- package/compliance/index.ts +2 -0
- package/compliance/pii.spec.ts +37 -0
- package/compliance/pii.ts +16 -1
- package/compliance/subject.spec.ts +45 -0
- package/compliance/subject.ts +64 -0
- package/connection/callResults.spec.ts +90 -0
- package/connection/callResults.ts +112 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +4 -3
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +4 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +31 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.js +52 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -1
- package/dist/compliance/IPIIManager.d.ts +11 -0
- package/dist/compliance/IPIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.d.ts +2 -0
- package/dist/compliance/PIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.js +8 -0
- package/dist/compliance/PIIManager.js.map +1 -1
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/pii.d.ts.map +1 -1
- package/dist/compliance/pii.js +13 -1
- package/dist/compliance/pii.js.map +1 -1
- package/dist/compliance/subject.d.ts +39 -0
- package/dist/compliance/subject.d.ts.map +1 -0
- package/dist/compliance/subject.js +58 -0
- package/dist/compliance/subject.js.map +1 -0
- package/dist/connection/callResults.d.ts +69 -0
- package/dist/connection/callResults.d.ts.map +1 -0
- package/dist/connection/callResults.js +74 -0
- package/dist/connection/callResults.js.map +1 -0
- package/dist/eventSequences/AppendOptions.d.ts +8 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +17 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/EventContext.d.ts +3 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/Tag.d.ts +10 -0
- package/dist/events/Tag.d.ts.map +1 -0
- package/dist/events/Tag.js +16 -0
- package/dist/events/Tag.js.map +1 -0
- package/dist/events/constraints/Constraints.js +3 -3
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
- package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
- package/dist/events/filterEventsByTagDecorator.js +48 -0
- package/dist/events/filterEventsByTagDecorator.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeTags.d.ts +12 -0
- package/dist/events/mergeTags.d.ts.map +1 -0
- package/dist/events/mergeTags.js +26 -0
- package/dist/events/mergeTags.js.map +1 -0
- package/dist/events/tagDecorator.d.ts +38 -0
- package/dist/events/tagDecorator.d.ts.map +1 -0
- package/dist/events/tagDecorator.js +54 -0
- package/dist/events/tagDecorator.js.map +1 -0
- package/dist/jobs/IJobs.d.ts +4 -4
- package/dist/jobs/IJobs.d.ts.map +1 -1
- package/dist/jobs/Jobs.d.ts +4 -4
- package/dist/jobs/Jobs.d.ts.map +1 -1
- package/dist/jobs/Jobs.js +12 -10
- package/dist/jobs/Jobs.js.map +1 -1
- package/dist/observation/ObserverRunningState.d.ts +9 -15
- package/dist/observation/ObserverRunningState.d.ts.map +1 -1
- package/dist/observation/ObserverRunningState.js +8 -14
- package/dist/observation/ObserverRunningState.js.map +1 -1
- package/dist/observation/index.d.ts +1 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +1 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/toObserverRunningState.d.ts +9 -0
- package/dist/observation/toObserverRunningState.d.ts.map +1 -0
- package/dist/observation/toObserverRunningState.js +26 -0
- package/dist/observation/toObserverRunningState.js.map +1 -0
- package/dist/projections/IProjections.d.ts +71 -1
- package/dist/projections/IProjections.d.ts.map +1 -1
- package/dist/projections/ProjectionQueryResult.d.ts +8 -0
- package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
- package/dist/projections/ProjectionQueryResult.js +4 -0
- package/dist/projections/ProjectionQueryResult.js.map +1 -0
- package/dist/projections/ProjectionState.d.ts +18 -0
- package/dist/projections/ProjectionState.d.ts.map +1 -0
- package/dist/projections/ProjectionState.js +4 -0
- package/dist/projections/ProjectionState.js.map +1 -0
- package/dist/projections/Projections.d.ts +46 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +153 -6
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/UnableToQueryProjection.d.ts +12 -0
- package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
- package/dist/projections/UnableToQueryProjection.js +17 -0
- package/dist/projections/UnableToQueryProjection.js.map +1 -0
- package/dist/projections/index.d.ts +3 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/modelBound/eventSequence.d.ts +22 -0
- package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
- package/dist/projections/modelBound/eventSequence.js +34 -0
- package/dist/projections/modelBound/eventSequence.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +26 -0
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
- package/dist/projections/modelBound/fromAll.js +31 -0
- package/dist/projections/modelBound/fromAll.js.map +1 -0
- package/dist/projections/modelBound/index.d.ts +4 -0
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/projections/modelBound/index.js +3 -0
- package/dist/projections/modelBound/index.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
- package/dist/projections/modelBound/noAutoMap.js +44 -0
- package/dist/projections/modelBound/noAutoMap.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +7 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/MaterializedReadModels.d.ts +0 -1
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -8
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
- package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
- package/dist/readModels/ReadModelSubjectResolver.js +38 -0
- package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +7 -7
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/index.d.ts +1 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +1 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +21 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchema.d.ts +14 -0
- package/dist/schemas/JsonSchema.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +130 -26
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +3 -1
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +9 -0
- package/eventSequences/EventForEventSourceId.ts +9 -0
- package/eventSequences/EventSequence.ts +20 -6
- package/events/EventContext.ts +4 -0
- package/events/Tag.ts +14 -0
- package/events/constraints/Constraints.ts +3 -3
- package/events/filterEventsByTagDecorator.ts +51 -0
- package/events/index.ts +4 -0
- package/events/mergeTags.spec.ts +50 -0
- package/events/mergeTags.ts +27 -0
- package/events/tagDecorator.spec.ts +115 -0
- package/events/tagDecorator.ts +59 -0
- package/jobs/IJobs.ts +4 -4
- package/jobs/Jobs.ts +16 -14
- package/observation/ObserverRunningState.ts +9 -18
- package/observation/index.ts +1 -0
- package/observation/toObserverRunningState.ts +27 -0
- package/package.json +25 -10
- package/projections/IProjections.ts +81 -1
- package/projections/ProjectionQueryResult.ts +10 -0
- package/projections/ProjectionState.ts +25 -0
- package/projections/Projections.childrenAndNested.spec.ts +1 -1
- package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
- package/projections/Projections.operationalSurface.spec.ts +257 -0
- package/projections/Projections.spec.ts +1 -1
- package/projections/Projections.ts +180 -5
- package/projections/UnableToQueryProjection.ts +17 -0
- package/projections/index.ts +3 -0
- package/projections/modelBound/eventSequence.ts +38 -0
- package/projections/modelBound/fromAll.ts +42 -0
- package/projections/modelBound/index.ts +4 -0
- package/projections/modelBound/noAutoMap.ts +47 -0
- package/reactors/Reactors.ts +7 -3
- package/readModels/MaterializedReadModels.spec.ts +98 -0
- package/readModels/MaterializedReadModels.ts +2 -8
- package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
- package/readModels/ReadModelSubjectResolver.ts +44 -0
- package/readModels/ReadModels.spec.ts +118 -0
- package/readModels/ReadModels.ts +7 -7
- package/readModels/index.ts +1 -0
- package/reducers/Reducers.spec.ts +80 -0
- package/reducers/Reducers.ts +22 -3
- package/schemas/JsonSchema.ts +16 -0
- package/schemas/JsonSchemaGenerator.spec.ts +195 -0
- package/schemas/JsonSchemaGenerator.ts +145 -27
- package/schemas/index.ts +1 -1
- package/seeding/EventSeeding.ts +3 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import { TypeIntrospector } from '../../types';
|
|
6
|
+
|
|
7
|
+
const CLASS_METADATA_KEY = 'chronicle:projection:noAutoMap:class';
|
|
8
|
+
const PROPERTY_METADATA_KEY = 'chronicle:projection:noAutoMap:property';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Class or property decorator that disables AutoMap for a model-bound projection.
|
|
12
|
+
* Applied to a class, it prevents AutoMap from mapping any property automatically.
|
|
13
|
+
* Applied to a property, it excludes only that single property from AutoMap while every other
|
|
14
|
+
* property keeps mapping - use it to stop an unrelated event that carries an identically named
|
|
15
|
+
* property from silently overwriting a property whose value is set explicitly (for example via
|
|
16
|
+
* `setFrom`).
|
|
17
|
+
* @param target - The class constructor, or the class prototype when used on a property.
|
|
18
|
+
* @param propertyKey - The property name, when used as a property decorator.
|
|
19
|
+
*/
|
|
20
|
+
export function noAutoMap(target: object, propertyKey?: string | symbol): void {
|
|
21
|
+
if (propertyKey !== undefined) {
|
|
22
|
+
const key = propertyKey.toString();
|
|
23
|
+
TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
|
|
24
|
+
Reflect.defineMetadata(PROPERTY_METADATA_KEY, true, target, key);
|
|
25
|
+
} else {
|
|
26
|
+
Reflect.defineMetadata(CLASS_METADATA_KEY, true, target as Function);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether the given class has AutoMap disabled entirely.
|
|
32
|
+
* @param target - The class constructor.
|
|
33
|
+
* @returns True if the class is marked with {@link noAutoMap}; false otherwise.
|
|
34
|
+
*/
|
|
35
|
+
export function isNoAutoMap(target: Function): boolean {
|
|
36
|
+
return Reflect.hasMetadata(CLASS_METADATA_KEY, target);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks whether a specific property is excluded from AutoMap.
|
|
41
|
+
* @param target - The class prototype.
|
|
42
|
+
* @param propertyKey - The property name.
|
|
43
|
+
* @returns True if the property is marked with {@link noAutoMap}; false otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export function isPropertyNoAutoMap(target: object, propertyKey: string): boolean {
|
|
46
|
+
return Reflect.hasMetadata(PROPERTY_METADATA_KEY, target, propertyKey);
|
|
47
|
+
}
|
package/reactors/Reactors.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { getEventTypeMetadata } from '../events/eventTypeDecorator';
|
|
|
12
12
|
import { EventContext } from '../events/EventContext';
|
|
13
13
|
import { EventTypeId } from '../events/EventTypeId';
|
|
14
14
|
import { EventTypeGeneration } from '../events/EventTypeGeneration';
|
|
15
|
+
import { Tag } from '../events/Tag';
|
|
16
|
+
import { getTagsFor } from '../events/tagDecorator';
|
|
17
|
+
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator';
|
|
15
18
|
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
16
19
|
import type { IEventLog } from '../eventSequences/IEventLog';
|
|
17
20
|
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle';
|
|
@@ -239,9 +242,9 @@ export class Reactors implements IReactors {
|
|
|
239
242
|
Key: EVENT_SOURCE_ID_KEY
|
|
240
243
|
})),
|
|
241
244
|
IsReplayable: false,
|
|
242
|
-
Tags:
|
|
245
|
+
Tags: getTagsFor(reactorType).map(t => t.value),
|
|
243
246
|
Filters: {
|
|
244
|
-
FilterTags:
|
|
247
|
+
FilterTags: getFilterTagsFor(reactorType).map(t => t.value),
|
|
245
248
|
EventSourceType: '',
|
|
246
249
|
EventStreamType: 'All'
|
|
247
250
|
}
|
|
@@ -303,7 +306,8 @@ export class Reactors implements IReactors {
|
|
|
303
306
|
},
|
|
304
307
|
occurred: new Date(event.Context!.Occurred?.Value ?? ''),
|
|
305
308
|
correlationId: event.Context?.CorrelationId ? `${event.Context.CorrelationId.lo}-${event.Context.CorrelationId.hi}` : '',
|
|
306
|
-
causation: []
|
|
309
|
+
causation: [],
|
|
310
|
+
tags: (event.Context!.Tags ?? []).map(value => new Tag(value))
|
|
307
311
|
};
|
|
308
312
|
|
|
309
313
|
this._logger.info('Invoking reactor handler', {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import { field } from '@cratis/fundamentals';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import type { ChronicleConnection } from '../connection';
|
|
8
|
+
import { pii } from '../compliance/pii';
|
|
9
|
+
import { subject } from '../compliance/subject';
|
|
10
|
+
import { readModel } from './readModel';
|
|
11
|
+
import { MaterializedReadModels } from './MaterializedReadModels';
|
|
12
|
+
|
|
13
|
+
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
14
|
+
// fixtures don't depend on the test runner's decorator-syntax support.
|
|
15
|
+
|
|
16
|
+
function createMaterializedReadModels(instancesJson: string[], releaseResponse: Record<string, unknown> = { HasError: false, Payload: '{}' }) {
|
|
17
|
+
const release = vi.fn().mockResolvedValue(releaseResponse);
|
|
18
|
+
const getInstances = vi.fn().mockResolvedValue({ Instances: instancesJson });
|
|
19
|
+
const connection = {
|
|
20
|
+
compliance: { release },
|
|
21
|
+
materializedReadModels: { getInstances }
|
|
22
|
+
} as unknown as ChronicleConnection;
|
|
23
|
+
|
|
24
|
+
const readModels = new MaterializedReadModels('test-store', 'test-namespace', connection);
|
|
25
|
+
return { readModels, release };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('MaterializedReadModels', () => {
|
|
29
|
+
describe('when a read model has a property decorated with @subject()', () => {
|
|
30
|
+
class Employee {
|
|
31
|
+
id = '';
|
|
32
|
+
personId = '';
|
|
33
|
+
ssn = '';
|
|
34
|
+
}
|
|
35
|
+
field(String)(Employee.prototype, 'id');
|
|
36
|
+
field(String)(Employee.prototype, 'personId');
|
|
37
|
+
field(String)(Employee.prototype, 'ssn');
|
|
38
|
+
pii()(Employee.prototype, 'ssn');
|
|
39
|
+
subject()(Employee.prototype, 'personId');
|
|
40
|
+
readModel('EmployeeWithSubjectMaterialized')(Employee);
|
|
41
|
+
|
|
42
|
+
it('should release using the decorated property as the subject', async () => {
|
|
43
|
+
const json = JSON.stringify({ id: 'employee-1', personId: 'person-42', ssn: '123-45-6789' });
|
|
44
|
+
const { readModels, release } = createMaterializedReadModels([json]);
|
|
45
|
+
|
|
46
|
+
await readModels.getInstances(Employee);
|
|
47
|
+
|
|
48
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'person-42' }));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('when a read model has no property decorated with @subject() but has an id property', () => {
|
|
53
|
+
class Customer {
|
|
54
|
+
id = '';
|
|
55
|
+
ssn = '';
|
|
56
|
+
}
|
|
57
|
+
field(String)(Customer.prototype, 'id');
|
|
58
|
+
field(String)(Customer.prototype, 'ssn');
|
|
59
|
+
pii()(Customer.prototype, 'ssn');
|
|
60
|
+
readModel('CustomerWithIdOnlyMaterialized')(Customer);
|
|
61
|
+
|
|
62
|
+
it('should fall back to the id property as the subject, unchanged from today', async () => {
|
|
63
|
+
const json = JSON.stringify({ id: 'customer-7', ssn: '987-65-4321' });
|
|
64
|
+
const { readModels, release } = createMaterializedReadModels([json]);
|
|
65
|
+
|
|
66
|
+
await readModels.getInstances(Customer);
|
|
67
|
+
|
|
68
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-7' }));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('when a read model has neither a decorated property nor an id property', () => {
|
|
73
|
+
class Anonymous {
|
|
74
|
+
ssn = '';
|
|
75
|
+
}
|
|
76
|
+
field(String)(Anonymous.prototype, 'ssn');
|
|
77
|
+
pii()(Anonymous.prototype, 'ssn');
|
|
78
|
+
readModel('AnonymousMaterialized')(Anonymous);
|
|
79
|
+
|
|
80
|
+
it('should not call release, unchanged from today', async () => {
|
|
81
|
+
const json = JSON.stringify({ ssn: '000-00-0000' });
|
|
82
|
+
const { readModels, release } = createMaterializedReadModels([json]);
|
|
83
|
+
|
|
84
|
+
await readModels.getInstances(Anonymous);
|
|
85
|
+
|
|
86
|
+
expect(release).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return the instance unreleased', async () => {
|
|
90
|
+
const json = JSON.stringify({ ssn: '000-00-0000' });
|
|
91
|
+
const { readModels } = createMaterializedReadModels([json]);
|
|
92
|
+
|
|
93
|
+
const [instance] = await readModels.getInstances(Anonymous);
|
|
94
|
+
|
|
95
|
+
expect(instance.ssn).toBe('000-00-0000');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -6,6 +6,7 @@ import { JsonSerializer } from '@cratis/fundamentals';
|
|
|
6
6
|
import { ChronicleConnection } from '../connection';
|
|
7
7
|
import { JsonSchemaGenerator } from '../schemas';
|
|
8
8
|
import { getReadModelMetadata } from './readModel';
|
|
9
|
+
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
|
|
9
10
|
import type { IMaterializedReadModels } from './IMaterializedReadModels';
|
|
10
11
|
|
|
11
12
|
const defaultTake = 50;
|
|
@@ -97,7 +98,7 @@ export class MaterializedReadModels implements IMaterializedReadModels {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
private async releaseInstance<TReadModel>(readModelType: Constructor<TReadModel>, instance: TReadModel, schema: string): Promise<TReadModel> {
|
|
100
|
-
const subject =
|
|
101
|
+
const subject = ReadModelSubjectResolver.resolveFrom(readModelType, instance);
|
|
101
102
|
if (!subject) {
|
|
102
103
|
return instance;
|
|
103
104
|
}
|
|
@@ -141,11 +142,4 @@ export class MaterializedReadModels implements IMaterializedReadModels {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
private extractSubject<TReadModel>(instance: TReadModel): string | undefined {
|
|
145
|
-
const anyInstance = instance as Record<string, unknown>;
|
|
146
|
-
if (anyInstance.id !== undefined && anyInstance.id !== null) {
|
|
147
|
-
return String(anyInstance.id);
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
145
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { subject } from '../compliance/subject';
|
|
7
|
+
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
|
|
8
|
+
|
|
9
|
+
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
10
|
+
// fixtures don't depend on the test runner's decorator-syntax support.
|
|
11
|
+
|
|
12
|
+
describe('ReadModelSubjectResolver', () => {
|
|
13
|
+
describe('when the read model has a property decorated with @subject()', () => {
|
|
14
|
+
class Employee {
|
|
15
|
+
id = '';
|
|
16
|
+
personId = '';
|
|
17
|
+
}
|
|
18
|
+
subject()(Employee.prototype, 'personId');
|
|
19
|
+
|
|
20
|
+
it('should resolve to the decorated property value', () => {
|
|
21
|
+
const instance = new Employee();
|
|
22
|
+
instance.id = 'employee-1';
|
|
23
|
+
instance.personId = 'person-42';
|
|
24
|
+
|
|
25
|
+
expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('person-42');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should take precedence over the id property', () => {
|
|
29
|
+
const instance = new Employee();
|
|
30
|
+
instance.id = 'employee-1';
|
|
31
|
+
instance.personId = 'person-42';
|
|
32
|
+
|
|
33
|
+
expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).not.toBe('employee-1');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should fall back to id when the decorated property has no value', () => {
|
|
37
|
+
const instance = new Employee();
|
|
38
|
+
instance.id = 'employee-1';
|
|
39
|
+
instance.personId = '';
|
|
40
|
+
|
|
41
|
+
expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('employee-1');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('when the read model has no property decorated with @subject()', () => {
|
|
46
|
+
class Customer {
|
|
47
|
+
id = '';
|
|
48
|
+
name = '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('should fall back to the id property', () => {
|
|
52
|
+
const instance = new Customer();
|
|
53
|
+
instance.id = 'customer-7';
|
|
54
|
+
|
|
55
|
+
expect(ReadModelSubjectResolver.resolveFrom(Customer, instance)).toBe('customer-7');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('when the read model has neither a decorated property nor an id property', () => {
|
|
60
|
+
class Anonymous {
|
|
61
|
+
name = '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
it('should not resolve a subject', () => {
|
|
65
|
+
const instance = new Anonymous();
|
|
66
|
+
|
|
67
|
+
expect(ReadModelSubjectResolver.resolveFrom(Anonymous, instance)).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('when the instance does not exist', () => {
|
|
72
|
+
class Customer {
|
|
73
|
+
id = '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
it('should not resolve a subject for null', () => {
|
|
77
|
+
expect(ReadModelSubjectResolver.resolveFrom(Customer, null as unknown as Customer)).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not resolve a subject for undefined', () => {
|
|
81
|
+
expect(ReadModelSubjectResolver.resolveFrom(Customer, undefined as unknown as Customer)).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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 type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import { getSubjectPropertyName } from '../compliance/subject';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the compliance subject - the natural person a read model's Personal Identifiable
|
|
9
|
+
* Information (PII) belongs to - from a read model instance.
|
|
10
|
+
*/
|
|
11
|
+
export class ReadModelSubjectResolver {
|
|
12
|
+
/**
|
|
13
|
+
* Attempts to derive the compliance subject from a read model instance.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. The property decorated with `@subject()` on {@link readModelType}, when it has a value.
|
|
17
|
+
* 2. The `id` property, by convention - kept so read models that predate `@subject()`
|
|
18
|
+
* continue to resolve exactly as before.
|
|
19
|
+
* @param readModelType - The read model type to resolve subject metadata for.
|
|
20
|
+
* @param instance - The read model instance to inspect, or undefined/null for a read model
|
|
21
|
+
* that does not exist.
|
|
22
|
+
* @returns The resolved subject, or undefined when neither source yields a value.
|
|
23
|
+
*/
|
|
24
|
+
static resolveFrom<TReadModel>(readModelType: Constructor<TReadModel>, instance: TReadModel): string | undefined {
|
|
25
|
+
if (instance === undefined || instance === null) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const anyInstance = instance as Record<string, unknown>;
|
|
30
|
+
const subjectProperty = getSubjectPropertyName(readModelType);
|
|
31
|
+
const explicitSubject = subjectProperty ? ReadModelSubjectResolver.toSubject(anyInstance[subjectProperty]) : undefined;
|
|
32
|
+
|
|
33
|
+
return explicitSubject ?? ReadModelSubjectResolver.toSubject(anyInstance.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static toSubject(value: unknown): string | undefined {
|
|
37
|
+
if (value === undefined || value === null) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stringValue = String(value);
|
|
42
|
+
return stringValue.length > 0 ? stringValue : undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../artifacts';
|
|
8
|
+
import type { ChronicleConnection } from '../connection';
|
|
9
|
+
import { subject } from '../compliance/subject';
|
|
10
|
+
import { fromEvent } from '../projections/modelBound/fromEvent';
|
|
11
|
+
import { readModel } from './readModel';
|
|
12
|
+
import { ReadModels } from './ReadModels';
|
|
13
|
+
|
|
14
|
+
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
15
|
+
// fixtures don't depend on the test runner's decorator-syntax support.
|
|
16
|
+
|
|
17
|
+
class SomeEvent {
|
|
18
|
+
value = '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createReadModels(readModelType: Constructor, releaseResponse: Record<string, unknown> = { HasError: false, Payload: '{}' }) {
|
|
22
|
+
const release = vi.fn().mockResolvedValue(releaseResponse);
|
|
23
|
+
const connection = {
|
|
24
|
+
compliance: { release }
|
|
25
|
+
} as unknown as ChronicleConnection;
|
|
26
|
+
|
|
27
|
+
const clientArtifacts = {
|
|
28
|
+
eventTypes: [],
|
|
29
|
+
readModels: [readModelType],
|
|
30
|
+
reactors: [],
|
|
31
|
+
reducers: [],
|
|
32
|
+
seeders: [],
|
|
33
|
+
constraints: [],
|
|
34
|
+
projections: [],
|
|
35
|
+
webhooks: [],
|
|
36
|
+
eventTypeMigrations: []
|
|
37
|
+
} as IClientArtifactsProvider;
|
|
38
|
+
|
|
39
|
+
const readModels = new ReadModels('test-store', 'test-namespace', connection, clientArtifacts, 'default-sink');
|
|
40
|
+
return { readModels, release };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('ReadModels', () => {
|
|
44
|
+
describe('when releasing a read model with a property decorated with @subject()', () => {
|
|
45
|
+
class Employee {
|
|
46
|
+
id = '';
|
|
47
|
+
personId = '';
|
|
48
|
+
}
|
|
49
|
+
subject()(Employee.prototype, 'personId');
|
|
50
|
+
fromEvent(SomeEvent)(Employee);
|
|
51
|
+
readModel('EmployeeWithSubject')(Employee);
|
|
52
|
+
|
|
53
|
+
it('should release using the decorated property as the subject', async () => {
|
|
54
|
+
const { readModels, release } = createReadModels(Employee);
|
|
55
|
+
const instance = new Employee();
|
|
56
|
+
instance.id = 'employee-1';
|
|
57
|
+
instance.personId = 'person-42';
|
|
58
|
+
|
|
59
|
+
await readModels.release(Employee, instance);
|
|
60
|
+
|
|
61
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'person-42' }));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('when releasing a read model without @subject() but with an id property', () => {
|
|
66
|
+
class Customer {
|
|
67
|
+
id = '';
|
|
68
|
+
}
|
|
69
|
+
fromEvent(SomeEvent)(Customer);
|
|
70
|
+
readModel('CustomerWithIdOnly')(Customer);
|
|
71
|
+
|
|
72
|
+
it('should fall back to the id property as the subject, unchanged from today', async () => {
|
|
73
|
+
const { readModels, release } = createReadModels(Customer);
|
|
74
|
+
const instance = new Customer();
|
|
75
|
+
instance.id = 'customer-7';
|
|
76
|
+
|
|
77
|
+
await readModels.release(Customer, instance);
|
|
78
|
+
|
|
79
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-7' }));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('when releasing a read model with neither @subject() nor an id property', () => {
|
|
84
|
+
class Anonymous {
|
|
85
|
+
name = '';
|
|
86
|
+
}
|
|
87
|
+
fromEvent(SomeEvent)(Anonymous);
|
|
88
|
+
readModel('AnonymousReadModel')(Anonymous);
|
|
89
|
+
|
|
90
|
+
it('should throw, same as today', async () => {
|
|
91
|
+
const { readModels } = createReadModels(Anonymous);
|
|
92
|
+
const instance = new Anonymous();
|
|
93
|
+
|
|
94
|
+
await expect(readModels.release(Anonymous, instance)).rejects.toThrow(/subject/);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('when releasing many read model instances', () => {
|
|
99
|
+
class Customer {
|
|
100
|
+
id = '';
|
|
101
|
+
}
|
|
102
|
+
fromEvent(SomeEvent)(Customer);
|
|
103
|
+
readModel('CustomerForReleaseMany')(Customer);
|
|
104
|
+
|
|
105
|
+
it('should release each instance using its own resolved subject', async () => {
|
|
106
|
+
const { readModels, release } = createReadModels(Customer);
|
|
107
|
+
const first = new Customer();
|
|
108
|
+
first.id = 'customer-1';
|
|
109
|
+
const second = new Customer();
|
|
110
|
+
second.id = 'customer-2';
|
|
111
|
+
|
|
112
|
+
await readModels.releaseMany(Customer, [first, second]);
|
|
113
|
+
|
|
114
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-1' }));
|
|
115
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-2' }));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
package/readModels/ReadModels.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { WellKnownSinks } from '../sinks';
|
|
|
23
23
|
import { getReadModelMetadata } from './readModel';
|
|
24
24
|
import type { IMaterializedReadModels } from './IMaterializedReadModels';
|
|
25
25
|
import { MaterializedReadModels } from './MaterializedReadModels';
|
|
26
|
+
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
|
|
26
27
|
import type { IReadModels } from './IReadModels';
|
|
27
28
|
import type { ReadModelChangeset } from './ReadModelChangeset';
|
|
28
29
|
import type { ReadModelSnapshot } from './ReadModelSnapshot';
|
|
@@ -173,7 +174,7 @@ export class ReadModels implements IReadModels {
|
|
|
173
174
|
const readModel = this.resolveReadModel(readModelType);
|
|
174
175
|
const schema = this.getReadModelSchema(readModelType, readModel.identifier);
|
|
175
176
|
const payload = JsonSerializer.serialize(instance);
|
|
176
|
-
const subject = this.extractSubject(instance);
|
|
177
|
+
const subject = this.extractSubject(readModelType, instance);
|
|
177
178
|
|
|
178
179
|
const response = await this._connection.compliance.release({
|
|
179
180
|
EventStore: this._eventStore,
|
|
@@ -345,12 +346,11 @@ export class ReadModels implements IReadModels {
|
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
private extractSubject<TReadModel>(instance: TReadModel): string {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return String(anyInstance.id);
|
|
349
|
+
private extractSubject<TReadModel>(readModelType: Constructor<TReadModel>, instance: TReadModel): string {
|
|
350
|
+
const subject = ReadModelSubjectResolver.resolveFrom(readModelType, instance);
|
|
351
|
+
if (subject !== undefined) {
|
|
352
|
+
return subject;
|
|
353
353
|
}
|
|
354
|
-
throw new Error('Read model instance must have an "id" property to serve as the subject for PII release');
|
|
354
|
+
throw new Error('Read model instance must have a property decorated with @subject() or an "id" property to serve as the subject for PII release');
|
|
355
355
|
}
|
|
356
356
|
}
|
package/readModels/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { IReadModels } from './IReadModels';
|
|
|
6
6
|
export type { IMaterializedReadModels } from './IMaterializedReadModels';
|
|
7
7
|
export { ReadModels } from './ReadModels';
|
|
8
8
|
export { MaterializedReadModels } from './MaterializedReadModels';
|
|
9
|
+
export { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
|
|
9
10
|
export type { ReadModelChangeset } from './ReadModelChangeset';
|
|
10
11
|
export type { ReadModelSnapshot } from './ReadModelSnapshot';
|
|
11
12
|
export { readModel, getReadModelMetadata, isReadModel } from './readModel';
|
|
@@ -6,11 +6,48 @@ import type { Constructor } from '@cratis/fundamentals';
|
|
|
6
6
|
import type { IClientArtifactsProvider } from '../artifacts';
|
|
7
7
|
import type { ChronicleConnection } from '../connection';
|
|
8
8
|
import { ConnectionLifecycle } from '../connection/ConnectionLifecycle';
|
|
9
|
+
import { eventType, getEventTypeFor } from '../events/eventTypeDecorator';
|
|
10
|
+
import type { EventContext } from '../events/EventContext';
|
|
11
|
+
import { filterEventsByTag } from '../events/filterEventsByTagDecorator';
|
|
12
|
+
import { tag } from '../events/tagDecorator';
|
|
9
13
|
import { reducer } from './reducer';
|
|
10
14
|
import { Reducers } from './Reducers';
|
|
11
15
|
|
|
12
16
|
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
13
17
|
|
|
18
|
+
class ReducersSomeEventHappened {
|
|
19
|
+
constructor(readonly value: string = '') {}
|
|
20
|
+
}
|
|
21
|
+
eventType('d2b2b2b2-2222-4a3c-9d3f-6f2f4a3c9d3f')(ReducersSomeEventHappened);
|
|
22
|
+
|
|
23
|
+
class SomeTaggedReducerState {
|
|
24
|
+
count = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class SomeTaggedReducer {
|
|
28
|
+
reducersSomeEventHappened(): SomeTaggedReducerState {
|
|
29
|
+
return { count: 1 };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
reducer('some-tagged-reducer', undefined, SomeTaggedReducerState)(SomeTaggedReducer);
|
|
33
|
+
tag('Analytics', 'Reporting')(SomeTaggedReducer);
|
|
34
|
+
filterEventsByTag('vip')(SomeTaggedReducer);
|
|
35
|
+
filterEventsByTag('priority')(SomeTaggedReducer);
|
|
36
|
+
|
|
37
|
+
const receivedContexts: EventContext[] = [];
|
|
38
|
+
|
|
39
|
+
class CapturingReducerState {
|
|
40
|
+
count = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class CapturingReducer {
|
|
44
|
+
reducersSomeEventHappened(_event: ReducersSomeEventHappened, current: CapturingReducerState | undefined, context: EventContext): CapturingReducerState {
|
|
45
|
+
receivedContexts.push(context);
|
|
46
|
+
return { count: (current?.count ?? 0) + 1 };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
reducer('capturing-reducer', undefined, CapturingReducerState)(CapturingReducer);
|
|
50
|
+
|
|
14
51
|
/**
|
|
15
52
|
* A minimal client artifacts provider exposing only the reducer types under test —
|
|
16
53
|
* Reducers.ts only reads .reducers and .eventTypes off this during discovery.
|
|
@@ -84,4 +121,47 @@ describe('Reducers', () => {
|
|
|
84
121
|
expect(message.Content.Value0.Reducer.IsActive).toBe(false);
|
|
85
122
|
});
|
|
86
123
|
});
|
|
124
|
+
|
|
125
|
+
describe('when registering a reducer that is tagged and filters by tag', () => {
|
|
126
|
+
type TaggedRegistration = {
|
|
127
|
+
Content: { Value0: { Reducer: { Tags: string[]; Filters: { FilterTags: string[] } } } };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const register = async () => {
|
|
131
|
+
const { connection, registrationMessages } = createConnection();
|
|
132
|
+
const reducers = new Reducers(createArtifacts([SomeTaggedReducer]), connection, 'my-event-store', 'my-namespace', new ConnectionLifecycle(), 'default-sink');
|
|
133
|
+
|
|
134
|
+
await reducers.register();
|
|
135
|
+
await flush();
|
|
136
|
+
|
|
137
|
+
return (registrationMessages[0] as TaggedRegistration).Content.Value0.Reducer;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
it('should carry the tags the reducer is labeled with', async () => {
|
|
141
|
+
expect((await register()).Tags).toEqual(['Analytics', 'Reporting']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should carry every tag it filters events by', async () => {
|
|
145
|
+
expect((await register()).Filters.FilterTags).toEqual(['vip', 'priority']);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('when registering a reducer that is neither tagged nor filtered', () => {
|
|
150
|
+
class SomeUntaggedReducer {}
|
|
151
|
+
reducer('some-untagged-reducer')(SomeUntaggedReducer);
|
|
152
|
+
|
|
153
|
+
it('should send empty tag lists rather than omitting them', async () => {
|
|
154
|
+
const { connection, registrationMessages } = createConnection();
|
|
155
|
+
const reducers = new Reducers(createArtifacts([SomeUntaggedReducer]), connection, 'my-event-store', 'my-namespace', new ConnectionLifecycle(), 'default-sink');
|
|
156
|
+
|
|
157
|
+
await reducers.register();
|
|
158
|
+
await flush();
|
|
159
|
+
|
|
160
|
+
const message = registrationMessages[0] as {
|
|
161
|
+
Content: { Value0: { Reducer: { Tags: string[]; Filters: { FilterTags: string[] } } } };
|
|
162
|
+
};
|
|
163
|
+
expect(message.Content.Value0.Reducer.Tags).toEqual([]);
|
|
164
|
+
expect(message.Content.Value0.Reducer.Filters.FilterTags).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
87
167
|
});
|
package/reducers/Reducers.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { ChronicleConnection } from '../connection';
|
|
|
10
10
|
import { toContractsGuid } from '../connection/Guid';
|
|
11
11
|
import { ConnectionLifecycle } from '../connection/ConnectionLifecycle';
|
|
12
12
|
import { getEventTypeMetadata } from '../events/eventTypeDecorator';
|
|
13
|
+
import { EventContext } from '../events/EventContext';
|
|
14
|
+
import { EventTypeId } from '../events/EventTypeId';
|
|
15
|
+
import { EventTypeGeneration } from '../events/EventTypeGeneration';
|
|
16
|
+
import { Tag } from '../events/Tag';
|
|
17
|
+
import { getTagsFor } from '../events/tagDecorator';
|
|
18
|
+
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator';
|
|
13
19
|
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
14
20
|
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle';
|
|
15
21
|
import { IReducers } from './IReducers';
|
|
@@ -310,9 +316,9 @@ export class Reducers implements IReducers {
|
|
|
310
316
|
})),
|
|
311
317
|
ReadModel: readModelName,
|
|
312
318
|
IsActive: isActive,
|
|
313
|
-
Tags:
|
|
319
|
+
Tags: getTagsFor(reducerType).map(t => t.value),
|
|
314
320
|
Filters: {
|
|
315
|
-
FilterTags:
|
|
321
|
+
FilterTags: getFilterTagsFor(reducerType).map(t => t.value),
|
|
316
322
|
EventSourceType: '',
|
|
317
323
|
EventStreamType: 'All'
|
|
318
324
|
}
|
|
@@ -369,6 +375,19 @@ export class Reducers implements IReducers {
|
|
|
369
375
|
}
|
|
370
376
|
|
|
371
377
|
const content = JSON.parse(event.Content) as Record<string, unknown>;
|
|
378
|
+
const context: EventContext = {
|
|
379
|
+
sequenceNumber: event.Context!.SequenceNumber,
|
|
380
|
+
eventSourceId: event.Context!.EventSourceId,
|
|
381
|
+
eventType: {
|
|
382
|
+
id: new EventTypeId(event.Context!.EventType!.Id),
|
|
383
|
+
generation: new EventTypeGeneration(event.Context!.EventType!.Generation),
|
|
384
|
+
tombstone: event.Context!.EventType!.Tombstone
|
|
385
|
+
},
|
|
386
|
+
occurred: new Date(event.Context!.Occurred?.Value ?? ''),
|
|
387
|
+
correlationId: event.Context?.CorrelationId ? `${event.Context.CorrelationId.lo}-${event.Context.CorrelationId.hi}` : '',
|
|
388
|
+
causation: [],
|
|
389
|
+
tags: (event.Context!.Tags ?? []).map(value => new Tag(value))
|
|
390
|
+
};
|
|
372
391
|
|
|
373
392
|
this._logger.info('Invoking reducer handler', {
|
|
374
393
|
reducerId: id,
|
|
@@ -378,7 +397,7 @@ export class Reducers implements IReducers {
|
|
|
378
397
|
hasState: currentState !== undefined
|
|
379
398
|
});
|
|
380
399
|
|
|
381
|
-
currentState = await reducerInstance[entry.methodName](content, currentState);
|
|
400
|
+
currentState = await reducerInstance[entry.methodName](content, currentState, context);
|
|
382
401
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
383
402
|
} catch (err) {
|
|
384
403
|
this._logger.error('Error handling event in reducer', { reducerId: id, error: String(err) });
|