@cratis/chronicle 6.9.1 → 6.10.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 +52 -13
- package/ChronicleOptions.spec.ts +54 -0
- package/ChronicleOptions.ts +20 -5
- package/EventStore.ts +7 -1
- package/README.md +2 -3
- package/artifacts/DefaultClientArtifactsProvider.ts +4 -1
- package/connection/ChronicleConnection.auth.spec.ts +219 -0
- package/connection/ChronicleConnection.ts +70 -31
- package/connection/ChronicleServices.ts +1 -1
- package/connection/CompatibilityPreflight.ts +1 -1
- package/connection/RejectedChronicleCredentials.ts +10 -0
- package/connection/TokenProvider.spec.ts +5 -6
- package/connection/TokenProvider.ts +26 -17
- package/connection/clientVersion.spec.ts +27 -0
- package/connection/clientVersion.ts +9 -0
- package/connection/describeRequestError.spec.ts +22 -0
- package/connection/fetchOAuthAccessToken.ts +33 -2
- package/dist/ChronicleClient.d.ts +3 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +55 -13
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +5 -1
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +20 -5
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +2 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +6 -1
- package/dist/EventStore.js.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +3 -3
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +74 -26
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +1 -2
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/connection/CompatibilityPreflight.js +1 -1
- package/dist/connection/CompatibilityPreflight.js.map +1 -1
- package/dist/connection/RejectedChronicleCredentials.d.ts +5 -0
- package/dist/connection/RejectedChronicleCredentials.d.ts.map +1 -0
- package/dist/connection/RejectedChronicleCredentials.js +10 -0
- package/dist/connection/RejectedChronicleCredentials.js.map +1 -0
- package/dist/connection/TokenProvider.d.ts +8 -6
- package/dist/connection/TokenProvider.d.ts.map +1 -1
- package/dist/connection/TokenProvider.js +25 -11
- package/dist/connection/TokenProvider.js.map +1 -1
- package/dist/connection/clientVersion.d.ts +3 -0
- package/dist/connection/clientVersion.d.ts.map +1 -0
- package/dist/connection/clientVersion.js +7 -0
- package/dist/connection/clientVersion.js.map +1 -0
- package/dist/connection/fetchOAuthAccessToken.d.ts +12 -0
- package/dist/connection/fetchOAuthAccessToken.d.ts.map +1 -1
- package/dist/connection/fetchOAuthAccessToken.js +37 -2
- package/dist/connection/fetchOAuthAccessToken.js.map +1 -1
- package/dist/externalServices/ExternalServiceBuilder.d.ts +1 -1
- package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
- package/dist/externalServices/ExternalServiceBuilder.js +1 -1
- package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
- package/dist/externalServices/ExternalServices.d.ts.map +1 -1
- package/dist/externalServices/ExternalServices.js +3 -1
- package/dist/externalServices/ExternalServices.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +9 -7
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/constantValueExpression.d.ts +3 -0
- package/dist/projections/constantValueExpression.d.ts.map +1 -0
- package/dist/projections/constantValueExpression.js +26 -0
- package/dist/projections/constantValueExpression.js.map +1 -0
- package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
- package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/ChildrenBuilder.js +5 -0
- package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
- package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/FromBuilder.js +3 -2
- package/dist/projections/declarative/FromBuilder.js.map +1 -1
- package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.js +2 -1
- package/dist/projections/declarative/JoinBuilder.js.map +1 -1
- package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/NestedBuilder.js +3 -1
- package/dist/projections/declarative/NestedBuilder.js.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
- package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
- package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
- package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/SetBuilder.js +2 -1
- package/dist/projections/declarative/SetBuilder.js.map +1 -1
- package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/addFrom.js +3 -2
- package/dist/projections/modelBound/addFrom.js.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
- package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenFrom.js +8 -4
- package/dist/projections/modelBound/childrenFrom.js.map +1 -1
- package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
- package/dist/projections/modelBound/clearWith.js +3 -2
- package/dist/projections/modelBound/clearWith.js.map +1 -1
- package/dist/projections/modelBound/count.d.ts.map +1 -1
- package/dist/projections/modelBound/count.js +3 -2
- package/dist/projections/modelBound/count.js.map +1 -1
- package/dist/projections/modelBound/decrement.d.ts.map +1 -1
- package/dist/projections/modelBound/decrement.js +3 -2
- package/dist/projections/modelBound/decrement.js.map +1 -1
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +3 -2
- package/dist/projections/modelBound/fromAll.js.map +1 -1
- package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
- package/dist/projections/modelBound/fromEvery.js +3 -2
- package/dist/projections/modelBound/fromEvery.js.map +1 -1
- package/dist/projections/modelBound/increment.d.ts.map +1 -1
- package/dist/projections/modelBound/increment.js +3 -2
- package/dist/projections/modelBound/increment.js.map +1 -1
- package/dist/projections/modelBound/join.d.ts.map +1 -1
- package/dist/projections/modelBound/join.js +3 -2
- package/dist/projections/modelBound/join.js.map +1 -1
- package/dist/projections/modelBound/nested.d.ts.map +1 -1
- package/dist/projections/modelBound/nested.js +3 -2
- package/dist/projections/modelBound/nested.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
- package/dist/projections/modelBound/noAutoMap.js +3 -2
- package/dist/projections/modelBound/noAutoMap.js.map +1 -1
- package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
- package/dist/projections/modelBound/removedWith.js +3 -2
- package/dist/projections/modelBound/removedWith.js.map +1 -1
- package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
- package/dist/projections/modelBound/removedWithJoin.js +3 -2
- package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
- package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/setFrom.js +3 -2
- package/dist/projections/modelBound/setFrom.js.map +1 -1
- package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
- package/dist/projections/modelBound/setFromContext.js +3 -2
- package/dist/projections/modelBound/setFromContext.js.map +1 -1
- package/dist/projections/modelBound/setValue.d.ts.map +1 -1
- package/dist/projections/modelBound/setValue.js +3 -2
- package/dist/projections/modelBound/setValue.js.map +1 -1
- package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/subtractFrom.js +3 -2
- package/dist/projections/modelBound/subtractFrom.js.map +1 -1
- package/dist/projections/notSetPropertyPath.d.ts +3 -0
- package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
- package/dist/projections/notSetPropertyPath.js +5 -0
- package/dist/projections/notSetPropertyPath.js.map +1 -0
- package/dist/reactors/Reactors.d.ts +4 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +24 -6
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/reactor.d.ts +2 -2
- package/dist/reactors/reactor.js +2 -2
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -4
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModels.d.ts +2 -1
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +16 -10
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/deserializeReadModel.d.ts +5 -0
- package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
- package/dist/readModels/deserializeReadModel.js +84 -0
- package/dist/readModels/deserializeReadModel.js.map +1 -0
- package/dist/readModels/rootReadModelTypes.d.ts +5 -0
- package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
- package/dist/readModels/rootReadModelTypes.js +69 -0
- package/dist/readModels/rootReadModelTypes.js.map +1 -0
- package/dist/reducers/Reducers.d.ts +4 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +24 -6
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +4 -4
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TypeDiscoverer.d.ts +8 -1
- package/dist/types/TypeDiscoverer.d.ts.map +1 -1
- package/dist/types/TypeDiscoverer.js +25 -5
- package/dist/types/TypeDiscoverer.js.map +1 -1
- package/dist/types/TypeIntrospector.d.ts.map +1 -1
- package/dist/types/TypeIntrospector.js +3 -2
- package/dist/types/TypeIntrospector.js.map +1 -1
- package/dist/types/conceptValueType.d.ts +4 -0
- package/dist/types/conceptValueType.d.ts.map +1 -0
- package/dist/types/conceptValueType.js +10 -0
- package/dist/types/conceptValueType.js.map +1 -0
- package/dist/types/modelBoundProperty.d.ts +6 -0
- package/dist/types/modelBoundProperty.d.ts.map +1 -0
- package/dist/types/modelBoundProperty.js +53 -0
- package/dist/types/modelBoundProperty.js.map +1 -0
- package/externalServices/ExternalServiceBuilder.ts +4 -4
- package/externalServices/ExternalServices.spec.ts +87 -0
- package/externalServices/ExternalServices.ts +3 -1
- package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
- package/index.ts +1 -0
- package/observation/observationDisposal.spec.ts +74 -0
- package/package.json +2 -1
- package/projections/Projections.childrenAndNested.spec.ts +2 -0
- package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
- package/projections/Projections.ts +9 -7
- package/projections/constantValueExpression.spec.ts +31 -0
- package/projections/constantValueExpression.ts +28 -0
- package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
- package/projections/declarative/ChildrenBuilder.ts +6 -0
- package/projections/declarative/FromBuilder.ts +3 -2
- package/projections/declarative/JoinBuilder.ts +2 -1
- package/projections/declarative/NestedBuilder.ts +3 -1
- package/projections/declarative/ProjectionBuilderCore.ts +20 -2
- package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
- package/projections/declarative/ProjectionBuilderFor.ts +7 -0
- package/projections/declarative/SetBuilder.ts +2 -1
- package/projections/modelBound/addFrom.ts +3 -2
- package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
- package/projections/modelBound/childrenFrom.ts +11 -5
- package/projections/modelBound/clearWith.ts +3 -2
- package/projections/modelBound/count.ts +3 -2
- package/projections/modelBound/decorators.standard.spec.ts +45 -0
- package/projections/modelBound/decrement.ts +3 -2
- package/projections/modelBound/fromAll.ts +3 -2
- package/projections/modelBound/fromEvery.ts +3 -2
- package/projections/modelBound/increment.ts +3 -2
- package/projections/modelBound/join.ts +3 -2
- package/projections/modelBound/nested.ts +3 -2
- package/projections/modelBound/noAutoMap.ts +3 -2
- package/projections/modelBound/removedWith.ts +3 -2
- package/projections/modelBound/removedWithJoin.ts +3 -2
- package/projections/modelBound/setFrom.ts +3 -2
- package/projections/modelBound/setFromContext.ts +3 -2
- package/projections/modelBound/setValue.ts +3 -2
- package/projections/modelBound/subtractFrom.ts +3 -2
- package/projections/notSetPropertyPath.ts +5 -0
- package/reactors/Reactors.ts +22 -7
- package/reactors/reactor.ts +2 -2
- package/readModels/MaterializedReadModels.spec.ts +22 -0
- package/readModels/MaterializedReadModels.ts +2 -4
- package/readModels/ReadModels.spec.ts +58 -1
- package/readModels/ReadModels.ts +17 -11
- package/readModels/conceptValueType.spec.ts +20 -0
- package/readModels/deserializeReadModel.spec.ts +84 -0
- package/readModels/deserializeReadModel.ts +82 -0
- package/readModels/discovery.spec.ts +27 -0
- package/readModels/rootReadModelTypes.spec.ts +115 -0
- package/readModels/rootReadModelTypes.ts +65 -0
- package/reducers/Reducers.ts +22 -7
- package/schemas/JsonSchemaGenerator.ts +4 -4
- package/schemas/legacyDesignMetadata.spec.ts +24 -2
- package/types/TypeDiscoverer.spec.ts +57 -0
- package/types/TypeDiscoverer.ts +27 -6
- package/types/TypeIntrospector.ts +3 -2
- package/types/conceptValueType.ts +11 -0
- package/types/modelBoundProperty.ts +53 -0
- package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
- package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
- package/dist/externalServices/ExternalServicesContracts.js +0 -654
- package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
- package/externalServices/ExternalServicesContracts.ts +0 -709
package/readModels/ReadModels.ts
CHANGED
|
@@ -24,9 +24,11 @@ import { JsonSchemaGenerator } from '../schemas/index.js';
|
|
|
24
24
|
import { WellKnownSinks } from '../sinks/index.js';
|
|
25
25
|
import { getReadModelMetadata, getReadModelId } from './readModel.js';
|
|
26
26
|
import { assertUniqueReadModelIds } from './assertUniqueReadModelIds.js';
|
|
27
|
+
import { rootReadModelTypes } from './rootReadModelTypes.js';
|
|
27
28
|
import type { IMaterializedReadModels } from './IMaterializedReadModels.js';
|
|
28
29
|
import { MaterializedReadModels } from './MaterializedReadModels.js';
|
|
29
30
|
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
|
|
31
|
+
import { deserializeReadModel } from './deserializeReadModel.js';
|
|
30
32
|
import type { IReadModels } from './IReadModels.js';
|
|
31
33
|
import type { ReadModelChangeset } from './ReadModelChangeset.js';
|
|
32
34
|
import type { ReadModelSnapshot } from './ReadModelSnapshot.js';
|
|
@@ -41,6 +43,7 @@ interface ResolvedReadModel {
|
|
|
41
43
|
readonly observerIdentifier: string;
|
|
42
44
|
readonly schema: string;
|
|
43
45
|
readonly isActive: boolean;
|
|
46
|
+
readonly isModelBound?: boolean;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -54,7 +57,8 @@ export class ReadModels implements IReadModels {
|
|
|
54
57
|
private readonly _namespace: string,
|
|
55
58
|
private readonly _connection: ChronicleConnection,
|
|
56
59
|
private readonly _clientArtifacts: IClientArtifactsProvider,
|
|
57
|
-
private readonly _defaultSinkTypeId: string
|
|
60
|
+
private readonly _defaultSinkTypeId: string,
|
|
61
|
+
private readonly _isModelBoundProjectionRegistered?: (readModelType: Constructor) => boolean
|
|
58
62
|
) {
|
|
59
63
|
this.materialized = new MaterializedReadModels(_eventStore, _namespace, _connection);
|
|
60
64
|
}
|
|
@@ -245,7 +249,8 @@ export class ReadModels implements IReadModels {
|
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
private resolveReadModels<TReadModel>(readModelType?: Constructor<TReadModel>): ResolvedReadModel[] {
|
|
248
|
-
|
|
252
|
+
const rootTypes = rootReadModelTypes(this._clientArtifacts);
|
|
253
|
+
assertUniqueReadModelIds(rootTypes);
|
|
249
254
|
const resolved = new Map<string, ResolvedReadModel>();
|
|
250
255
|
|
|
251
256
|
for (const projectionType of this._clientArtifacts.projections) {
|
|
@@ -270,7 +275,7 @@ export class ReadModels implements IReadModels {
|
|
|
270
275
|
});
|
|
271
276
|
}
|
|
272
277
|
|
|
273
|
-
for (const modelBoundType of
|
|
278
|
+
for (const modelBoundType of rootTypes) {
|
|
274
279
|
if (!hasFromEventMetadata(modelBoundType) && !hasModelBoundProperties(modelBoundType)) {
|
|
275
280
|
continue;
|
|
276
281
|
}
|
|
@@ -287,7 +292,8 @@ export class ReadModels implements IReadModels {
|
|
|
287
292
|
observerType: ContractReadModelObserverType.Projection,
|
|
288
293
|
observerIdentifier: identifier,
|
|
289
294
|
schema: this.getReadModelSchema(modelBoundType, identifier),
|
|
290
|
-
isActive: !isPassive(modelBoundType)
|
|
295
|
+
isActive: !isPassive(modelBoundType),
|
|
296
|
+
isModelBound: true
|
|
291
297
|
});
|
|
292
298
|
}
|
|
293
299
|
|
|
@@ -327,13 +333,16 @@ export class ReadModels implements IReadModels {
|
|
|
327
333
|
|
|
328
334
|
private resolveReadModel<TReadModel>(readModelType: Constructor<TReadModel>): ResolvedReadModel {
|
|
329
335
|
const [resolved] = this.resolveReadModels(readModelType);
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
const neverRegistered = resolved?.isModelBound && this._isModelBoundProjectionRegistered && !this._isModelBoundProjectionRegistered(readModelType);
|
|
337
|
+
if (!resolved || neverRegistered) {
|
|
338
|
+
throw new Error(hasModelBoundProperties(readModelType)
|
|
339
|
+
? `Unknown read model '${readModelType.name}'. It has model-bound property mappings but was not registered when the event store was created. With standard decorators, a class whose mappings are all on properties is only registered once an instance exists; add a class-level @fromEvent(...) decorator so it registers when its module loads.`
|
|
340
|
+
: `Unknown read model '${readModelType.name}'. Make sure it is discoverable through a projection, reducer, or model-bound mapping.`);
|
|
332
341
|
}
|
|
333
|
-
|
|
334
342
|
return resolved;
|
|
335
343
|
}
|
|
336
344
|
|
|
345
|
+
|
|
337
346
|
private toDefinition(readModel: ResolvedReadModel) {
|
|
338
347
|
return {
|
|
339
348
|
Type: {
|
|
@@ -367,10 +376,7 @@ export class ReadModels implements IReadModels {
|
|
|
367
376
|
}
|
|
368
377
|
|
|
369
378
|
private deserializeReadModel<TReadModel>(readModelType: Constructor<TReadModel>, json: string): TReadModel {
|
|
370
|
-
|
|
371
|
-
return Object.create(readModelType.prototype) as TReadModel;
|
|
372
|
-
}
|
|
373
|
-
return JsonSerializer.deserialize(readModelType as Constructor<object>, json) as TReadModel;
|
|
379
|
+
return deserializeReadModel(readModelType, json);
|
|
374
380
|
}
|
|
375
381
|
|
|
376
382
|
private schemaHasComplianceMetadata(schema: string): boolean {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ConceptAs, Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { deserializeReadModel } from './deserializeReadModel.js';
|
|
7
|
+
|
|
8
|
+
class Cid extends ConceptAs<Guid> { static readonly valueType = Guid; }
|
|
9
|
+
class BirthDate extends ConceptAs<Date> { static readonly valueType = Date; }
|
|
10
|
+
class Concepts { cid = new Cid(Guid.empty); birth = new BirthDate(new Date(0)); }
|
|
11
|
+
|
|
12
|
+
describe('concept value restoration', () => {
|
|
13
|
+
it('uses static valueType for Guid and Date without design metadata', () => {
|
|
14
|
+
const concepts = deserializeReadModel(Concepts, JSON.stringify({ cid: 'f417bba6-5737-488a-a225-37da46b96221', birth: '2025-01-02T00:00:00.000Z' }));
|
|
15
|
+
expect(concepts.cid).toBeInstanceOf(Cid);
|
|
16
|
+
expect(concepts.cid.value).toBeInstanceOf(Guid);
|
|
17
|
+
expect(concepts.birth).toBeInstanceOf(BirthDate);
|
|
18
|
+
expect(concepts.birth.value).toBeInstanceOf(Date);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -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 { ConceptAs, Guid, field } from '@cratis/fundamentals';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { TypeIntrospector } from '../types/TypeIntrospector.js';
|
|
8
|
+
import { deserializeReadModel } from './deserializeReadModel.js';
|
|
9
|
+
|
|
10
|
+
class Identity extends ConceptAs<Guid> {}
|
|
11
|
+
Reflect.defineMetadata('design:paramtypes', [Guid], Identity);
|
|
12
|
+
class Model {
|
|
13
|
+
date!: Date;
|
|
14
|
+
id!: Guid;
|
|
15
|
+
identity!: Identity;
|
|
16
|
+
enabled!: boolean;
|
|
17
|
+
get computed(): string { return 'computed'; }
|
|
18
|
+
}
|
|
19
|
+
for (const name of ['date', 'id', 'identity', 'enabled', 'computed']) TypeIntrospector.trackProperty(Model, name);
|
|
20
|
+
Reflect.defineMetadata('design:type', Date, Model.prototype, 'date');
|
|
21
|
+
Reflect.defineMetadata('design:type', Guid, Model.prototype, 'id');
|
|
22
|
+
Reflect.defineMetadata('design:type', Identity, Model.prototype, 'identity');
|
|
23
|
+
Reflect.defineMetadata('design:type', Boolean, Model.prototype, 'enabled');
|
|
24
|
+
|
|
25
|
+
class Inferred {
|
|
26
|
+
date = new Date(0);
|
|
27
|
+
id = Guid.empty;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class Address { street = ''; city = ''; }
|
|
31
|
+
class DecoratedAddress { street = ''; city = ''; }
|
|
32
|
+
field(String)(DecoratedAddress.prototype, 'street');
|
|
33
|
+
class Person { address = new Address(); decorated = new DecoratedAddress(); children: Address[] = []; }
|
|
34
|
+
field(Array, { genericArguments: [Address] })(Person.prototype, 'children');
|
|
35
|
+
|
|
36
|
+
const guid = 'f417bba6-5737-488a-a225-37da46b96221';
|
|
37
|
+
|
|
38
|
+
describe('deserializeReadModel', () => {
|
|
39
|
+
it('restores declared values, concepts and false strings while skipping read-only accessors', () => {
|
|
40
|
+
const model = deserializeReadModel(Model, JSON.stringify({
|
|
41
|
+
date: '2025-01-02T00:00:00.000Z', id: guid, identity: guid, enabled: 'false', computed: 'stored'
|
|
42
|
+
}));
|
|
43
|
+
expect(model.date).toBeInstanceOf(Date);
|
|
44
|
+
expect(model.id).toBeInstanceOf(Guid);
|
|
45
|
+
expect(model.identity.value).toBeInstanceOf(Guid);
|
|
46
|
+
expect(model.enabled).toBe(false);
|
|
47
|
+
expect(model.computed).toBe('computed');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('uses inferred member types without legacy design metadata', () => {
|
|
51
|
+
const model = deserializeReadModel(Inferred, JSON.stringify({ date: '2025-01-02T00:00:00.000Z', id: guid }));
|
|
52
|
+
expect(model.date).toBeInstanceOf(Date);
|
|
53
|
+
expect(model.id).toBeInstanceOf(Guid);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('restores nested plain members, mixed decorated members and typed child arrays', () => {
|
|
57
|
+
const person = deserializeReadModel(Person, JSON.stringify({
|
|
58
|
+
address: { street: 'Main', city: 'Oslo' },
|
|
59
|
+
decorated: { street: 'Oak', city: 'Rome' },
|
|
60
|
+
children: [{ street: 'First', city: 'Paris' }, { street: 'Second', city: 'Berlin' }]
|
|
61
|
+
}));
|
|
62
|
+
expect(person.address).toBeInstanceOf(Address);
|
|
63
|
+
expect(person.address).toEqual({ street: 'Main', city: 'Oslo' });
|
|
64
|
+
expect(person.decorated).toBeInstanceOf(DecoratedAddress);
|
|
65
|
+
expect(person.decorated).toEqual({ street: 'Oak', city: 'Rome' });
|
|
66
|
+
expect(person.children).toHaveLength(2);
|
|
67
|
+
expect(person.children[0]).toBeInstanceOf(Address);
|
|
68
|
+
expect(person.children[1]).toEqual({ street: 'Second', city: 'Berlin' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('introspects members only once for multiple rows', () => {
|
|
72
|
+
const getMembers = TypeIntrospector.getMembers;
|
|
73
|
+
let calls = 0;
|
|
74
|
+
TypeIntrospector.getMembers = function (...args) { calls++; return getMembers.apply(this, args); };
|
|
75
|
+
try {
|
|
76
|
+
class Rows { enabled = false; }
|
|
77
|
+
deserializeReadModel(Rows, '{"enabled":false}');
|
|
78
|
+
deserializeReadModel(Rows, '{"enabled":false}');
|
|
79
|
+
expect(calls).toBe(1);
|
|
80
|
+
} finally {
|
|
81
|
+
TypeIntrospector.getMembers = getMembers;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
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 { conceptAsTypeKey, type Constructor, Fields, Guid, JsonSerializer, typeKeyOf } from '@cratis/fundamentals';
|
|
6
|
+
import { TypeIntrospector } from '../types/TypeIntrospector.js';
|
|
7
|
+
import { conceptValueType } from '../types/conceptValueType.js';
|
|
8
|
+
import { getChildrenFromMetadata } from '../projections/modelBound/childrenFrom.js';
|
|
9
|
+
|
|
10
|
+
const memberCache = new WeakMap<Function, Map<string, Function | undefined>>();
|
|
11
|
+
|
|
12
|
+
function membersFor(type: Function): Map<string, Function | undefined> {
|
|
13
|
+
let members = memberCache.get(type);
|
|
14
|
+
if (!members) {
|
|
15
|
+
members = TypeIntrospector.getMembers(type);
|
|
16
|
+
memberCache.set(type, members);
|
|
17
|
+
}
|
|
18
|
+
return members;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writable(type: Function, name: string): boolean {
|
|
22
|
+
let prototype = type.prototype;
|
|
23
|
+
while (prototype) {
|
|
24
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
25
|
+
if (descriptor) return !descriptor.get || !!descriptor.set;
|
|
26
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convert(type: Function, value: unknown): unknown {
|
|
32
|
+
if (value === null || value === undefined) return value;
|
|
33
|
+
if (typeKeyOf(type as Constructor) === conceptAsTypeKey) {
|
|
34
|
+
const underlying = conceptValueType(type);
|
|
35
|
+
return Reflect.construct(type, [underlying ? convert(underlying, value) : value]);
|
|
36
|
+
}
|
|
37
|
+
if (type === Boolean) return typeof value === 'string' && /^(true|false)$/i.test(value) ? value.toLowerCase() === 'true' : value;
|
|
38
|
+
if (type === Number) return Number(value);
|
|
39
|
+
if (type === String) return String(value);
|
|
40
|
+
if (type === Date || type === Guid || typeKeyOf(type as Constructor) === 'Guid') {
|
|
41
|
+
return JsonSerializer.deserializeFromInstance(type as Constructor<object>, value);
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'object' && type !== Array && type !== Object && !Array.isArray(value)) {
|
|
44
|
+
return restore(type as Constructor<object>, value as Record<string, unknown>);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function restore<T>(type: Constructor<T>, stored: Record<string, unknown> | null): T {
|
|
50
|
+
const fields = Fields.getFieldsForType(type);
|
|
51
|
+
const instance = fields.length > 0
|
|
52
|
+
? JsonSerializer.deserializeFromInstance(type as Constructor<object>, stored) as T
|
|
53
|
+
: Reflect.construct(type, []) as T;
|
|
54
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return instance;
|
|
55
|
+
|
|
56
|
+
const fieldByName = new Map(fields.map(field => [field.name, field]));
|
|
57
|
+
const result = instance as Record<string, unknown>;
|
|
58
|
+
for (const [name, runtimeType] of membersFor(type)) {
|
|
59
|
+
if (name.startsWith('__') || !writable(type, name) || !Object.prototype.hasOwnProperty.call(stored, name)) continue;
|
|
60
|
+
const value = stored[name];
|
|
61
|
+
const field = fieldByName.get(name);
|
|
62
|
+
const childType = getChildrenFromMetadata(type.prototype, name).find(metadata => metadata.childType)?.childType ?? field?.genericArguments?.[0];
|
|
63
|
+
if (Array.isArray(value) && childType) {
|
|
64
|
+
result[name] = value.map(item => convert(childType, item));
|
|
65
|
+
} else if (field && (value === null || value === undefined || !runtimeType || runtimeType === Array || runtimeType === Object || typeof value !== 'object')) {
|
|
66
|
+
// The serializer has already handled decorated scalar fields.
|
|
67
|
+
if (value === null || value === undefined) result[name] = value;
|
|
68
|
+
} else {
|
|
69
|
+
result[name] = value === null || value === undefined || !runtimeType ? value : convert(runtimeType, value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return instance;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Restores declared model members not covered by Fundamentals' @field-based deserializer. */
|
|
76
|
+
export function deserializeReadModel<TReadModel>(readModelType: Constructor<TReadModel>, json: string): TReadModel {
|
|
77
|
+
if (!json) {
|
|
78
|
+
return Object.create(readModelType.prototype) as TReadModel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return restore(readModelType, JSON.parse(json) as Record<string, unknown> | null);
|
|
82
|
+
}
|
|
@@ -67,6 +67,33 @@ describe('when discovering read models from observers', () => {
|
|
|
67
67
|
expect(JSON.parse(definition.Schema).properties.value.type).toBe('string');
|
|
68
68
|
expect(register.mock.calls.some(call => call[0].Projections[0].ReadModel === 'MappedOnly')).toBe(true);
|
|
69
69
|
});
|
|
70
|
+
it('should register a property-bound model without file discovery', async () => {
|
|
71
|
+
class DecoratedOnly { value = ''; }
|
|
72
|
+
setFrom(Event)(DecoratedOnly.prototype, 'value');
|
|
73
|
+
setFrom(Event)(DecoratedOnly.prototype, 'value');
|
|
74
|
+
expect(DefaultClientArtifactsProvider.default.readModels.filter(type => type === DecoratedOnly)).toHaveLength(1);
|
|
75
|
+
const { connection, registerMany } = createConnection();
|
|
76
|
+
await new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink').register(DecoratedOnly);
|
|
77
|
+
expect(registerMany.mock.calls[0][0].ReadModels[0].Type.Identifier).toBe('DecoratedOnly');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should refuse to query a model-bound read model whose projection was never registered', async () => {
|
|
81
|
+
class RegisteredLate { value = ''; }
|
|
82
|
+
setFrom(Event)(RegisteredLate.prototype, 'value');
|
|
83
|
+
const { connection } = createConnection();
|
|
84
|
+
const readModels = new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink', () => false);
|
|
85
|
+
await expect(readModels.findInstanceById(RegisteredLate, 'key')).rejects.toThrow(/add a class-level @fromEvent/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should explain how to register an unregistered property-bound model', async () => {
|
|
89
|
+
class NeverRegistered { value = ''; }
|
|
90
|
+
Reflect.defineMetadata('chronicle:projection:setFrom', [{ eventType: Event }], NeverRegistered.prototype, 'value');
|
|
91
|
+
Reflect.defineMetadata('chronicle:typeIntrospection:properties', ['value'], NeverRegistered);
|
|
92
|
+
const { connection } = createConnection();
|
|
93
|
+
await expect(new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink').findInstanceById(NeverRegistered, 'key'))
|
|
94
|
+
.rejects.toThrow(/add a class-level @fromEvent/);
|
|
95
|
+
});
|
|
96
|
+
|
|
70
97
|
it('should deduplicate inferred types and retain custom identifiers', async () => {
|
|
71
98
|
const provider = DefaultClientArtifactsProvider.default;
|
|
72
99
|
for (const type of [ModelBound, Projected, Reduced]) {
|
|
@@ -0,0 +1,115 @@
|
|
|
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 } from 'vitest';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
|
|
8
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
9
|
+
import { childrenFrom } from '../projections/modelBound/childrenFrom.js';
|
|
10
|
+
import { nested } from '../projections/modelBound/nested.js';
|
|
11
|
+
import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
|
|
12
|
+
import { DecoratorType } from '../types/DecoratorType.js';
|
|
13
|
+
import { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
|
|
14
|
+
import { JsonSchemaGenerator } from '../schemas/JsonSchemaGenerator.js';
|
|
15
|
+
import { projection } from '../projections/declarative/projection.js';
|
|
16
|
+
import type { IProjectionBuilderFor } from '../projections/declarative/IProjectionBuilderFor.js';
|
|
17
|
+
import { setFrom } from '../projections/modelBound/setFrom.js';
|
|
18
|
+
import { rootReadModelTypes } from './rootReadModelTypes.js';
|
|
19
|
+
import { Projections } from '../projections/Projections.js';
|
|
20
|
+
import type { ChronicleConnection } from '../connection/ChronicleConnection.js';
|
|
21
|
+
import { assertUniqueReadModelIds } from './assertUniqueReadModelIds.js';
|
|
22
|
+
|
|
23
|
+
class Added { value!: string; }
|
|
24
|
+
eventType()(Added);
|
|
25
|
+
|
|
26
|
+
function parentWithChild() {
|
|
27
|
+
class Item { value!: string; }
|
|
28
|
+
setFrom(Added)(Item.prototype, 'value');
|
|
29
|
+
class Parent { items!: Item[]; }
|
|
30
|
+
field(Array, { genericArguments: [Item] })(Parent.prototype, 'items');
|
|
31
|
+
childrenFrom(Added)(Parent.prototype, 'items');
|
|
32
|
+
return { Parent, Item };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('root model-bound discovery', () => {
|
|
36
|
+
it('discovers only the parent as a root projection', async () => {
|
|
37
|
+
const { Parent, Item } = parentWithChild();
|
|
38
|
+
const artifacts = { readModels: [Parent, Item], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
39
|
+
const projections = new Projections('test', 'Default', {} as ChronicleConnection, artifacts, 'sink');
|
|
40
|
+
await projections.discover();
|
|
41
|
+
expect(projections.hasFor('Parent')).toBe(true);
|
|
42
|
+
expect(projections.hasFor('Item')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not register a child with its own setFrom as a root', () => {
|
|
46
|
+
const { Parent, Item } = parentWithChild();
|
|
47
|
+
const artifacts = { readModels: [Parent, Item], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
48
|
+
expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('resolves children from decorator metadata without design metadata', () => {
|
|
52
|
+
class Item { value = ''; }
|
|
53
|
+
setFrom(Added)(Item.prototype, 'value');
|
|
54
|
+
class Parent { items: Item[] = []; }
|
|
55
|
+
childrenFrom(Added, Item)(Parent.prototype, 'items');
|
|
56
|
+
const artifacts = { readModels: [Parent, Item], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
57
|
+
expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
|
|
58
|
+
expect(JsonSchemaGenerator.generate(Parent, undefined, true).properties?.items.items?.properties?.value.type).toBe('string');
|
|
59
|
+
const projections = new Projections('test', 'Default', {} as ChronicleConnection, artifacts, 'sink');
|
|
60
|
+
return projections.discover().then(() => {
|
|
61
|
+
expect(projections.hasFor('Parent')).toBe(true);
|
|
62
|
+
expect(projections.hasFor('Item')).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('excludes nested default-instance members and their grandchildren', () => {
|
|
67
|
+
class Leaf { value = ''; }
|
|
68
|
+
setFrom(Added)(Leaf.prototype, 'value');
|
|
69
|
+
class Address { leaf = new Leaf(); }
|
|
70
|
+
nested(Address.prototype, 'leaf');
|
|
71
|
+
class Parent { addr = new Address(); }
|
|
72
|
+
nested(Parent.prototype, 'addr');
|
|
73
|
+
const artifacts = { readModels: [Parent, Address, Leaf], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
74
|
+
expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('excludes a typed child referenced only by a declarative children builder', () => {
|
|
78
|
+
class Child { value = ''; }
|
|
79
|
+
setFrom(Added)(Child.prototype, 'value');
|
|
80
|
+
class Parent { children: Child[] = []; }
|
|
81
|
+
field(Array, { genericArguments: [Child] })(Parent.prototype, 'children');
|
|
82
|
+
class ParentProjection {
|
|
83
|
+
define(builder: IProjectionBuilderFor<Parent>) { builder.children<Child>(parent => parent.children, () => {}); }
|
|
84
|
+
}
|
|
85
|
+
projection('parent-projection', Parent)(ParentProjection);
|
|
86
|
+
const artifacts = { readModels: [Parent, Child], projections: [ParentProjection], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
87
|
+
expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('retains a self-referencing tree as a root', () => {
|
|
91
|
+
class Node { children: Node[] = []; }
|
|
92
|
+
childrenFrom(Added, Node)(Node.prototype, 'children');
|
|
93
|
+
const artifacts = { readModels: [Node], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
94
|
+
expect(rootReadModelTypes(artifacts)).toEqual([Node]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not eagerly register a property-only child as a root', () => {
|
|
98
|
+
const discoverer = new TypeDiscoverer();
|
|
99
|
+
const before = discoverer.getTypesByDecoratorType(DecoratorType.ReadModel);
|
|
100
|
+
const { Parent, Item } = parentWithChild();
|
|
101
|
+
expect(discoverer.getTypesByDecoratorType(DecoratorType.ReadModel)).toEqual(before);
|
|
102
|
+
const artifacts = new DefaultClientArtifactsProvider(discoverer);
|
|
103
|
+
expect(rootReadModelTypes(artifacts)).toContain(Parent);
|
|
104
|
+
expect(rootReadModelTypes(artifacts)).not.toContain(Item);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('permits equal-named child types under different parents', () => {
|
|
108
|
+
const first = parentWithChild();
|
|
109
|
+
const second = parentWithChild();
|
|
110
|
+
Object.defineProperty(second.Parent, 'name', { value: 'OtherParent' });
|
|
111
|
+
const artifacts = { readModels: [first.Parent, first.Item, second.Item, second.Parent], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
112
|
+
expect(() => assertUniqueReadModelIds(rootReadModelTypes(artifacts))).not.toThrow();
|
|
113
|
+
expect(rootReadModelTypes(artifacts)).toEqual([first.Parent, second.Parent]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
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 type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
|
|
6
|
+
import { getProjectionMetadata } from '../projections/declarative/projection.js';
|
|
7
|
+
import { ProjectionBuilderFor } from '../projections/declarative/ProjectionBuilderFor.js';
|
|
8
|
+
import type { IProjectionFor } from '../projections/declarative/IProjectionFor.js';
|
|
9
|
+
import type { ChildrenDefinitionLike } from '../projections/modelBound/childrenAndNestedBuilder.js';
|
|
10
|
+
import { getChildrenFromMetadata } from '../projections/modelBound/childrenFrom.js';
|
|
11
|
+
import { resolveChildElementType, resolveNestedType } from '../projections/modelBound/childrenAndNestedBuilder.js';
|
|
12
|
+
import { isNested } from '../projections/modelBound/nested.js';
|
|
13
|
+
import { getReducerMetadata } from '../reducers/reducer.js';
|
|
14
|
+
import { TypeIntrospector } from '../types/TypeIntrospector.js';
|
|
15
|
+
|
|
16
|
+
/** Excludes models used as children or nested objects from root projection registration. */
|
|
17
|
+
export function rootReadModelTypes(artifacts: IClientArtifactsProvider): Constructor[] {
|
|
18
|
+
const models = artifacts.readModels;
|
|
19
|
+
const usedAsChildren = new Set<Function>();
|
|
20
|
+
const visited = new Set<Function>();
|
|
21
|
+
const inspect = (type: Function): void => {
|
|
22
|
+
if (visited.has(type)) return;
|
|
23
|
+
visited.add(type);
|
|
24
|
+
for (const property of TypeIntrospector.getTrackedProperties(type)) {
|
|
25
|
+
const child = getChildrenFromMetadata(type.prototype, property).length > 0;
|
|
26
|
+
const nested = isNested(type.prototype, property);
|
|
27
|
+
if (!child && !nested) continue;
|
|
28
|
+
const referenced = child ? resolveChildElementType(type, property) : resolveNestedType(type, property);
|
|
29
|
+
if (referenced && referenced !== type && referenced !== Array && referenced !== Object) {
|
|
30
|
+
usedAsChildren.add(referenced);
|
|
31
|
+
inspect(referenced);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
for (const type of models) inspect(type);
|
|
36
|
+
const inspectDeclarative = (type: Function, definitions: { children: Record<string, ChildrenDefinitionLike>; nested: Record<string, ChildrenDefinitionLike> }): void => {
|
|
37
|
+
for (const [property, definition] of Object.entries(definitions.children)) {
|
|
38
|
+
const referenced = resolveChildElementType(type, property);
|
|
39
|
+
if (!referenced) continue;
|
|
40
|
+
if (referenced !== type) usedAsChildren.add(referenced);
|
|
41
|
+
inspect(referenced);
|
|
42
|
+
inspectDeclarative(referenced, { children: definition.Children, nested: definition.Nested });
|
|
43
|
+
}
|
|
44
|
+
for (const [property, definition] of Object.entries(definitions.nested)) {
|
|
45
|
+
const referenced = resolveNestedType(type, property);
|
|
46
|
+
if (!referenced) continue;
|
|
47
|
+
if (referenced !== type) usedAsChildren.add(referenced);
|
|
48
|
+
inspect(referenced);
|
|
49
|
+
inspectDeclarative(referenced, { children: definition.Children, nested: definition.Nested });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
for (const type of artifacts.projections) {
|
|
53
|
+
const model = getProjectionMetadata(type)?.readModelType;
|
|
54
|
+
if (!model) continue;
|
|
55
|
+
inspect(model);
|
|
56
|
+
const builder = new ProjectionBuilderFor<unknown>();
|
|
57
|
+
(new type() as IProjectionFor<unknown>).define(builder);
|
|
58
|
+
inspectDeclarative(model, builder.getSubobjectDefinitions());
|
|
59
|
+
}
|
|
60
|
+
for (const type of artifacts.reducers) {
|
|
61
|
+
const model = getReducerMetadata(type)?.readModel;
|
|
62
|
+
if (model) inspect(model);
|
|
63
|
+
}
|
|
64
|
+
return models.filter(type => !usedAsChildren.has(type));
|
|
65
|
+
}
|
package/reducers/Reducers.ts
CHANGED
|
@@ -104,7 +104,9 @@ export class Reducers implements IReducers {
|
|
|
104
104
|
private readonly _lifecycle: ConnectionLifecycle;
|
|
105
105
|
private readonly _reducers = new Map<string, Constructor>();
|
|
106
106
|
private readonly _queues = new Map<string, AsyncQueue<ReducerMessage>>();
|
|
107
|
+
private readonly _observations = new Map<string, AbortController>();
|
|
107
108
|
private _registered = false;
|
|
109
|
+
private _disposed = false;
|
|
108
110
|
|
|
109
111
|
/**
|
|
110
112
|
* Creates a new {@link Reducers} instance.
|
|
@@ -131,6 +133,13 @@ export class Reducers implements IReducers {
|
|
|
131
133
|
});
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
/** Stops observations permanently when the owning client is disposed. */
|
|
137
|
+
dispose(): void {
|
|
138
|
+
this._disposed = true;
|
|
139
|
+
this._registered = false;
|
|
140
|
+
this.disconnectAll();
|
|
141
|
+
}
|
|
142
|
+
|
|
134
143
|
/** @inheritdoc */
|
|
135
144
|
async discover(): Promise<void> {
|
|
136
145
|
this._reducers.clear();
|
|
@@ -145,7 +154,7 @@ export class Reducers implements IReducers {
|
|
|
145
154
|
|
|
146
155
|
/** @inheritdoc */
|
|
147
156
|
async register(): Promise<void> {
|
|
148
|
-
if (this._registered) {
|
|
157
|
+
if (this._registered || this._disposed) {
|
|
149
158
|
return;
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -156,6 +165,7 @@ export class Reducers implements IReducers {
|
|
|
156
165
|
assertUniqueReadModelIds(this._clientArtifacts.readModels);
|
|
157
166
|
await this.registerReadModels();
|
|
158
167
|
|
|
168
|
+
if (this._disposed) return;
|
|
159
169
|
this._logger.info('Registering reducers', { count: this._reducers.size });
|
|
160
170
|
for (const [id, reducerType] of this._reducers) {
|
|
161
171
|
this.startObservation(id, reducerType);
|
|
@@ -226,6 +236,7 @@ export class Reducers implements IReducers {
|
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
private startObservation(id: string, reducerType: Constructor): void {
|
|
239
|
+
if (this._disposed) return;
|
|
229
240
|
const metadata = getReducerMetadata(reducerType)!;
|
|
230
241
|
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
231
242
|
const eventTypes = this.getEventTypesFor(reducerType);
|
|
@@ -271,12 +282,12 @@ export class Reducers implements IReducers {
|
|
|
271
282
|
private scheduleReobserve(id: string, reducerType: Constructor): void {
|
|
272
283
|
// A disconnect clears the registration; the reconnect re-registers every
|
|
273
284
|
// reducer from scratch, so retrying here as well would double up.
|
|
274
|
-
if (!this._registered) {
|
|
285
|
+
if (!this._registered || this._disposed) {
|
|
275
286
|
return;
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
const handle = setTimeout(() => {
|
|
279
|
-
if (!this._registered) {
|
|
290
|
+
if (!this._registered || this._disposed) {
|
|
280
291
|
return;
|
|
281
292
|
}
|
|
282
293
|
|
|
@@ -304,7 +315,9 @@ export class Reducers implements IReducers {
|
|
|
304
315
|
readModelName: string
|
|
305
316
|
): Promise<void> {
|
|
306
317
|
const queue = new AsyncQueue<ReducerMessage>();
|
|
318
|
+
const controller = new AbortController();
|
|
307
319
|
this._queues.set(id, queue);
|
|
320
|
+
this._observations.set(id, controller);
|
|
308
321
|
const isActive = getReducerMetadata(reducerType)?.isActive ?? true;
|
|
309
322
|
|
|
310
323
|
queue.send({
|
|
@@ -338,7 +351,7 @@ export class Reducers implements IReducers {
|
|
|
338
351
|
try {
|
|
339
352
|
const reducerInstance = new (reducerType as new () => Record<string, Function>)();
|
|
340
353
|
|
|
341
|
-
for await (const operation of this._connection.reducers.observe(queue)) {
|
|
354
|
+
for await (const operation of this._connection.reducers.observe(queue, { signal: controller.signal })) {
|
|
342
355
|
let lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
343
356
|
let state = ObservationState.Success;
|
|
344
357
|
const exceptionMessages: string[] = [];
|
|
@@ -433,7 +446,9 @@ export class Reducers implements IReducers {
|
|
|
433
446
|
// leak a duplicate stream on every reconnect.
|
|
434
447
|
if (this._queues.get(id) === queue) {
|
|
435
448
|
this._queues.delete(id);
|
|
449
|
+
this._observations.delete(id);
|
|
436
450
|
}
|
|
451
|
+
queue.complete();
|
|
437
452
|
}
|
|
438
453
|
}
|
|
439
454
|
|
|
@@ -461,9 +476,9 @@ export class Reducers implements IReducers {
|
|
|
461
476
|
}
|
|
462
477
|
|
|
463
478
|
private disconnectAll(): void {
|
|
464
|
-
for (const
|
|
465
|
-
|
|
466
|
-
|
|
479
|
+
for (const controller of this._observations.values()) controller.abort();
|
|
480
|
+
for (const queue of this._queues.values()) queue.complete();
|
|
481
|
+
this._observations.clear();
|
|
467
482
|
this._queues.clear();
|
|
468
483
|
}
|
|
469
484
|
}
|
|
@@ -5,6 +5,8 @@ import 'reflect-metadata';
|
|
|
5
5
|
import { conceptAsTypeKey, Constructor, Fields, Guid, typeKeyOf } from '@cratis/fundamentals';
|
|
6
6
|
import { ComplianceSchemaMetadata, JsonSchema, SecuritySchemaMetadata } from './JsonSchema.js';
|
|
7
7
|
import { TypeIntrospector } from '../types/index.js';
|
|
8
|
+
import { conceptValueType } from '../types/conceptValueType.js';
|
|
9
|
+
import { getChildrenFromMetadata } from '../projections/modelBound/childrenFrom.js';
|
|
8
10
|
import { hasOwnStandardMetadata } from '../types/standardDecoratorMetadata.js';
|
|
9
11
|
import { ComplianceMetadata } from '../compliance/ComplianceMetadata.js';
|
|
10
12
|
import { ComplianceMetadataResolver } from '../compliance/ComplianceMetadataResolver.js';
|
|
@@ -124,9 +126,7 @@ export class JsonSchemaGenerator {
|
|
|
124
126
|
// TypeScript erases ConceptAs<T>'s primitive type. Prefer an explicit field or
|
|
125
127
|
// static hint; legacy emitDecoratorMetadata is the last available source.
|
|
126
128
|
if (this.isConceptAs(runtimeType)) {
|
|
127
|
-
const
|
|
128
|
-
const fieldType = Fields.getFieldsForType(concept as Constructor).find(field => field.name === 'value')?.type;
|
|
129
|
-
const valueType = fieldType ?? concept.valueType ?? Reflect.getMetadata('design:type', runtimeType.prototype, 'value') as Function | undefined;
|
|
129
|
+
const valueType = conceptValueType(runtimeType);
|
|
130
130
|
if (!valueType && !requireResolvedTypes) return { type: 'string' }; // Legacy schema compatibility.
|
|
131
131
|
const valueSchema = this.mapRuntimeTypeToSchema(valueType, undefined, undefined, requireResolvedTypes);
|
|
132
132
|
if (!valueType || !valueSchema.type || (requireResolvedTypes && valueSchema.type === 'object')) {
|
|
@@ -191,7 +191,7 @@ export class JsonSchemaGenerator {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
const field = Fields.getFieldsForType(declaringType as Constructor).find(candidate => candidate.name === propertyName);
|
|
194
|
-
return field?.genericArguments?.[0];
|
|
194
|
+
return getChildrenFromMetadata(declaringType.prototype, propertyName).find(metadata => metadata.childType)?.childType ?? field?.genericArguments?.[0];
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|