@cratis/chronicle 6.4.0 → 6.5.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 +1 -1
- package/EventStore.ts +5 -3
- package/artifacts/validateArtifactSchemas.spec.ts +14 -0
- package/auditing/AmbientContext.spec.ts +57 -0
- package/auditing/CausationManager.ts +13 -15
- package/correlation/CorrelationIdManager.ts +5 -0
- package/dist/ChronicleClient.js +1 -1
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts +3 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +5 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/auditing/CausationManager.d.ts +3 -1
- package/dist/auditing/CausationManager.d.ts.map +1 -1
- package/dist/auditing/CausationManager.js +9 -15
- package/dist/auditing/CausationManager.js.map +1 -1
- package/dist/correlation/CorrelationIdManager.d.ts +2 -0
- package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
- package/dist/correlation/CorrelationIdManager.js +4 -0
- package/dist/correlation/CorrelationIdManager.js.map +1 -1
- package/dist/eventSequences/AppendOptions.d.ts +3 -4
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +19 -14
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/EventSequenceNumber.d.ts +3 -1
- package/dist/eventSequences/EventSequenceNumber.d.ts.map +1 -1
- package/dist/eventSequences/EventSequenceNumber.js +3 -1
- package/dist/eventSequences/EventSequenceNumber.js.map +1 -1
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/isRegisteredEvent.d.ts +4 -0
- package/dist/events/isRegisteredEvent.d.ts.map +1 -0
- package/dist/events/isRegisteredEvent.js +9 -0
- package/dist/events/isRegisteredEvent.js.map +1 -0
- package/dist/identity/IdentityProvider.d.ts +2 -0
- package/dist/identity/IdentityProvider.d.ts.map +1 -1
- package/dist/identity/IdentityProvider.js +4 -0
- package/dist/identity/IdentityProvider.js.map +1 -1
- package/dist/readModels/IReadModels.d.ts +10 -1
- package/dist/readModels/IReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -2
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModels.d.ts +6 -1
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +28 -2
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +3 -4
- package/eventSequences/EventSequence.spec.ts +59 -0
- package/eventSequences/EventSequence.ts +21 -15
- package/eventSequences/EventSequenceNumber.ts +4 -1
- package/eventSequences/for_EventSequence/when_appending/with_metadata.spec.ts +2 -1
- package/events/index.ts +1 -0
- package/events/isRegisteredEvent.spec.ts +26 -0
- package/events/isRegisteredEvent.ts +12 -0
- package/identity/IdentityProvider.ts +5 -0
- package/package.json +1 -1
- package/readModels/IReadModels.ts +11 -1
- package/readModels/MaterializedReadModels.spec.ts +17 -6
- package/readModels/MaterializedReadModels.ts +2 -2
- package/readModels/ReadModels.spec.ts +96 -3
- package/readModels/ReadModels.ts +32 -2
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import 'reflect-metadata';
|
|
5
|
-
import type
|
|
5
|
+
import { field, type Constructor } from '@cratis/fundamentals';
|
|
6
6
|
import { describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import type { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
8
8
|
import type { ChronicleConnection } from '../connection/index.js';
|
|
9
9
|
import { subject } from '../compliance/subject.js';
|
|
10
|
+
import { pii } from '../compliance/pii.js';
|
|
11
|
+
import { reducer } from '../reducers/reducer.js';
|
|
10
12
|
import { fromEvent } from '../projections/modelBound/fromEvent.js';
|
|
11
13
|
import { readModel } from './readModel.js';
|
|
12
14
|
import { ReadModels } from './ReadModels.js';
|
|
@@ -20,8 +22,10 @@ class SomeEvent {
|
|
|
20
22
|
|
|
21
23
|
function createReadModels(readModelType: Constructor, releaseResponse: Record<string, unknown> = { HasError: false, Payload: '{}' }) {
|
|
22
24
|
const release = vi.fn().mockResolvedValue(releaseResponse);
|
|
25
|
+
const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '' });
|
|
23
26
|
const connection = {
|
|
24
|
-
compliance: { release }
|
|
27
|
+
compliance: { release },
|
|
28
|
+
readModels: { getInstanceByKey }
|
|
25
29
|
} as unknown as ChronicleConnection;
|
|
26
30
|
|
|
27
31
|
const clientArtifacts = {
|
|
@@ -38,10 +42,99 @@ function createReadModels(readModelType: Constructor, releaseResponse: Record<st
|
|
|
38
42
|
} as IClientArtifactsProvider;
|
|
39
43
|
|
|
40
44
|
const readModels = new ReadModels('test-store', 'test-namespace', connection, clientArtifacts, 'default-sink');
|
|
41
|
-
return { readModels, release };
|
|
45
|
+
return { readModels, release, getInstanceByKey };
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
describe('ReadModels', () => {
|
|
49
|
+
describe('when no read model instance exists for a key', () => {
|
|
50
|
+
class MissingModel { id = ''; }
|
|
51
|
+
fromEvent(SomeEvent)(MissingModel);
|
|
52
|
+
readModel('MissingModel')(MissingModel);
|
|
53
|
+
|
|
54
|
+
it('should retain the legacy prototype-only instance for getInstanceById', async () => {
|
|
55
|
+
const { readModels, release } = createReadModels(MissingModel);
|
|
56
|
+
const instance = await readModels.getInstanceById(MissingModel, 'missing');
|
|
57
|
+
expect(instance).toBeInstanceOf(MissingModel);
|
|
58
|
+
expect(Object.keys(instance)).toEqual([]);
|
|
59
|
+
expect(release).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return null from findInstanceById without releasing PII', async () => {
|
|
63
|
+
const { readModels, release, getInstanceByKey } = createReadModels(MissingModel);
|
|
64
|
+
const instance = await readModels.findInstanceById(MissingModel, 'missing', 'session-1');
|
|
65
|
+
expect(instance).toBeNull();
|
|
66
|
+
expect(getInstanceByKey).toHaveBeenCalledWith(expect.objectContaining({ ReadModelKey: 'missing', SessionId: 'session-1' }));
|
|
67
|
+
expect(release).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('when a read model instance exists for a key', () => {
|
|
72
|
+
class ExistingModel { id = ''; }
|
|
73
|
+
field(String)(ExistingModel.prototype, 'id');
|
|
74
|
+
fromEvent(SomeEvent)(ExistingModel);
|
|
75
|
+
readModel('ExistingModel')(ExistingModel);
|
|
76
|
+
|
|
77
|
+
it('should deserialize the model', async () => {
|
|
78
|
+
const { readModels, getInstanceByKey } = createReadModels(ExistingModel);
|
|
79
|
+
getInstanceByKey.mockResolvedValue({ ReadModel: '{"id":"found"}' });
|
|
80
|
+
const instance = await readModels.findInstanceById(ExistingModel, 'found');
|
|
81
|
+
expect(instance).toBeInstanceOf(ExistingModel);
|
|
82
|
+
expect(instance?.id).toBe('found');
|
|
83
|
+
const legacyInstance = await readModels.getInstanceById(ExistingModel, 'found');
|
|
84
|
+
expect(legacyInstance).toBeInstanceOf(ExistingModel);
|
|
85
|
+
expect(legacyInstance.id).toBe('found');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('when reading a compliance-bearing reducer', () => {
|
|
90
|
+
class PrivateModel { id = ''; ssn = ''; }
|
|
91
|
+
field(String)(PrivateModel.prototype, 'id');
|
|
92
|
+
field(String)(PrivateModel.prototype, 'ssn');
|
|
93
|
+
pii()(PrivateModel.prototype, 'ssn');
|
|
94
|
+
readModel('PrivateInstanceModel')(PrivateModel);
|
|
95
|
+
class PrivateReducer {}
|
|
96
|
+
reducer('PrivateInstanceReducer', undefined, PrivateModel)(PrivateReducer);
|
|
97
|
+
|
|
98
|
+
it('should reject both read methods when compliance release fails', async () => {
|
|
99
|
+
const release = vi.fn().mockResolvedValue({ HasError: true, Error: 'denied' });
|
|
100
|
+
const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '{"id":"a","ssn":"ciphertext"}' });
|
|
101
|
+
const connection = {
|
|
102
|
+
readModels: { getInstanceByKey },
|
|
103
|
+
compliance: { release }
|
|
104
|
+
} as unknown as ChronicleConnection;
|
|
105
|
+
const provider = { reducers: [PrivateReducer], projections: [], readModels: [] } as unknown as IClientArtifactsProvider;
|
|
106
|
+
const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
|
|
107
|
+
|
|
108
|
+
await expect(readModels.getInstanceById(PrivateModel, 'a')).rejects.toThrow('Failed to release PII: denied');
|
|
109
|
+
await expect(readModels.findInstanceById(PrivateModel, 'a')).rejects.toThrow('Failed to release PII: denied');
|
|
110
|
+
expect(release).toHaveBeenCalledTimes(2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('when watching a compliance-bearing reducer', () => {
|
|
115
|
+
class PrivateModel { id = ''; ssn = ''; }
|
|
116
|
+
field(String)(PrivateModel.prototype, 'id');
|
|
117
|
+
field(String)(PrivateModel.prototype, 'ssn');
|
|
118
|
+
pii()(PrivateModel.prototype, 'ssn');
|
|
119
|
+
readModel('PrivateReducerModel')(PrivateModel);
|
|
120
|
+
class PrivateReducer {}
|
|
121
|
+
reducer('PrivateReducer', undefined, PrivateModel)(PrivateReducer);
|
|
122
|
+
|
|
123
|
+
it('should reject rather than expose an unreleased changeset', async () => {
|
|
124
|
+
const release = vi.fn().mockResolvedValue({ HasError: true, Error: 'denied' });
|
|
125
|
+
const connection = {
|
|
126
|
+
readModels: { watch: async function* () {
|
|
127
|
+
yield { Namespace: 'tenant', ModelKey: 'a', ReadModel: '{"id":"a","ssn":"ciphertext"}', Removed: false };
|
|
128
|
+
} },
|
|
129
|
+
compliance: { release }
|
|
130
|
+
} as unknown as ChronicleConnection;
|
|
131
|
+
const provider = { reducers: [PrivateReducer], projections: [], readModels: [] } as unknown as IClientArtifactsProvider;
|
|
132
|
+
const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
|
|
133
|
+
const iterator = readModels.watch(PrivateModel)[Symbol.asyncIterator]();
|
|
134
|
+
await expect(iterator.next()).rejects.toThrow('Failed to release PII: denied');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
45
138
|
describe('when releasing a read model with a property decorated with @subject()', () => {
|
|
46
139
|
class Employee {
|
|
47
140
|
id = '';
|
package/readModels/ReadModels.ts
CHANGED
|
@@ -72,7 +72,10 @@ export class ReadModels implements IReadModels {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* @inheritdoc
|
|
77
|
+
* @deprecated Use {@link findInstanceById} to distinguish an absent instance from a stored one.
|
|
78
|
+
*/
|
|
76
79
|
async getInstanceById<TReadModel>(readModelType: Constructor<TReadModel>, key: string, sessionId?: string): Promise<TReadModel> {
|
|
77
80
|
const readModel = this.resolveReadModel(readModelType);
|
|
78
81
|
const response = await this._connection.readModels.getInstanceByKey({
|
|
@@ -93,6 +96,30 @@ export class ReadModels implements IReadModels {
|
|
|
93
96
|
return instance;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
/** @inheritdoc */
|
|
100
|
+
async findInstanceById<TReadModel>(readModelType: Constructor<TReadModel>, key: string, sessionId?: string): Promise<TReadModel | null> {
|
|
101
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
102
|
+
const response = await this._connection.readModels.getInstanceByKey({
|
|
103
|
+
EventStore: this._eventStore,
|
|
104
|
+
Namespace: this._namespace,
|
|
105
|
+
ReadModelIdentifier: readModel.identifier,
|
|
106
|
+
EventSequenceId: readModel.eventSequenceId,
|
|
107
|
+
ReadModelKey: key,
|
|
108
|
+
SessionId: sessionId ?? ''
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!response.ReadModel) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const instance = this.deserializeReadModel(readModelType, response.ReadModel);
|
|
115
|
+
|
|
116
|
+
if (readModel.observerType === ContractReadModelObserverType.Reducer && this.schemaHasComplianceMetadata(readModel.schema)) {
|
|
117
|
+
return this.release(readModelType, instance);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return instance;
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
/** @inheritdoc */
|
|
97
124
|
async getInstances<TReadModel>(readModelType: Constructor<TReadModel>, eventCount: bigint = unlimitedEventCount): Promise<TReadModel[]> {
|
|
98
125
|
const readModel = this.resolveReadModel(readModelType);
|
|
@@ -149,10 +176,13 @@ export class ReadModels implements IReadModels {
|
|
|
149
176
|
ReadModelIdentifier: readModel.identifier,
|
|
150
177
|
EventSequenceId: readModel.eventSequenceId
|
|
151
178
|
})) {
|
|
179
|
+
const instance = this.deserializeReadModel(readModelType, changeset.ReadModel);
|
|
180
|
+
const requiresRelease = !changeset.Removed && readModel.observerType === ContractReadModelObserverType.Reducer &&
|
|
181
|
+
this.schemaHasComplianceMetadata(readModel.schema);
|
|
152
182
|
yield {
|
|
153
183
|
namespace: changeset.Namespace,
|
|
154
184
|
key: changeset.ModelKey,
|
|
155
|
-
readModel: this.
|
|
185
|
+
readModel: requiresRelease ? await this.release(readModelType, instance) : instance,
|
|
156
186
|
removed: changeset.Removed
|
|
157
187
|
};
|
|
158
188
|
}
|