@cratis/chronicle 6.3.2 → 6.4.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/EventStore.ts +3 -0
- package/README.md +13 -2
- package/artifacts/validateArtifactSchemas.spec.ts +61 -0
- package/artifacts/validateArtifactSchemas.ts +39 -0
- package/compliance/pii.ts +7 -6
- package/compliance/subject.ts +6 -5
- package/confidentiality/encrypted.ts +7 -6
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +2 -0
- package/dist/EventStore.js.map +1 -1
- package/dist/artifacts/validateArtifactSchemas.d.ts +4 -0
- package/dist/artifacts/validateArtifactSchemas.d.ts.map +1 -0
- package/dist/artifacts/validateArtifactSchemas.js +38 -0
- package/dist/artifacts/validateArtifactSchemas.js.map +1 -0
- package/dist/compliance/pii.d.ts +2 -1
- package/dist/compliance/pii.d.ts.map +1 -1
- package/dist/compliance/pii.js +6 -5
- package/dist/compliance/pii.js.map +1 -1
- package/dist/compliance/subject.d.ts +2 -1
- package/dist/compliance/subject.d.ts.map +1 -1
- package/dist/compliance/subject.js +5 -4
- package/dist/compliance/subject.js.map +1 -1
- package/dist/confidentiality/encrypted.d.ts +2 -1
- package/dist/confidentiality/encrypted.d.ts.map +1 -1
- package/dist/confidentiality/encrypted.js +6 -5
- package/dist/confidentiality/encrypted.js.map +1 -1
- package/dist/events/eventTypeDecorator.d.ts +8 -7
- package/dist/events/eventTypeDecorator.d.ts.map +1 -1
- package/dist/events/eventTypeDecorator.js +22 -5
- package/dist/events/eventTypeDecorator.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/projections/modelBound/addFrom.d.ts +2 -1
- package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/addFrom.js +4 -3
- package/dist/projections/modelBound/addFrom.js.map +1 -1
- package/dist/projections/modelBound/childrenFrom.d.ts +2 -1
- package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenFrom.js +4 -3
- package/dist/projections/modelBound/childrenFrom.js.map +1 -1
- package/dist/projections/modelBound/clearWith.d.ts +2 -1
- package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
- package/dist/projections/modelBound/clearWith.js +4 -3
- package/dist/projections/modelBound/clearWith.js.map +1 -1
- package/dist/projections/modelBound/count.d.ts +2 -1
- package/dist/projections/modelBound/count.d.ts.map +1 -1
- package/dist/projections/modelBound/count.js +4 -3
- package/dist/projections/modelBound/count.js.map +1 -1
- package/dist/projections/modelBound/decrement.d.ts +2 -1
- package/dist/projections/modelBound/decrement.d.ts.map +1 -1
- package/dist/projections/modelBound/decrement.js +4 -3
- package/dist/projections/modelBound/decrement.js.map +1 -1
- package/dist/projections/modelBound/fromAll.d.ts +2 -1
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +4 -3
- package/dist/projections/modelBound/fromAll.js.map +1 -1
- package/dist/projections/modelBound/fromEvery.d.ts +2 -1
- package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
- package/dist/projections/modelBound/fromEvery.js +4 -3
- package/dist/projections/modelBound/fromEvery.js.map +1 -1
- package/dist/projections/modelBound/increment.d.ts +2 -1
- package/dist/projections/modelBound/increment.d.ts.map +1 -1
- package/dist/projections/modelBound/increment.js +4 -3
- package/dist/projections/modelBound/increment.js.map +1 -1
- package/dist/projections/modelBound/join.d.ts +2 -1
- package/dist/projections/modelBound/join.d.ts.map +1 -1
- package/dist/projections/modelBound/join.js +4 -3
- package/dist/projections/modelBound/join.js.map +1 -1
- package/dist/projections/modelBound/nested.d.ts +1 -6
- package/dist/projections/modelBound/nested.d.ts.map +1 -1
- package/dist/projections/modelBound/nested.js +6 -2
- package/dist/projections/modelBound/nested.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts +4 -11
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
- package/dist/projections/modelBound/noAutoMap.js +6 -2
- package/dist/projections/modelBound/noAutoMap.js.map +1 -1
- package/dist/projections/modelBound/removedWith.d.ts +2 -1
- package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
- package/dist/projections/modelBound/removedWith.js +4 -3
- package/dist/projections/modelBound/removedWith.js.map +1 -1
- package/dist/projections/modelBound/removedWithJoin.d.ts +2 -1
- package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
- package/dist/projections/modelBound/removedWithJoin.js +4 -3
- package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
- package/dist/projections/modelBound/setFrom.d.ts +2 -1
- package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/setFrom.js +4 -3
- package/dist/projections/modelBound/setFrom.js.map +1 -1
- package/dist/projections/modelBound/setFromContext.d.ts +3 -2
- package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
- package/dist/projections/modelBound/setFromContext.js +4 -3
- package/dist/projections/modelBound/setFromContext.js.map +1 -1
- package/dist/projections/modelBound/setValue.d.ts +2 -1
- package/dist/projections/modelBound/setValue.d.ts.map +1 -1
- package/dist/projections/modelBound/setValue.js +4 -3
- package/dist/projections/modelBound/setValue.js.map +1 -1
- package/dist/projections/modelBound/subtractFrom.d.ts +2 -1
- package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/subtractFrom.js +4 -3
- package/dist/projections/modelBound/subtractFrom.js.map +1 -1
- package/dist/readModels/readModel.d.ts +2 -1
- package/dist/readModels/readModel.d.ts.map +1 -1
- package/dist/readModels/readModel.js +22 -5
- package/dist/readModels/readModel.js.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +6 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +41 -31
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/schemas/createDeferredSchema.d.ts +7 -0
- package/dist/schemas/createDeferredSchema.d.ts.map +1 -0
- package/dist/schemas/createDeferredSchema.js +19 -0
- package/dist/schemas/createDeferredSchema.js.map +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/jsonSchemaProperty.d.ts +2 -1
- package/dist/schemas/jsonSchemaProperty.d.ts.map +1 -1
- package/dist/schemas/jsonSchemaProperty.js +3 -2
- package/dist/schemas/jsonSchemaProperty.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TypeIntrospector.d.ts.map +1 -1
- package/dist/types/TypeIntrospector.js +9 -3
- package/dist/types/TypeIntrospector.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/propertyDecoratorMetadata.d.ts +17 -0
- package/dist/types/propertyDecoratorMetadata.d.ts.map +1 -0
- package/dist/types/propertyDecoratorMetadata.js +76 -0
- package/dist/types/propertyDecoratorMetadata.js.map +1 -0
- package/dist/types/standardDecoratorMetadata.d.ts +9 -0
- package/dist/types/standardDecoratorMetadata.d.ts.map +1 -0
- package/dist/types/standardDecoratorMetadata.js +20 -0
- package/dist/types/standardDecoratorMetadata.js.map +1 -0
- package/events/classDecorators.standard.spec.ts +77 -0
- package/events/eventTypeDecorator.ts +31 -13
- package/events/index.ts +2 -0
- package/package.json +3 -1
- package/projections/modelBound/addFrom.ts +5 -4
- package/projections/modelBound/childrenFrom.ts +5 -4
- package/projections/modelBound/clearWith.ts +5 -4
- package/projections/modelBound/count.ts +5 -4
- package/projections/modelBound/decorators.standard.spec.ts +71 -0
- package/projections/modelBound/decrement.ts +5 -4
- package/projections/modelBound/fromAll.ts +5 -4
- package/projections/modelBound/fromEvery.ts +5 -4
- package/projections/modelBound/increment.ts +5 -4
- package/projections/modelBound/join.ts +5 -4
- package/projections/modelBound/nested.ts +9 -2
- package/projections/modelBound/noAutoMap.ts +11 -2
- package/projections/modelBound/removedWith.ts +5 -4
- package/projections/modelBound/removedWithJoin.ts +5 -4
- package/projections/modelBound/setFrom.ts +5 -4
- package/projections/modelBound/setFromContext.ts +7 -6
- package/projections/modelBound/setValue.ts +5 -4
- package/projections/modelBound/subtractFrom.ts +5 -4
- package/readModels/readModel.ts +24 -6
- package/readModels/standardFallbacks.standard.spec.ts +39 -0
- package/schemas/JsonSchemaGenerator.confidentiality.spec.ts +2 -0
- package/schemas/JsonSchemaGenerator.spec.ts +28 -1
- package/schemas/JsonSchemaGenerator.ts +45 -30
- package/schemas/createDeferredSchema.ts +21 -0
- package/schemas/decorators.expected.fixture.ts +24 -0
- package/schemas/decorators.legacy.spec.ts +62 -0
- package/schemas/decorators.standard.spec.ts +235 -0
- package/schemas/index.ts +1 -0
- package/schemas/jsonSchemaProperty.ts +4 -3
- package/schemas/legacyDesignMetadata.spec.ts +39 -0
- package/types/TypeIntrospector.ts +8 -3
- package/types/index.ts +2 -0
- package/types/propertyDecoratorMetadata.ts +80 -0
- package/types/standardDecoratorMetadata.spec.ts +31 -0
- package/types/standardDecoratorMetadata.ts +26 -0
- package/vitest.config.ts +17 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import 'reflect-metadata';
|
|
5
5
|
import { TypeIntrospector } from '../../types/index.js';
|
|
6
|
+
import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
|
|
6
7
|
|
|
7
8
|
/** Metadata stored by the subtractFrom property decorator. */
|
|
8
9
|
export interface SubtractFromMetadata {
|
|
@@ -20,14 +21,14 @@ const METADATA_KEY = 'chronicle:projection:subtractFrom';
|
|
|
20
21
|
* @param eventPropertyName - Optional event property name. Defaults to the property name.
|
|
21
22
|
* @returns A property decorator.
|
|
22
23
|
*/
|
|
23
|
-
export function subtractFrom(eventType: Function, eventPropertyName?: string):
|
|
24
|
-
return (target: object, propertyKey: string | symbol) => {
|
|
24
|
+
export function subtractFrom(eventType: Function, eventPropertyName?: string): ChroniclePropertyDecorator {
|
|
25
|
+
return decorateProperty((target: object, propertyKey: string | symbol) => {
|
|
25
26
|
const key = propertyKey.toString();
|
|
26
27
|
TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
|
|
27
28
|
const existing: SubtractFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
|
|
28
29
|
const metadata: SubtractFromMetadata = { eventType, eventPropertyName };
|
|
29
30
|
Reflect.defineMetadata(METADATA_KEY, [...existing, metadata], target, key);
|
|
30
|
-
};
|
|
31
|
+
});
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -37,5 +38,5 @@ export function subtractFrom(eventType: Function, eventPropertyName?: string): P
|
|
|
37
38
|
* @returns An array of subtractFrom metadata entries.
|
|
38
39
|
*/
|
|
39
40
|
export function getSubtractFromMetadata(target: object, propertyKey: string): SubtractFromMetadata[] {
|
|
40
|
-
return
|
|
41
|
+
return getPropertyMetadata<SubtractFromMetadata[]>(METADATA_KEY, target, propertyKey) ?? [];
|
|
41
42
|
}
|
package/readModels/readModel.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { Constructor } from '@cratis/fundamentals';
|
|
|
6
6
|
import { ReadModelId } from './ReadModelId.js';
|
|
7
7
|
import { DecoratorType, TypeDiscoverer, TypeIntrospector } from '../types/index.js';
|
|
8
8
|
import { JsonSchema, JsonSchemaGenerator } from '../schemas/index.js';
|
|
9
|
+
import { ChronicleClassDecorator } from '../types/standardDecoratorMetadata.js';
|
|
10
|
+
import { createDeferredSchema } from '../schemas/createDeferredSchema.js';
|
|
9
11
|
|
|
10
12
|
/** Metadata key used to store read model information on a class. */
|
|
11
13
|
const READ_MODEL_METADATA_KEY = 'chronicle:readModel';
|
|
@@ -29,14 +31,23 @@ export interface ReadModelMetadata {
|
|
|
29
31
|
* @param id - The unique identifier for the read model. Defaults to the class name if omitted.
|
|
30
32
|
* @returns A class decorator.
|
|
31
33
|
*/
|
|
32
|
-
export function readModel(id: string = ''):
|
|
33
|
-
return (target: object) => {
|
|
34
|
-
|
|
34
|
+
export function readModel(id: string = ''): ChronicleClassDecorator {
|
|
35
|
+
return (target: object, context?: ClassDecoratorContext) => {
|
|
36
|
+
let constructor = target as Function;
|
|
35
37
|
const readModelId = new ReadModelId(id || constructor.name);
|
|
36
|
-
const members = TypeIntrospector.getMembers(constructor);
|
|
37
|
-
const
|
|
38
|
+
const members = context?.kind === 'class' ? undefined : TypeIntrospector.getMembers(constructor);
|
|
39
|
+
const resolve = createDeferredSchema(() => constructor);
|
|
40
|
+
const metadata: ReadModelMetadata = context?.kind === 'class' ? {
|
|
38
41
|
id: readModelId,
|
|
39
|
-
members
|
|
42
|
+
get members() {
|
|
43
|
+
return resolve().members;
|
|
44
|
+
},
|
|
45
|
+
get schema() {
|
|
46
|
+
return resolve().schema;
|
|
47
|
+
}
|
|
48
|
+
} : {
|
|
49
|
+
id: readModelId,
|
|
50
|
+
members: members!,
|
|
40
51
|
schema: JsonSchemaGenerator.generate(constructor, members)
|
|
41
52
|
};
|
|
42
53
|
Reflect.defineMetadata(READ_MODEL_METADATA_KEY, metadata, target);
|
|
@@ -45,6 +56,13 @@ export function readModel(id: string = ''): ClassDecorator {
|
|
|
45
56
|
constructor as Constructor,
|
|
46
57
|
readModelId.value
|
|
47
58
|
);
|
|
59
|
+
context?.addInitializer(function () {
|
|
60
|
+
if (this !== constructor) {
|
|
61
|
+
constructor = this;
|
|
62
|
+
Reflect.defineMetadata(READ_MODEL_METADATA_KEY, metadata, constructor);
|
|
63
|
+
TypeDiscoverer.default.register(DecoratorType.ReadModel, constructor as Constructor, readModelId.value);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
48
66
|
};
|
|
49
67
|
}
|
|
50
68
|
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { field } from '@cratis/fundamentals';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
|
|
7
|
+
import { ChronicleConnection } from '../connection/ChronicleConnection.js';
|
|
8
|
+
import { reducer } from '../reducers/reducer.js';
|
|
9
|
+
import { MaterializedReadModels } from './MaterializedReadModels.js';
|
|
10
|
+
import { ReadModels } from './ReadModels.js';
|
|
11
|
+
|
|
12
|
+
class UndecoratedModel {
|
|
13
|
+
@field(Array) items!: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@reducer('undecorated-model', undefined, UndecoratedModel)
|
|
17
|
+
class ModelReducer {}
|
|
18
|
+
|
|
19
|
+
const artifacts = {
|
|
20
|
+
eventTypes: [], readModels: [], reactors: [], reducers: [ModelReducer], seeders: [],
|
|
21
|
+
constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
22
|
+
} as IClientArtifactsProvider;
|
|
23
|
+
|
|
24
|
+
describe('standard read models without @readModel', () => {
|
|
25
|
+
it('rejects an untyped array before registering a reducer read model', async () => {
|
|
26
|
+
const registerMany = vi.fn();
|
|
27
|
+
const connection = { readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
28
|
+
const readModels = new ReadModels('test', 'Default', connection, artifacts, 'sink');
|
|
29
|
+
await expect(readModels.register(UndecoratedModel)).rejects.toThrow(/Cannot determine the element type of UndecoratedModel.items/);
|
|
30
|
+
expect(registerMany).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('rejects an untyped array instead of releasing an incomplete materialized schema', async () => {
|
|
34
|
+
const getInstances = vi.fn().mockResolvedValue({ Instances: [] });
|
|
35
|
+
const connection = { materializedReadModels: { getInstances } } as unknown as ChronicleConnection;
|
|
36
|
+
const materialized = new MaterializedReadModels('test', 'Default', connection);
|
|
37
|
+
await expect(materialized.getInstances(UndecoratedModel)).rejects.toThrow(/Cannot determine the element type of UndecoratedModel.items/);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -77,6 +77,8 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
77
77
|
|
|
78
78
|
describe('when a property is typed as a ConceptAs<T> marked @encrypted()', () => {
|
|
79
79
|
class ApiKey extends ConceptAs<string> {
|
|
80
|
+
static readonly valueType = String;
|
|
81
|
+
|
|
80
82
|
constructor(value: string) {
|
|
81
83
|
super(value);
|
|
82
84
|
}
|
|
@@ -2,7 +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 { ConceptAs, field } from '@cratis/fundamentals';
|
|
5
|
+
import { ConceptAs, field, Guid } from '@cratis/fundamentals';
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
7
|
import { pii } from '../compliance/pii.js';
|
|
8
8
|
import { eventType, getEventTypeJsonSchemaFor } from '../events/eventTypeDecorator.js';
|
|
@@ -22,6 +22,31 @@ function schemaFor(target: Function): JsonSchema {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
describe('JsonSchemaGenerator', () => {
|
|
25
|
+
it('retains the legacy string-concept fallback without emitted type metadata', () => {
|
|
26
|
+
class LegacyCode extends ConceptAs<string> {}
|
|
27
|
+
class LegacyModel {
|
|
28
|
+
code = new LegacyCode('A');
|
|
29
|
+
}
|
|
30
|
+
readModel()(LegacyModel);
|
|
31
|
+
expect(schemaFor(LegacyModel).properties?.code.type).toBe('string');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('preserves Guid and Date-backed legacy concepts with design:type', () => {
|
|
35
|
+
class LegacyId extends ConceptAs<Guid> {}
|
|
36
|
+
class LegacyTime extends ConceptAs<Date> {}
|
|
37
|
+
Reflect.defineMetadata('design:type', Guid, LegacyId.prototype, 'value');
|
|
38
|
+
Reflect.defineMetadata('design:type', Date, LegacyTime.prototype, 'value');
|
|
39
|
+
class LegacyEvent {
|
|
40
|
+
id = new LegacyId(Guid.empty);
|
|
41
|
+
occurred = new LegacyTime(new Date());
|
|
42
|
+
}
|
|
43
|
+
eventType()(LegacyEvent);
|
|
44
|
+
expect(getEventTypeJsonSchemaFor(LegacyEvent).properties).toMatchObject({
|
|
45
|
+
id: { type: 'string', format: 'guid' },
|
|
46
|
+
occurred: { type: 'string', format: 'date-time' }
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
25
50
|
describe('when a class is marked @pii() at the class level', () => {
|
|
26
51
|
class PersonProfile {
|
|
27
52
|
name = '';
|
|
@@ -59,6 +84,8 @@ describe('JsonSchemaGenerator', () => {
|
|
|
59
84
|
|
|
60
85
|
describe('when a property is typed as a ConceptAs<T> marked @pii()', () => {
|
|
61
86
|
class EmailAddress extends ConceptAs<string> {
|
|
87
|
+
static readonly valueType = String;
|
|
88
|
+
|
|
62
89
|
constructor(value: string) {
|
|
63
90
|
super(value);
|
|
64
91
|
}
|
|
@@ -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 {
|
|
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 { hasOwnStandardMetadata } from '../types/standardDecoratorMetadata.js';
|
|
8
9
|
import { ComplianceMetadata } from '../compliance/ComplianceMetadata.js';
|
|
9
10
|
import { ComplianceMetadataResolver } from '../compliance/ComplianceMetadataResolver.js';
|
|
10
11
|
import { SecurityMetadata } from '../confidentiality/SecurityMetadata.js';
|
|
@@ -14,6 +15,11 @@ import { PIIAndEncryptedCombinedNotSupported } from '../confidentiality/PIIAndEn
|
|
|
14
15
|
/**
|
|
15
16
|
* Generates JSON schemas for class constructors using reflection metadata.
|
|
16
17
|
*/
|
|
18
|
+
export interface JsonSchemaGenerationOptions {
|
|
19
|
+
/** Reject members and array elements without runtime types. Defaults to true for standard-decorated classes. */
|
|
20
|
+
readonly requireResolvedTypes?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export class JsonSchemaGenerator {
|
|
18
24
|
private static readonly _knownTypeFormats = new Map<Function, { type: JsonSchema['type']; format: string }>([
|
|
19
25
|
[Guid, { type: 'string', format: 'guid' }],
|
|
@@ -44,15 +50,20 @@ export class JsonSchemaGenerator {
|
|
|
44
50
|
* Generates a JSON schema for a class constructor.
|
|
45
51
|
* @param target - The class constructor to generate schema for.
|
|
46
52
|
* @param members - Optional pre-introspected members for reuse.
|
|
53
|
+
* @param options - Schema resolution options. A boolean remains supported for existing callers.
|
|
47
54
|
* @returns The generated JSON schema.
|
|
48
55
|
*/
|
|
49
|
-
static generate(target: Function, members?: ReadonlyMap<string, Function | undefined
|
|
56
|
+
static generate(target: Function, members?: ReadonlyMap<string, Function | undefined>, options?: JsonSchemaGenerationOptions | boolean): JsonSchema {
|
|
57
|
+
const requireResolvedTypes = typeof options === 'boolean' ? options : options?.requireResolvedTypes ?? hasOwnStandardMetadata(target);
|
|
50
58
|
const membersToUse = members ?? TypeIntrospector.getMembers(target);
|
|
51
59
|
const schemaProperties: Record<string, JsonSchema> = {};
|
|
52
60
|
const prototype = target.prototype;
|
|
53
61
|
|
|
54
62
|
for (const [memberName, memberType] of membersToUse.entries()) {
|
|
55
|
-
|
|
63
|
+
if (!memberType && requireResolvedTypes) {
|
|
64
|
+
throw new TypeError(`Cannot determine the type of ${target.name}.${memberName}; declare @field with its runtime type.`);
|
|
65
|
+
}
|
|
66
|
+
const propertySchema = this.mapRuntimeTypeToSchema(memberType, target, memberName, requireResolvedTypes);
|
|
56
67
|
// Only include properties whose type was resolved. An empty schema ({}) means
|
|
57
68
|
// the runtime type was unavailable (e.g. esbuild/tsx omits design:paramtypes).
|
|
58
69
|
if (Object.keys(propertySchema).length > 0) {
|
|
@@ -71,9 +82,8 @@ export class JsonSchemaGenerator {
|
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// content as-is via ConvertUnknownSchemaTypeToClrType.
|
|
85
|
+
// Legacy decorators retain the minimal schema fallback for members with no
|
|
86
|
+
// runtime type metadata. Standard decorators reject unresolved types above.
|
|
77
87
|
if (Object.keys(schemaProperties).length === 0) {
|
|
78
88
|
return this.createEmptySchema(target.name);
|
|
79
89
|
}
|
|
@@ -85,7 +95,7 @@ export class JsonSchemaGenerator {
|
|
|
85
95
|
};
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
private static mapRuntimeTypeToSchema(runtimeType: Function | undefined, declaringType?: Function, propertyName?: string): JsonSchema {
|
|
98
|
+
private static mapRuntimeTypeToSchema(runtimeType: Function | undefined, declaringType?: Function, propertyName?: string, requireResolvedTypes = false): JsonSchema {
|
|
89
99
|
const knownTypeFormat = this.getKnownTypeFormat(runtimeType);
|
|
90
100
|
if (knownTypeFormat) {
|
|
91
101
|
return knownTypeFormat;
|
|
@@ -104,25 +114,29 @@ export class JsonSchemaGenerator {
|
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
if (runtimeType === Array) {
|
|
107
|
-
return this.mapArrayTypeToSchema(declaringType, propertyName);
|
|
117
|
+
return this.mapArrayTypeToSchema(declaringType, propertyName, requireResolvedTypes);
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
if (!runtimeType) {
|
|
111
121
|
return {};
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// not available at runtime, so resolve it from the 'value' property's design:type
|
|
117
|
-
// when present (ts-node/webpack with emitDecoratorMetadata) and fall back to string
|
|
118
|
-
// when it is not (esbuild/tsx) — string-backed concepts are by far the common case.
|
|
124
|
+
// TypeScript erases ConceptAs<T>'s primitive type. Prefer an explicit field or
|
|
125
|
+
// static hint; legacy emitDecoratorMetadata is the last available source.
|
|
119
126
|
if (this.isConceptAs(runtimeType)) {
|
|
120
|
-
const
|
|
121
|
-
|
|
127
|
+
const concept = runtimeType as Function & { valueType?: Function };
|
|
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;
|
|
130
|
+
if (!valueType && !requireResolvedTypes) return { type: 'string' }; // Legacy schema compatibility.
|
|
131
|
+
const valueSchema = this.mapRuntimeTypeToSchema(valueType, undefined, undefined, requireResolvedTypes);
|
|
132
|
+
if (!valueType || !valueSchema.type || (requireResolvedTypes && valueSchema.type === 'object')) {
|
|
133
|
+
throw new TypeError(`Cannot determine the value type of concept ${runtimeType.name}; declare static readonly valueType = String, Number, Boolean, Guid, or Date.`);
|
|
134
|
+
}
|
|
135
|
+
return valueSchema;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
if (runtimeType !== Object) {
|
|
125
|
-
return this.generate(runtimeType);
|
|
139
|
+
return this.generate(runtimeType, undefined, requireResolvedTypes);
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
return { type: 'object' };
|
|
@@ -135,7 +149,7 @@ export class JsonSchemaGenerator {
|
|
|
135
149
|
* @param propertyName - The array property name.
|
|
136
150
|
* @returns The array schema, with the element's own compliance metadata carried onto `items` when the element is a PII concept.
|
|
137
151
|
*/
|
|
138
|
-
private static mapArrayTypeToSchema(declaringType: Function | undefined, propertyName: string | undefined): JsonSchema {
|
|
152
|
+
private static mapArrayTypeToSchema(declaringType: Function | undefined, propertyName: string | undefined, requireResolvedTypes: boolean): JsonSchema {
|
|
139
153
|
const elementType = this.getArrayElementType(declaringType, propertyName);
|
|
140
154
|
|
|
141
155
|
// An array whose element is a ConceptAs<T> loses its classification the moment it is put in
|
|
@@ -143,7 +157,7 @@ export class JsonSchemaGenerator {
|
|
|
143
157
|
// item schema - a value that would be encrypted as a scalar would otherwise be persisted in
|
|
144
158
|
// the clear as a list element. Mirrors the C# generator's explicit enumerable-of-concept branch.
|
|
145
159
|
if (elementType && this.isConceptAs(elementType)) {
|
|
146
|
-
const itemSchema = this.mapRuntimeTypeToSchema(elementType);
|
|
160
|
+
const itemSchema = this.mapRuntimeTypeToSchema(elementType, undefined, undefined, requireResolvedTypes);
|
|
147
161
|
const complianceMetadata = ComplianceMetadataResolver.getMetadataForType(elementType);
|
|
148
162
|
const securityMetadata = SecurityMetadataResolver.getMetadataForType(elementType);
|
|
149
163
|
this.throwIfBothCompliantAndSecure(elementType.name, complianceMetadata, securityMetadata);
|
|
@@ -152,7 +166,15 @@ export class JsonSchemaGenerator {
|
|
|
152
166
|
return { type: 'array', items: itemSchema };
|
|
153
167
|
}
|
|
154
168
|
|
|
155
|
-
|
|
169
|
+
if (!elementType) {
|
|
170
|
+
if (requireResolvedTypes) {
|
|
171
|
+
throw new TypeError(`Cannot determine the element type of ${declaringType?.name}.${propertyName}; declare @field(Array, { genericArguments: [ItemType] }).`);
|
|
172
|
+
}
|
|
173
|
+
return { type: 'array', items: { type: 'object' } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!requireResolvedTypes) return { type: 'array', items: { type: 'object' } }; // Legacy schema compatibility.
|
|
177
|
+
return { type: 'array', items: this.mapRuntimeTypeToSchema(elementType, undefined, undefined, true) };
|
|
156
178
|
}
|
|
157
179
|
|
|
158
180
|
/**
|
|
@@ -189,16 +211,7 @@ export class JsonSchemaGenerator {
|
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
private static isConceptAs(runtimeType: Function): boolean {
|
|
192
|
-
|
|
193
|
-
while (current && current !== Function.prototype) {
|
|
194
|
-
if (current === ConceptAs) {
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
current = Object.getPrototypeOf(current) as Function | null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return false;
|
|
214
|
+
return typeKeyOf(runtimeType as Constructor) === conceptAsTypeKey;
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
private static getKnownTypeFormat(runtimeType: Function | undefined): JsonSchema | undefined {
|
|
@@ -206,7 +219,9 @@ export class JsonSchemaGenerator {
|
|
|
206
219
|
return undefined;
|
|
207
220
|
}
|
|
208
221
|
|
|
209
|
-
const known =
|
|
222
|
+
const known = typeKeyOf(runtimeType as Constructor) === 'Guid'
|
|
223
|
+
? this._knownTypeFormats.get(Guid)
|
|
224
|
+
: this._knownTypeFormats.get(runtimeType);
|
|
210
225
|
if (!known) {
|
|
211
226
|
return undefined;
|
|
212
227
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 { JsonSchema } from './JsonSchema.js';
|
|
5
|
+
import { JsonSchemaGenerator } from './JsonSchemaGenerator.js';
|
|
6
|
+
import { TypeIntrospector } from '../types/TypeIntrospector.js';
|
|
7
|
+
import { requireCompletedStandardMetadata } from '../types/standardDecoratorMetadata.js';
|
|
8
|
+
|
|
9
|
+
/** Resolves a standard-decorated class's members and schema together, after class evaluation. */
|
|
10
|
+
export function createDeferredSchema(getType: () => Function): () => { members: ReadonlyMap<string, Function | undefined>; schema: JsonSchema } {
|
|
11
|
+
let resolved: { members: ReadonlyMap<string, Function | undefined>; schema: JsonSchema } | undefined;
|
|
12
|
+
return () => {
|
|
13
|
+
const type = getType();
|
|
14
|
+
requireCompletedStandardMetadata(type);
|
|
15
|
+
if (!resolved) {
|
|
16
|
+
const members = TypeIntrospector.getMembers(type);
|
|
17
|
+
resolved = { members, schema: JsonSchemaGenerator.generate(type, members, { requireResolvedTypes: true }) };
|
|
18
|
+
}
|
|
19
|
+
return resolved;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { JsonSchema } from './JsonSchema.js';
|
|
5
|
+
|
|
6
|
+
/** Expected schema contract for both decorator transforms. */
|
|
7
|
+
export const expectedProperties: Record<string, JsonSchema> = {
|
|
8
|
+
name: { type: 'string' },
|
|
9
|
+
age: { type: 'number' },
|
|
10
|
+
active: { type: 'boolean' },
|
|
11
|
+
id: { type: 'string', format: 'guid' },
|
|
12
|
+
occurred: { type: 'string', format: 'date-time' },
|
|
13
|
+
codes: { type: 'array', items: { type: 'string' } },
|
|
14
|
+
address: {
|
|
15
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
16
|
+
title: 'Address',
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: { city: { type: 'string' } },
|
|
19
|
+
required: ['city'],
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
},
|
|
22
|
+
code: { type: 'string' },
|
|
23
|
+
quantity: { type: 'number' }
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
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, field, Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { eventType, getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
7
|
+
import { getReadModelMetadata, readModel } from '../readModels/readModel.js';
|
|
8
|
+
import { expectedProperties } from './decorators.expected.fixture.js';
|
|
9
|
+
import { getProjectionMetadata, projection } from '../projections/declarative/projection.js';
|
|
10
|
+
import { getReducerMetadata, reducer } from '../reducers/reducer.js';
|
|
11
|
+
import { getReactorMetadata, reactor } from '../reactors/reactor.js';
|
|
12
|
+
|
|
13
|
+
class Code extends ConceptAs<string> {
|
|
14
|
+
static readonly valueType = String;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class Quantity extends ConceptAs<number> {
|
|
18
|
+
static readonly valueType = Number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Address {
|
|
22
|
+
@field(String) city!: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@eventType('legacy-fixture')
|
|
26
|
+
class LegacyEvent {
|
|
27
|
+
@field(String) name!: string;
|
|
28
|
+
@field(Number) age!: number;
|
|
29
|
+
@field(Boolean) active!: boolean;
|
|
30
|
+
@field(Guid) id!: Guid;
|
|
31
|
+
@field(Date) occurred!: Date;
|
|
32
|
+
@field(Array, { genericArguments: [Code] }) codes!: Code[];
|
|
33
|
+
@field(Address) address!: Address;
|
|
34
|
+
@field(Code) code!: Code;
|
|
35
|
+
@field(Quantity) quantity!: Quantity;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@readModel('legacy-model')
|
|
39
|
+
class LegacyModel {
|
|
40
|
+
@field(Number) count!: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@projection('legacy-projection', LegacyModel)
|
|
44
|
+
class LegacyProjection {}
|
|
45
|
+
|
|
46
|
+
@reducer('legacy-reducer', undefined, LegacyModel)
|
|
47
|
+
class LegacyReducer {}
|
|
48
|
+
|
|
49
|
+
@reactor('legacy-reactor')
|
|
50
|
+
class LegacyReactor {}
|
|
51
|
+
|
|
52
|
+
describe('legacy decorator syntax', () => {
|
|
53
|
+
it('registers the same schemas as standard decorators', () => {
|
|
54
|
+
expect(getEventTypeMetadata(LegacyEvent)?.eventType.id.value).toBe('legacy-fixture');
|
|
55
|
+
expect(getReadModelMetadata(LegacyModel)?.schema.properties?.count.type).toBe('number');
|
|
56
|
+
expect(getEventTypeMetadata(LegacyEvent)?.schema.properties).toEqual(expectedProperties);
|
|
57
|
+
expect(getEventTypeMetadata(LegacyEvent)?.schema.required).toEqual(Object.keys(expectedProperties));
|
|
58
|
+
expect(getProjectionMetadata(LegacyProjection)?.id.value).toBe('legacy-projection');
|
|
59
|
+
expect(getReducerMetadata(LegacyReducer)?.id.value).toBe('legacy-reducer');
|
|
60
|
+
expect(getReactorMetadata(LegacyReactor)?.id.value).toBe('legacy-reactor');
|
|
61
|
+
});
|
|
62
|
+
});
|