@cratis/chronicle 0.2.0 → 0.2.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/EventStore.ts +8 -0
- package/IEventStore.ts +8 -0
- package/compliance/ComplianceContracts.ts +63 -0
- package/compliance/ComplianceMetadata.ts +19 -0
- package/compliance/ComplianceMetadataResolver.ts +51 -0
- package/compliance/ComplianceMetadataType.ts +36 -0
- package/compliance/index.ts +7 -0
- package/compliance/pii.ts +115 -0
- package/connection/ChronicleConnection.ts +19 -1
- package/connection/ChronicleServices.ts +6 -0
- package/dist/EventStore.d.ts +4 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +6 -0
- package/dist/EventStore.js.map +1 -1
- package/dist/IEventStore.d.ts +6 -0
- package/dist/IEventStore.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +56 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -0
- package/dist/compliance/ComplianceContracts.js +25 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -0
- package/dist/compliance/ComplianceMetadata.d.ts +15 -0
- package/dist/compliance/ComplianceMetadata.d.ts.map +1 -0
- package/dist/compliance/ComplianceMetadata.js +4 -0
- package/dist/compliance/ComplianceMetadata.js.map +1 -0
- package/dist/compliance/ComplianceMetadataResolver.d.ts +27 -0
- package/dist/compliance/ComplianceMetadataResolver.d.ts.map +1 -0
- package/dist/compliance/ComplianceMetadataResolver.js +45 -0
- package/dist/compliance/ComplianceMetadataResolver.js.map +1 -0
- package/dist/compliance/ComplianceMetadataType.d.ts +24 -0
- package/dist/compliance/ComplianceMetadataType.d.ts.map +1 -0
- package/dist/compliance/ComplianceMetadataType.js +32 -0
- package/dist/compliance/ComplianceMetadataType.js.map +1 -0
- package/dist/compliance/index.d.ts +5 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +6 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/compliance/pii.d.ts +69 -0
- package/dist/compliance/pii.d.ts.map +1 -0
- package/dist/compliance/pii.js +106 -0
- package/dist/compliance/pii.js.map +1 -0
- package/dist/connection/ChronicleConnection.d.ts +3 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +16 -2
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +6 -1
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +3 -3
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionBuilder.d.ts +20 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionBuilder.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionBuilder.js +41 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionBuilder.js.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionDefinition.d.ts +12 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionDefinition.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionDefinition.js +16 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionDefinition.js.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionId.d.ts +10 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionId.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionId.js +16 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptionId.js.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts +21 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +60 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptionBuilder.d.ts +20 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptionBuilder.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptionBuilder.js +4 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptionBuilder.js.map +1 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptions.d.ts +21 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptions.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptions.js +4 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptions.js.map +1 -0
- package/dist/eventStoreSubscriptions/contracts.d.ts +71 -0
- package/dist/eventStoreSubscriptions/contracts.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/contracts.js +310 -0
- package/dist/eventStoreSubscriptions/contracts.js.map +1 -0
- package/dist/eventStoreSubscriptions/index.d.ts +7 -0
- package/dist/eventStoreSubscriptions/index.d.ts.map +1 -0
- package/dist/eventStoreSubscriptions/index.js +7 -0
- package/dist/eventStoreSubscriptions/index.js.map +1 -0
- package/dist/events/constraints/IUniqueConstraintBuilder.d.ts +3 -2
- package/dist/events/constraints/IUniqueConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueConstraintBuilder.d.ts +2 -8
- package/dist/events/constraints/UniqueConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueConstraintBuilder.js +4 -15
- package/dist/events/constraints/UniqueConstraintBuilder.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/reactors/Reactors.js +1 -1
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/IMaterializedReadModels.d.ts +23 -0
- package/dist/readModels/IMaterializedReadModels.d.ts.map +1 -0
- package/dist/readModels/IMaterializedReadModels.js +4 -0
- package/dist/readModels/IMaterializedReadModels.js.map +1 -0
- package/dist/readModels/IReadModels.d.ts +75 -0
- package/dist/readModels/IReadModels.d.ts.map +1 -0
- package/dist/readModels/IReadModels.js +4 -0
- package/dist/readModels/IReadModels.js.map +1 -0
- package/dist/readModels/MaterializedReadModels.d.ts +25 -0
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -0
- package/dist/readModels/MaterializedReadModels.js +125 -0
- package/dist/readModels/MaterializedReadModels.js.map +1 -0
- package/dist/readModels/ReadModelChangeset.d.ts +14 -0
- package/dist/readModels/ReadModelChangeset.d.ts.map +1 -0
- package/dist/readModels/ReadModelChangeset.js +4 -0
- package/dist/readModels/ReadModelChangeset.js.map +1 -0
- package/dist/readModels/ReadModelSnapshot.d.ts +15 -0
- package/dist/readModels/ReadModelSnapshot.d.ts.map +1 -0
- package/dist/readModels/ReadModelSnapshot.js +4 -0
- package/dist/readModels/ReadModelSnapshot.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts +42 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -0
- package/dist/readModels/ReadModels.js +270 -0
- package/dist/readModels/ReadModels.js.map +1 -0
- package/dist/readModels/index.d.ts +6 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +2 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/readModels/readModel.d.ts.map +1 -1
- package/dist/readModels/readModel.js +0 -20
- package/dist/readModels/readModel.js.map +1 -1
- package/dist/reducers/Reducers.d.ts +1 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +10 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +9 -0
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +53 -1
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TypeIntrospector.d.ts +11 -1
- package/dist/types/TypeIntrospector.d.ts.map +1 -1
- package/dist/types/TypeIntrospector.js +29 -3
- package/dist/types/TypeIntrospector.js.map +1 -1
- package/eventSequences/EventSequence.ts +3 -3
- package/eventStoreSubscriptions/EventStoreSubscriptionBuilder.ts +52 -0
- package/eventStoreSubscriptions/EventStoreSubscriptionDefinition.ts +16 -0
- package/eventStoreSubscriptions/EventStoreSubscriptionId.ts +14 -0
- package/eventStoreSubscriptions/EventStoreSubscriptions.ts +74 -0
- package/eventStoreSubscriptions/IEventStoreSubscriptionBuilder.ts +24 -0
- package/eventStoreSubscriptions/IEventStoreSubscriptions.ts +29 -0
- package/eventStoreSubscriptions/contracts.ts +372 -0
- package/eventStoreSubscriptions/index.ts +9 -0
- package/events/constraints/IUniqueConstraintBuilder.ts +3 -2
- package/events/constraints/UniqueConstraintBuilder.ts +5 -17
- package/index.ts +6 -0
- package/package.json +17 -7
- package/reactors/Reactors.ts +1 -1
- package/readModels/IMaterializedReadModels.ts +27 -0
- package/readModels/IReadModels.ts +87 -0
- package/readModels/MaterializedReadModels.ts +151 -0
- package/readModels/ReadModelChangeset.ts +19 -0
- package/readModels/ReadModelSnapshot.ts +21 -0
- package/readModels/ReadModels.ts +333 -0
- package/readModels/index.ts +6 -0
- package/readModels/readModel.ts +0 -22
- package/reducers/Reducers.ts +12 -3
- package/schemas/JsonSchemaGenerator.ts +61 -1
- package/types/TypeIntrospector.ts +32 -3
|
@@ -0,0 +1,333 @@
|
|
|
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 {
|
|
5
|
+
AppendedEvent,
|
|
6
|
+
ReadModelObserverType
|
|
7
|
+
} from '@cratis/chronicle.contracts';
|
|
8
|
+
import {
|
|
9
|
+
ReadModelObserverType as ContractReadModelObserverType
|
|
10
|
+
} from '@cratis/chronicle.contracts';
|
|
11
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
12
|
+
import { JsonSerializer } from '@cratis/fundamentals';
|
|
13
|
+
import { IClientArtifactsProvider } from '../artifacts';
|
|
14
|
+
import { toContractsGuid } from '../connection/Guid';
|
|
15
|
+
import { ChronicleConnection } from '../connection';
|
|
16
|
+
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
17
|
+
import { getProjectionMetadata } from '../projections/declarative/projection';
|
|
18
|
+
import { hasFromEventMetadata } from '../projections/modelBound/fromEvent';
|
|
19
|
+
import { getReducerMetadata } from '../reducers/reducer';
|
|
20
|
+
import { JsonSchemaGenerator } from '../schemas';
|
|
21
|
+
import { WellKnownSinks } from '../sinks';
|
|
22
|
+
import { getReadModelMetadata } from './readModel';
|
|
23
|
+
import type { IMaterializedReadModels } from './IMaterializedReadModels';
|
|
24
|
+
import { MaterializedReadModels } from './MaterializedReadModels';
|
|
25
|
+
import type { IReadModels } from './IReadModels';
|
|
26
|
+
import type { ReadModelChangeset } from './ReadModelChangeset';
|
|
27
|
+
import type { ReadModelSnapshot } from './ReadModelSnapshot';
|
|
28
|
+
|
|
29
|
+
const unlimitedEventCount = BigInt('18446744073709551615');
|
|
30
|
+
|
|
31
|
+
interface ResolvedReadModel {
|
|
32
|
+
readonly type: Constructor;
|
|
33
|
+
readonly identifier: string;
|
|
34
|
+
readonly eventSequenceId: string;
|
|
35
|
+
readonly observerType: ReadModelObserverType;
|
|
36
|
+
readonly observerIdentifier: string;
|
|
37
|
+
readonly schema: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Implements {@link IReadModels} by working with the Chronicle read-model gRPC service.
|
|
42
|
+
*/
|
|
43
|
+
export class ReadModels implements IReadModels {
|
|
44
|
+
readonly materialized: IMaterializedReadModels;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private readonly _eventStore: string,
|
|
48
|
+
private readonly _namespace: string,
|
|
49
|
+
private readonly _connection: ChronicleConnection,
|
|
50
|
+
private readonly _clientArtifacts: IClientArtifactsProvider
|
|
51
|
+
) {
|
|
52
|
+
this.materialized = new MaterializedReadModels(_eventStore, _namespace, _connection);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @inheritdoc */
|
|
56
|
+
async register<TReadModel>(readModelType?: Constructor<TReadModel>): Promise<void> {
|
|
57
|
+
const readModels = this.resolveReadModels(readModelType);
|
|
58
|
+
if (readModels.length === 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await this._connection.readModels.registerMany({
|
|
63
|
+
EventStore: this._eventStore,
|
|
64
|
+
Owner: 1,
|
|
65
|
+
Source: 1,
|
|
66
|
+
ReadModels: readModels.map(readModel => this.toDefinition(readModel))
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @inheritdoc */
|
|
71
|
+
async getInstanceById<TReadModel>(readModelType: Constructor<TReadModel>, key: string, sessionId?: string): Promise<TReadModel> {
|
|
72
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
73
|
+
const response = await this._connection.readModels.getInstanceByKey({
|
|
74
|
+
EventStore: this._eventStore,
|
|
75
|
+
Namespace: this._namespace,
|
|
76
|
+
ReadModelIdentifier: readModel.identifier,
|
|
77
|
+
EventSequenceId: readModel.eventSequenceId,
|
|
78
|
+
ReadModelKey: key,
|
|
79
|
+
SessionId: sessionId ?? ''
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const instance = this.deserializeReadModel(readModelType, response.ReadModel);
|
|
83
|
+
|
|
84
|
+
if (readModel.observerType === ContractReadModelObserverType.Reducer && this.schemaHasComplianceMetadata(readModel.schema)) {
|
|
85
|
+
return this.release(readModelType, instance);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return instance;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @inheritdoc */
|
|
92
|
+
async getInstances<TReadModel>(readModelType: Constructor<TReadModel>, eventCount: bigint = unlimitedEventCount): Promise<TReadModel[]> {
|
|
93
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
94
|
+
const response = await this._connection.readModels.getAllInstances({
|
|
95
|
+
EventStore: this._eventStore,
|
|
96
|
+
Namespace: this._namespace,
|
|
97
|
+
ReadModelIdentifier: readModel.identifier,
|
|
98
|
+
EventSequenceId: readModel.eventSequenceId,
|
|
99
|
+
EventCount: eventCount
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const instances = response.Instances.map(instance => this.deserializeReadModel(readModelType, instance));
|
|
103
|
+
|
|
104
|
+
if (readModel.observerType === ContractReadModelObserverType.Reducer && this.schemaHasComplianceMetadata(readModel.schema)) {
|
|
105
|
+
return this.releaseMany(readModelType, instances);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return instances;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @inheritdoc */
|
|
112
|
+
async getSnapshotsById<TReadModel>(readModelType: Constructor<TReadModel>, key: string): Promise<ReadModelSnapshot<TReadModel>[]> {
|
|
113
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
114
|
+
const response = await this._connection.readModels.getSnapshotsByKey({
|
|
115
|
+
EventStore: this._eventStore,
|
|
116
|
+
Namespace: this._namespace,
|
|
117
|
+
ReadModelIdentifier: readModel.identifier,
|
|
118
|
+
EventSequenceId: readModel.eventSequenceId,
|
|
119
|
+
ReadModelKey: key
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return response.Snapshots.map(snapshot => ({
|
|
123
|
+
readModel: this.deserializeReadModel(readModelType, snapshot.ReadModel),
|
|
124
|
+
events: (snapshot.Events ?? []) as AppendedEvent[],
|
|
125
|
+
occurred: snapshot.Occurred?.Value ? new Date(snapshot.Occurred.Value) : undefined,
|
|
126
|
+
correlationId: snapshot.CorrelationId
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @inheritdoc */
|
|
131
|
+
async *watch<TReadModel>(readModelType: Constructor<TReadModel>): AsyncIterable<ReadModelChangeset<TReadModel>> {
|
|
132
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
133
|
+
|
|
134
|
+
for await (const changeset of this._connection.readModels.watch({
|
|
135
|
+
EventStore: this._eventStore,
|
|
136
|
+
Namespace: this._namespace,
|
|
137
|
+
ReadModelIdentifier: readModel.identifier,
|
|
138
|
+
EventSequenceId: readModel.eventSequenceId
|
|
139
|
+
})) {
|
|
140
|
+
yield {
|
|
141
|
+
namespace: changeset.Namespace,
|
|
142
|
+
key: changeset.ModelKey,
|
|
143
|
+
readModel: this.deserializeReadModel(readModelType, changeset.ReadModel),
|
|
144
|
+
removed: changeset.Removed
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @inheritdoc */
|
|
150
|
+
async dehydrateSession<TReadModel>(sessionId: string, readModelType: Constructor<TReadModel>, key: string): Promise<void> {
|
|
151
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
152
|
+
await this._connection.readModels.dehydrateSession({
|
|
153
|
+
EventStore: this._eventStore,
|
|
154
|
+
Namespace: this._namespace,
|
|
155
|
+
ReadModelIdentifier: readModel.identifier,
|
|
156
|
+
EventSequenceId: readModel.eventSequenceId,
|
|
157
|
+
ReadModelKey: key,
|
|
158
|
+
SessionId: sessionId
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @inheritdoc */
|
|
163
|
+
async release<TReadModel>(readModelType: Constructor<TReadModel>, instance: TReadModel): Promise<TReadModel> {
|
|
164
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
165
|
+
const schema = this.getReadModelSchema(readModelType, readModel.identifier);
|
|
166
|
+
const payload = JsonSerializer.serialize(instance);
|
|
167
|
+
const subject = this.extractSubject(instance);
|
|
168
|
+
|
|
169
|
+
const response = await this._connection.compliance.release({
|
|
170
|
+
EventStore: this._eventStore,
|
|
171
|
+
Namespace: this._namespace,
|
|
172
|
+
Subject: subject,
|
|
173
|
+
Schema: schema,
|
|
174
|
+
Payload: payload
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (response.HasError) {
|
|
178
|
+
throw new Error(`Failed to release PII: ${response.Error}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.deserializeReadModel(readModelType, response.Payload);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @inheritdoc */
|
|
185
|
+
async releaseMany<TReadModel>(readModelType: Constructor<TReadModel>, instances: TReadModel[]): Promise<TReadModel[]> {
|
|
186
|
+
const releasePromises = instances.map(instance => this.release(readModelType, instance));
|
|
187
|
+
return Promise.all(releasePromises);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private resolveReadModels<TReadModel>(readModelType?: Constructor<TReadModel>): ResolvedReadModel[] {
|
|
191
|
+
const resolved = new Map<string, ResolvedReadModel>();
|
|
192
|
+
|
|
193
|
+
for (const projectionType of this._clientArtifacts.projections) {
|
|
194
|
+
const metadata = getProjectionMetadata(projectionType);
|
|
195
|
+
if (!metadata?.readModelType) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (readModelType && metadata.readModelType !== readModelType) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const readModelMetadata = getReadModelMetadata(metadata.readModelType);
|
|
204
|
+
const identifier = readModelMetadata?.id.value ?? metadata.readModelType.name;
|
|
205
|
+
resolved.set(identifier, {
|
|
206
|
+
type: metadata.readModelType,
|
|
207
|
+
identifier,
|
|
208
|
+
eventSequenceId: metadata.eventSequenceId ?? EventSequenceId.eventLog.value,
|
|
209
|
+
observerType: ContractReadModelObserverType.Projection,
|
|
210
|
+
observerIdentifier: metadata.id.value,
|
|
211
|
+
schema: this.getReadModelSchema(metadata.readModelType, identifier)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const modelBoundType of this._clientArtifacts.readModels) {
|
|
216
|
+
if (!hasFromEventMetadata(modelBoundType)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (readModelType && modelBoundType !== readModelType) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const metadata = getReadModelMetadata(modelBoundType);
|
|
225
|
+
if (!metadata) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
resolved.set(metadata.id.value, {
|
|
230
|
+
type: modelBoundType,
|
|
231
|
+
identifier: metadata.id.value,
|
|
232
|
+
eventSequenceId: EventSequenceId.eventLog.value,
|
|
233
|
+
observerType: ContractReadModelObserverType.Projection,
|
|
234
|
+
observerIdentifier: metadata.id.value,
|
|
235
|
+
schema: JSON.stringify(metadata.schema)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const reducerType of this._clientArtifacts.reducers) {
|
|
240
|
+
const metadata = getReducerMetadata(reducerType);
|
|
241
|
+
if (!metadata?.readModel) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (readModelType && metadata.readModel !== readModelType) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const readModelMetadata = getReadModelMetadata(metadata.readModel);
|
|
250
|
+
const identifier = readModelMetadata?.id.value ?? metadata.readModel.name;
|
|
251
|
+
resolved.set(identifier, {
|
|
252
|
+
type: metadata.readModel,
|
|
253
|
+
identifier,
|
|
254
|
+
eventSequenceId: metadata.eventSequenceId ?? EventSequenceId.eventLog.value,
|
|
255
|
+
observerType: ContractReadModelObserverType.Reducer,
|
|
256
|
+
observerIdentifier: metadata.id.value,
|
|
257
|
+
schema: this.getReadModelSchema(metadata.readModel, identifier)
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return Array.from(resolved.values());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private resolveReadModel<TReadModel>(readModelType: Constructor<TReadModel>): ResolvedReadModel {
|
|
265
|
+
const [resolved] = this.resolveReadModels(readModelType);
|
|
266
|
+
if (!resolved) {
|
|
267
|
+
throw new Error(`Unknown read model '${readModelType.name}'. Make sure it is decorated and discoverable.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return resolved;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private toDefinition(readModel: ResolvedReadModel) {
|
|
274
|
+
return {
|
|
275
|
+
Type: {
|
|
276
|
+
Identifier: readModel.identifier,
|
|
277
|
+
Generation: 1
|
|
278
|
+
},
|
|
279
|
+
ContainerName: readModel.identifier,
|
|
280
|
+
DisplayName: readModel.identifier,
|
|
281
|
+
Sink: {
|
|
282
|
+
ConfigurationId: toContractsGuid(WellKnownSinks.Null),
|
|
283
|
+
TypeId: toContractsGuid(WellKnownSinks.MongoDB)
|
|
284
|
+
},
|
|
285
|
+
Schema: readModel.schema,
|
|
286
|
+
Indexes: [],
|
|
287
|
+
ObserverType: readModel.observerType,
|
|
288
|
+
ObserverIdentifier: readModel.observerIdentifier,
|
|
289
|
+
Owner: 1,
|
|
290
|
+
Source: 1
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private getReadModelSchema(readModelType: Constructor, identifier: string): string {
|
|
295
|
+
const metadata = getReadModelMetadata(readModelType);
|
|
296
|
+
if (metadata) {
|
|
297
|
+
return JSON.stringify(metadata.schema);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return JSON.stringify(JsonSchemaGenerator.generate(readModelType) ?? JsonSchemaGenerator.createEmptySchema(identifier));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private deserializeReadModel<TReadModel>(readModelType: Constructor<TReadModel>, json: string): TReadModel {
|
|
304
|
+
if (!json) {
|
|
305
|
+
return Object.create(readModelType.prototype) as TReadModel;
|
|
306
|
+
}
|
|
307
|
+
return JsonSerializer.deserialize(readModelType as Constructor<object>, json) as TReadModel;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private schemaHasComplianceMetadata(schema: string): boolean {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(schema) as Record<string, unknown>;
|
|
313
|
+
const properties = parsed.properties as Record<string, Record<string, unknown>> | undefined;
|
|
314
|
+
if (!properties) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return Object.values(properties).some(
|
|
318
|
+
property => Array.isArray(property.compliance) && (property.compliance as unknown[]).length > 0
|
|
319
|
+
);
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private extractSubject<TReadModel>(instance: TReadModel): string {
|
|
326
|
+
// By convention, use the 'id' property as the subject
|
|
327
|
+
const anyInstance = instance as any;
|
|
328
|
+
if (anyInstance.id !== undefined && anyInstance.id !== null) {
|
|
329
|
+
return String(anyInstance.id);
|
|
330
|
+
}
|
|
331
|
+
throw new Error('Read model instance must have an "id" property to serve as the subject for PII release');
|
|
332
|
+
}
|
|
333
|
+
}
|
package/readModels/index.ts
CHANGED
|
@@ -2,5 +2,11 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
export { ReadModelId } from './ReadModelId';
|
|
5
|
+
export type { IReadModels } from './IReadModels';
|
|
6
|
+
export type { IMaterializedReadModels } from './IMaterializedReadModels';
|
|
7
|
+
export { ReadModels } from './ReadModels';
|
|
8
|
+
export { MaterializedReadModels } from './MaterializedReadModels';
|
|
9
|
+
export type { ReadModelChangeset } from './ReadModelChangeset';
|
|
10
|
+
export type { ReadModelSnapshot } from './ReadModelSnapshot';
|
|
5
11
|
export { readModel, getReadModelMetadata, isReadModel } from './readModel';
|
|
6
12
|
export type { ReadModelMetadata } from './readModel';
|
package/readModels/readModel.ts
CHANGED
|
@@ -33,7 +33,6 @@ export function readModel(id: string = ''): ClassDecorator {
|
|
|
33
33
|
return (target: object) => {
|
|
34
34
|
const constructor = target as Function;
|
|
35
35
|
const readModelId = new ReadModelId(id || constructor.name);
|
|
36
|
-
trackInstanceProperties(constructor);
|
|
37
36
|
const members = TypeIntrospector.getMembers(constructor);
|
|
38
37
|
const metadata: ReadModelMetadata = {
|
|
39
38
|
id: readModelId,
|
|
@@ -49,27 +48,6 @@ export function readModel(id: string = ''): ClassDecorator {
|
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
function trackInstanceProperties(constructor: Function): void {
|
|
53
|
-
const instance = tryCreateInstance(constructor);
|
|
54
|
-
if (!instance || typeof instance !== 'object') {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const propertyName of Object.keys(instance as Record<string, unknown>)) {
|
|
59
|
-
TypeIntrospector.trackProperty(constructor, propertyName);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function tryCreateInstance(constructor: Function): unknown {
|
|
64
|
-
try {
|
|
65
|
-
const parameterCount = constructor.length;
|
|
66
|
-
const parameters = Array.from({ length: parameterCount }, () => undefined);
|
|
67
|
-
return Reflect.construct(constructor as new (...args: unknown[]) => unknown, parameters);
|
|
68
|
-
} catch {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
51
|
/**
|
|
74
52
|
* Gets the {@link ReadModelMetadata} associated with a class decorated with {@link readModel}.
|
|
75
53
|
* @param target - The class constructor to retrieve metadata for.
|
package/reducers/Reducers.ts
CHANGED
|
@@ -159,7 +159,7 @@ export class Reducers implements IReducers {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const readModels = Array.from(this._reducers.entries()).map(([id, reducerType]) => {
|
|
162
|
-
const readModelName = (reducerType
|
|
162
|
+
const readModelName = this.getReducerReadModelIdentifier(reducerType);
|
|
163
163
|
return {
|
|
164
164
|
Type: {
|
|
165
165
|
Identifier: readModelName,
|
|
@@ -212,7 +212,7 @@ export class Reducers implements IReducers {
|
|
|
212
212
|
const metadata = getReducerMetadata(reducerType)!;
|
|
213
213
|
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
214
214
|
const eventTypes = this.getEventTypesFor(reducerType);
|
|
215
|
-
const readModelName = (reducerType
|
|
215
|
+
const readModelName = this.getReducerReadModelIdentifier(reducerType);
|
|
216
216
|
|
|
217
217
|
this._logger.info('Starting reducer observation', {
|
|
218
218
|
reducerId: id,
|
|
@@ -227,6 +227,15 @@ export class Reducers implements IReducers {
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
private getReducerReadModelIdentifier(reducerType: Constructor): string {
|
|
231
|
+
const metadata = getReducerMetadata(reducerType);
|
|
232
|
+
if (metadata?.readModel) {
|
|
233
|
+
return getReadModelMetadata(metadata.readModel)?.id.value ?? metadata.readModel.name;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (reducerType as Function).name;
|
|
237
|
+
}
|
|
238
|
+
|
|
230
239
|
private async observeReducer(
|
|
231
240
|
id: string,
|
|
232
241
|
reducerType: Constructor,
|
|
@@ -309,7 +318,7 @@ export class Reducers implements IReducers {
|
|
|
309
318
|
this._logger.info('Invoking reducer handler', {
|
|
310
319
|
reducerId: id,
|
|
311
320
|
method: entry.methodName,
|
|
312
|
-
sequenceNumber: event.Context!.SequenceNumber,
|
|
321
|
+
sequenceNumber: event.Context!.SequenceNumber.toString(),
|
|
313
322
|
eventTypeId,
|
|
314
323
|
hasState: currentState !== undefined
|
|
315
324
|
});
|
|
@@ -2,9 +2,10 @@
|
|
|
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 { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { ConceptAs, Guid } from '@cratis/fundamentals';
|
|
6
6
|
import { JsonSchema } from './JsonSchema';
|
|
7
7
|
import { TypeIntrospector } from '../types';
|
|
8
|
+
import { ComplianceMetadataResolver } from '../compliance/ComplianceMetadataResolver';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Generates JSON schemas for class constructors using reflection metadata.
|
|
@@ -44,12 +45,15 @@ export class JsonSchemaGenerator {
|
|
|
44
45
|
static generate(target: Function, members?: ReadonlyMap<string, Function | undefined>): JsonSchema {
|
|
45
46
|
const membersToUse = members ?? TypeIntrospector.getMembers(target);
|
|
46
47
|
const schemaProperties: Record<string, JsonSchema> = {};
|
|
48
|
+
const prototype = target.prototype;
|
|
47
49
|
|
|
48
50
|
for (const [memberName, memberType] of membersToUse.entries()) {
|
|
49
51
|
const propertySchema = this.mapRuntimeTypeToSchema(memberType);
|
|
50
52
|
// Only include properties whose type was resolved. An empty schema ({}) means
|
|
51
53
|
// the runtime type was unavailable (e.g. esbuild/tsx omits design:paramtypes).
|
|
52
54
|
if (Object.keys(propertySchema).length > 0) {
|
|
55
|
+
// Add compliance metadata to property schema if present (both property and type-level)
|
|
56
|
+
this.addComplianceMetadata(propertySchema, prototype, memberName, memberType);
|
|
53
57
|
schemaProperties[memberName] = propertySchema;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -94,6 +98,16 @@ export class JsonSchemaGenerator {
|
|
|
94
98
|
return {};
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
// A ConceptAs<T> serializes as its underlying primitive value, so the schema must
|
|
102
|
+
// describe that primitive rather than the wrapper object. The generic argument T is
|
|
103
|
+
// not available at runtime, so resolve it from the 'value' property's design:type
|
|
104
|
+
// when present (ts-node/webpack with emitDecoratorMetadata) and fall back to string
|
|
105
|
+
// when it is not (esbuild/tsx) — string-backed concepts are by far the common case.
|
|
106
|
+
if (this.isConceptAs(runtimeType)) {
|
|
107
|
+
const valueType = Reflect.getMetadata('design:type', runtimeType.prototype, 'value') as Function | undefined;
|
|
108
|
+
return this.mapRuntimeTypeToSchema(valueType ?? String);
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
if (runtimeType !== Object) {
|
|
98
112
|
return this.generate(runtimeType);
|
|
99
113
|
}
|
|
@@ -101,6 +115,19 @@ export class JsonSchemaGenerator {
|
|
|
101
115
|
return { type: 'object' };
|
|
102
116
|
}
|
|
103
117
|
|
|
118
|
+
private static isConceptAs(runtimeType: Function): boolean {
|
|
119
|
+
let current: Function | null = runtimeType;
|
|
120
|
+
while (current && current !== Function.prototype) {
|
|
121
|
+
if (current === ConceptAs) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
current = Object.getPrototypeOf(current) as Function | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
private static getKnownTypeFormat(runtimeType: Function | undefined): JsonSchema | undefined {
|
|
105
132
|
if (!runtimeType) {
|
|
106
133
|
return undefined;
|
|
@@ -121,4 +148,37 @@ export class JsonSchemaGenerator {
|
|
|
121
148
|
const normalized = format.toLowerCase();
|
|
122
149
|
return this._formatAliases.get(normalized) ?? normalized;
|
|
123
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Adds compliance metadata to a property schema if the property has compliance decorators.
|
|
154
|
+
* Also checks if the property's type itself is marked as PII (e.g., ConceptAs types).
|
|
155
|
+
* @param schema - The property schema to add compliance metadata to.
|
|
156
|
+
* @param target - The class prototype.
|
|
157
|
+
* @param propertyKey - The property name.
|
|
158
|
+
*/
|
|
159
|
+
private static addComplianceMetadata(schema: JsonSchema, target: object, propertyKey: string, propertyType?: Function): void {
|
|
160
|
+
const complianceArray: Array<{ metadataType: string; details: string }> = [];
|
|
161
|
+
|
|
162
|
+
// Check for property-level compliance decorators
|
|
163
|
+
if (ComplianceMetadataResolver.hasMetadataFor(target, propertyKey)) {
|
|
164
|
+
const propertyMetadata = ComplianceMetadataResolver.getMetadataFor(target, propertyKey);
|
|
165
|
+
complianceArray.push(...propertyMetadata.map(metadata => ({
|
|
166
|
+
metadataType: metadata.metadataType.value.toString(),
|
|
167
|
+
details: metadata.details
|
|
168
|
+
})));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for type-level compliance decorators (e.g., @pii on ConceptAs)
|
|
172
|
+
if (propertyType) {
|
|
173
|
+
const typeMetadata = ComplianceMetadataResolver.getMetadataForType(propertyType);
|
|
174
|
+
complianceArray.push(...typeMetadata.map(metadata => ({
|
|
175
|
+
metadataType: metadata.metadataType.value.toString(),
|
|
176
|
+
details: metadata.details
|
|
177
|
+
})));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (complianceArray.length > 0) {
|
|
181
|
+
(schema as Record<string, unknown>).compliance = complianceArray;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
124
184
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
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 { Constructor, Fields } from '@cratis/fundamentals';
|
|
5
6
|
|
|
6
7
|
/** Metadata key for tracked schema properties on a target type. */
|
|
7
8
|
const TRACKED_PROPERTIES_METADATA_KEY = 'chronicle:typeIntrospection:properties';
|
|
@@ -35,16 +36,35 @@ export class TypeIntrospector {
|
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Gets members and their runtime types for a class.
|
|
38
|
-
* Members are discovered from tracked properties
|
|
39
|
+
* Members are discovered from explicit `@field` decorators, tracked properties, the
|
|
40
|
+
* properties present on a default-constructed instance, and constructor parameters.
|
|
41
|
+
*
|
|
42
|
+
* `@field` declarations are the authoritative source: they carry the exact declared
|
|
43
|
+
* type (including `ConceptAs` types) regardless of whether the property has an
|
|
44
|
+
* initializer, so event types — whose properties are typically declared with definite
|
|
45
|
+
* assignment (`prop!: T`) and no default value — resolve their schema just like read
|
|
46
|
+
* models. Default-value and `design:type` resolution remain as fallbacks for plain
|
|
47
|
+
* classes that don't use `@field`, and keep working even under esbuild/tsx, which does
|
|
48
|
+
* not emit decorator metadata ('design:type'/'design:paramtypes').
|
|
39
49
|
* @param target - The class constructor to inspect.
|
|
40
50
|
* @returns A map of member name to runtime type.
|
|
41
51
|
*/
|
|
42
52
|
static getMembers(target: Function): Map<string, Function | undefined> {
|
|
43
53
|
const members = new Map<string, Function | undefined>();
|
|
44
54
|
const defaultValues = this.tryGetDefaultPropertyValues(target);
|
|
55
|
+
const fieldTypes = this.getFieldTypes(target);
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
const propertyNames = new Set<string>([
|
|
58
|
+
...this.getTrackedProperties(target),
|
|
59
|
+
...fieldTypes.keys(),
|
|
60
|
+
...Object.keys(defaultValues)
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
for (const property of propertyNames) {
|
|
64
|
+
let runtimeType = fieldTypes.get(property);
|
|
65
|
+
if (!runtimeType || runtimeType === Object) {
|
|
66
|
+
runtimeType = Reflect.getMetadata('design:type', target.prototype, property) as Function | undefined;
|
|
67
|
+
}
|
|
48
68
|
if (!runtimeType || runtimeType === Object) {
|
|
49
69
|
runtimeType = this.getRuntimeTypeFromValue(defaultValues[property]);
|
|
50
70
|
}
|
|
@@ -67,6 +87,15 @@ export class TypeIntrospector {
|
|
|
67
87
|
return members;
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
private static getFieldTypes(target: Function): Map<string, Function | undefined> {
|
|
91
|
+
const fieldTypes = new Map<string, Function | undefined>();
|
|
92
|
+
for (const field of Fields.getFieldsForType(target as Constructor)) {
|
|
93
|
+
fieldTypes.set(field.name, field.type);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return fieldTypes;
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
private static tryGetDefaultPropertyValues(target: Function): Record<string, unknown> {
|
|
71
100
|
try {
|
|
72
101
|
const parameters = Array.from({ length: target.length }, () => undefined);
|