@cratis/chronicle 6.27.0 → 6.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChronicleClient.ts +1 -1
- package/ChronicleOptions.ts +14 -1
- package/EventStore.ts +6 -4
- package/connection/ChronicleConnection.auth.spec.ts +15 -11
- package/dist/ChronicleClient.js +1 -1
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +9 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +10 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +2 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +4 -4
- package/dist/EventStore.js.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.d.ts +4 -1
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +6 -2
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- package/dist/projections/Projections.d.ts +4 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +5 -2
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/modelBound/childrenFrom.d.ts +19 -0
- package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenFrom.js +14 -13
- package/dist/projections/modelBound/childrenFrom.js.map +1 -1
- package/dist/projections/modelBound/index.d.ts +1 -1
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/readModels/ReadModelNamingPolicy.d.ts +13 -0
- package/dist/readModels/ReadModelNamingPolicy.d.ts.map +1 -0
- package/dist/readModels/ReadModelNamingPolicy.js +4 -0
- package/dist/readModels/ReadModelNamingPolicy.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts +3 -1
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +5 -2
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/buildReadModelDefinition.d.ts +3 -0
- package/dist/readModels/buildReadModelDefinition.d.ts.map +1 -1
- package/dist/readModels/buildReadModelDefinition.js +1 -1
- package/dist/readModels/buildReadModelDefinition.js.map +1 -1
- package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.d.ts +47 -0
- package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.d.ts.map +1 -0
- package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.js +115 -0
- package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.js.map +1 -0
- package/dist/readModels/index.d.ts +1 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js.map +1 -1
- package/dist/reducers/Reducers.d.ts +6 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +8 -2
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/projections/ProjectionDefinitionCompiler.ts +6 -2
- package/projections/Projections.childrenFromOptions.spec.ts +118 -0
- package/projections/Projections.ts +5 -2
- package/projections/modelBound/childrenFrom.ts +41 -5
- package/projections/modelBound/index.ts +1 -1
- package/readModels/ReadModelNamingPolicy.ts +16 -0
- package/readModels/ReadModels.ts +5 -2
- package/readModels/buildReadModelDefinition.ts +5 -2
- package/readModels/for_ReadModelNamingPolicy/given/registered_read_models.ts +137 -0
- package/readModels/for_ReadModelNamingPolicy/when_configuring_options/with_a_naming_policy.spec.ts +96 -0
- package/readModels/for_ReadModelNamingPolicy/when_registering/a_declarative_projection.spec.ts +40 -0
- package/readModels/for_ReadModelNamingPolicy/when_registering/a_model_bound_projection.spec.ts +40 -0
- package/readModels/for_ReadModelNamingPolicy/when_registering/a_model_bound_read_model_directly.spec.ts +40 -0
- package/readModels/for_ReadModelNamingPolicy/when_registering/a_projection_directly.spec.ts +40 -0
- package/readModels/for_ReadModelNamingPolicy/when_registering/a_reducer.spec.ts +40 -0
- package/readModels/for_ReadModelNamingPolicy/when_registering/a_reducer_read_model_directly.spec.ts +40 -0
- package/readModels/index.ts +1 -0
- package/reducers/Reducers.ts +8 -2
- package/testing/ReadModelScenario.childrenFrom.spec.ts +35 -0
|
@@ -20,8 +20,28 @@ export interface ChildrenFromMetadata {
|
|
|
20
20
|
readonly parentKey?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Options for the options form of the childrenFrom property decorator. */
|
|
24
|
+
export interface ChildrenFromOptions {
|
|
25
|
+
/** The child type, for TypeScript runtimes without emitted generic metadata. */
|
|
26
|
+
readonly childType?: Function;
|
|
27
|
+
/** The event property name used as the key for children. Defaults to the event source identifier. */
|
|
28
|
+
readonly key?: string;
|
|
29
|
+
/** The child model property name used to uniquely identify instances in the collection. */
|
|
30
|
+
readonly identifiedBy?: string;
|
|
31
|
+
/** The event property name used as the parent key. Defaults to the event source identifier. */
|
|
32
|
+
readonly parentKey?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
const METADATA_KEY = 'chronicle:projection:childrenFrom';
|
|
24
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Property decorator that configures a children collection sub-projection from an event type.
|
|
39
|
+
* Use this form to name the child type and an explicit child key together.
|
|
40
|
+
* @param eventType - The event constructor.
|
|
41
|
+
* @param options - The child type, key, identifiedBy and parentKey.
|
|
42
|
+
* @returns A property decorator.
|
|
43
|
+
*/
|
|
44
|
+
export function childrenFrom(eventType: Function, options: ChildrenFromOptions): ChroniclePropertyDecorator;
|
|
25
45
|
/**
|
|
26
46
|
* Property decorator that configures a children collection sub-projection from an event type.
|
|
27
47
|
* @param eventType - The event constructor.
|
|
@@ -30,24 +50,40 @@ const METADATA_KEY = 'chronicle:projection:childrenFrom';
|
|
|
30
50
|
* @param parentKey - Optional event property name used as the parent key.
|
|
31
51
|
* @returns A property decorator.
|
|
32
52
|
*/
|
|
53
|
+
export function childrenFrom(eventType: Function, key?: string | Function, identifiedBy?: string, parentKey?: string): ChroniclePropertyDecorator;
|
|
33
54
|
export function childrenFrom(
|
|
34
55
|
eventType: Function,
|
|
35
|
-
|
|
56
|
+
keyOrOptions?: string | Function | ChildrenFromOptions,
|
|
36
57
|
identifiedBy?: string,
|
|
37
58
|
parentKey?: string
|
|
38
59
|
): ChroniclePropertyDecorator {
|
|
60
|
+
const metadata = toMetadata(eventType, keyOrOptions, identifiedBy, parentKey);
|
|
39
61
|
return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
|
|
40
62
|
const propKey = propertyKey.toString();
|
|
41
63
|
TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propKey);
|
|
42
64
|
const existing: ChildrenFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, propKey) ?? [];
|
|
43
|
-
const metadata: ChildrenFromMetadata = {
|
|
44
|
-
eventType, key: typeof key === 'string' ? key : undefined,
|
|
45
|
-
childType: typeof key === 'function' ? key : undefined, identifiedBy, parentKey
|
|
46
|
-
};
|
|
47
65
|
Reflect.defineMetadata(METADATA_KEY, [...existing, metadata], target, propKey);
|
|
48
66
|
});
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
function toMetadata(
|
|
70
|
+
eventType: Function,
|
|
71
|
+
keyOrOptions: string | Function | ChildrenFromOptions | undefined,
|
|
72
|
+
identifiedBy: string | undefined,
|
|
73
|
+
parentKey: string | undefined
|
|
74
|
+
): ChildrenFromMetadata {
|
|
75
|
+
if (typeof keyOrOptions === 'object' && keyOrOptions !== null) {
|
|
76
|
+
return {
|
|
77
|
+
eventType, childType: keyOrOptions.childType, key: keyOrOptions.key,
|
|
78
|
+
identifiedBy: keyOrOptions.identifiedBy, parentKey: keyOrOptions.parentKey
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
eventType, key: typeof keyOrOptions === 'string' ? keyOrOptions : undefined,
|
|
83
|
+
childType: typeof keyOrOptions === 'function' ? keyOrOptions : undefined, identifiedBy, parentKey
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
/**
|
|
52
88
|
* Retrieves all childrenFrom metadata stored on the given property.
|
|
53
89
|
* @param target - The class prototype.
|
|
@@ -21,7 +21,7 @@ export type { DecrementMetadata } from './decrement.js';
|
|
|
21
21
|
export { count, getCountMetadata } from './count.js';
|
|
22
22
|
export type { CountMetadata } from './count.js';
|
|
23
23
|
export { childrenFrom, getChildrenFromMetadata } from './childrenFrom.js';
|
|
24
|
-
export type { ChildrenFromMetadata } from './childrenFrom.js';
|
|
24
|
+
export type { ChildrenFromMetadata, ChildrenFromOptions } from './childrenFrom.js';
|
|
25
25
|
export { nested, isNested } from './nested.js';
|
|
26
26
|
export { clearWith, getClearWithClassMetadata, getClearWithPropertyMetadata } from './clearWith.js';
|
|
27
27
|
export type { ClearWithMetadata } from './clearWith.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Decides the container name a read model is stored under: the collection, table, or file the sink writes to.
|
|
8
|
+
* The identifier the read model is registered and queried by is not affected.
|
|
9
|
+
*
|
|
10
|
+
* When no policy is configured the container name is the read model identifier.
|
|
11
|
+
* @param identifier - The read model identifier.
|
|
12
|
+
* @param readModelType - The read model class, when the client knows it. A projection whose read model is only
|
|
13
|
+
* named by identifier has no class, so this is undefined.
|
|
14
|
+
* @returns The container name to register the read model with.
|
|
15
|
+
*/
|
|
16
|
+
export type ReadModelNamingPolicy = (identifier: string, readModelType?: Constructor) => string;
|
package/readModels/ReadModels.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type { IMaterializedReadModels } from './IMaterializedReadModels.js';
|
|
|
29
29
|
import { MaterializedReadModels } from './MaterializedReadModels.js';
|
|
30
30
|
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
|
|
31
31
|
import { deserializeReadModel } from './deserializeReadModel.js';
|
|
32
|
+
import type { ReadModelNamingPolicy } from './ReadModelNamingPolicy.js';
|
|
32
33
|
import type { IReadModels } from './IReadModels.js';
|
|
33
34
|
import type { ReadModelChangeset } from './ReadModelChangeset.js';
|
|
34
35
|
import type { ReadModelSnapshot } from './ReadModelSnapshot.js';
|
|
@@ -58,7 +59,8 @@ export class ReadModels implements IReadModels {
|
|
|
58
59
|
private readonly _connection: ChronicleConnection,
|
|
59
60
|
private readonly _clientArtifacts: IClientArtifactsProvider,
|
|
60
61
|
private readonly _defaultSinkTypeId: string,
|
|
61
|
-
private readonly _isModelBoundProjectionRegistered?: (readModelType: Constructor) => boolean
|
|
62
|
+
private readonly _isModelBoundProjectionRegistered?: (readModelType: Constructor) => boolean,
|
|
63
|
+
private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
|
|
62
64
|
) {
|
|
63
65
|
this.materialized = new MaterializedReadModels(_eventStore, _namespace, _connection);
|
|
64
66
|
}
|
|
@@ -349,7 +351,8 @@ export class ReadModels implements IReadModels {
|
|
|
349
351
|
// None sink and the kernel resolves them via immediate projection instead of an empty sink.
|
|
350
352
|
sinkTypeId: readModel.isActive ? this._defaultSinkTypeId : WellKnownSinks.None,
|
|
351
353
|
observerType: readModel.observerType,
|
|
352
|
-
observerIdentifier: readModel.observerIdentifier
|
|
354
|
+
observerIdentifier: readModel.observerIdentifier,
|
|
355
|
+
namingPolicy: this._readModelNamingPolicy
|
|
353
356
|
});
|
|
354
357
|
}
|
|
355
358
|
|
|
@@ -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 { ReadModelObserverType } from '@cratis/chronicle.contracts';
|
|
5
|
-
import { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { Constructor, Guid } from '@cratis/fundamentals';
|
|
6
6
|
import { toContractsGuid } from '../connection/Guid.js';
|
|
7
7
|
import { getIndexesForType } from './indexDecorator.js';
|
|
8
|
+
import type { ReadModelNamingPolicy } from './ReadModelNamingPolicy.js';
|
|
8
9
|
|
|
9
10
|
interface ReadModelDefinitionOptions {
|
|
10
11
|
identifier: string;
|
|
@@ -13,6 +14,8 @@ interface ReadModelDefinitionOptions {
|
|
|
13
14
|
sinkTypeId: string;
|
|
14
15
|
observerType: ReadModelObserverType;
|
|
15
16
|
observerIdentifier: string;
|
|
17
|
+
/** Chooses the container name. Absent means the container is named by the identifier. */
|
|
18
|
+
namingPolicy?: ReadModelNamingPolicy;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/** Builds the registration payload shared by projections, reducers, and direct read-model registration. */
|
|
@@ -22,7 +25,7 @@ export function buildReadModelDefinition(options: ReadModelDefinitionOptions) {
|
|
|
22
25
|
Identifier: options.identifier,
|
|
23
26
|
Generation: 1
|
|
24
27
|
},
|
|
25
|
-
ContainerName: options.identifier,
|
|
28
|
+
ContainerName: options.namingPolicy ? options.namingPolicy(options.identifier, options.type as Constructor | undefined) : options.identifier,
|
|
26
29
|
DisplayName: options.identifier,
|
|
27
30
|
Sink: {
|
|
28
31
|
ConfigurationId: toContractsGuid(Guid.empty),
|
|
@@ -0,0 +1,137 @@
|
|
|
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 { Constructor } from '@cratis/fundamentals';
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
8
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
9
|
+
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
10
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
11
|
+
import { IProjectionBuilderFor } from '../../../projections/declarative/IProjectionBuilderFor.js';
|
|
12
|
+
import { IProjectionFor } from '../../../projections/declarative/IProjectionFor.js';
|
|
13
|
+
import { projection } from '../../../projections/declarative/projection.js';
|
|
14
|
+
import { fromEvent } from '../../../projections/modelBound/fromEvent.js';
|
|
15
|
+
import { setFrom } from '../../../projections/modelBound/setFrom.js';
|
|
16
|
+
import { Projections } from '../../../projections/Projections.js';
|
|
17
|
+
import { reducer } from '../../../reducers/reducer.js';
|
|
18
|
+
import { Reducers } from '../../../reducers/Reducers.js';
|
|
19
|
+
import { ReadModels } from '../../ReadModels.js';
|
|
20
|
+
import type { ReadModelNamingPolicy } from '../../ReadModelNamingPolicy.js';
|
|
21
|
+
|
|
22
|
+
export class AccountOpened { owner!: string; }
|
|
23
|
+
eventType()(AccountOpened);
|
|
24
|
+
|
|
25
|
+
export class AccountSummary { id!: string; owner!: string; }
|
|
26
|
+
export class AccountSummaryProjection implements IProjectionFor<AccountSummary> {
|
|
27
|
+
define(builder: IProjectionBuilderFor<AccountSummary>): void {
|
|
28
|
+
builder.from(AccountOpened);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
projection('AccountSummary', AccountSummary)(AccountSummaryProjection);
|
|
32
|
+
|
|
33
|
+
export class AccountBalance { id!: string; owner!: string; }
|
|
34
|
+
setFrom(AccountOpened, 'owner')(AccountBalance.prototype, 'owner');
|
|
35
|
+
fromEvent(AccountOpened)(AccountBalance);
|
|
36
|
+
|
|
37
|
+
export class AccountActivity { count = 0; }
|
|
38
|
+
export class AccountActivityReducer {
|
|
39
|
+
accountOpened(_event: AccountOpened, current: AccountActivity | undefined): AccountActivity {
|
|
40
|
+
return { count: (current?.count ?? 0) + 1 };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
reducer('account-activity', undefined, AccountActivity)(AccountActivityReducer);
|
|
44
|
+
|
|
45
|
+
/** Container name a policy that upper-cases the identifier and reports the class it was given. */
|
|
46
|
+
export const upperCasePolicy: ReadModelNamingPolicy = identifier => identifier.toUpperCase();
|
|
47
|
+
|
|
48
|
+
/** Identifies the class handed to the policy, or `none` when the read model has no class. */
|
|
49
|
+
export const classNamePolicy: ReadModelNamingPolicy = (identifier, readModelType) => `${identifier}:${readModelType?.name ?? 'none'}`;
|
|
50
|
+
|
|
51
|
+
export interface RegisteredReadModel {
|
|
52
|
+
identifier: string;
|
|
53
|
+
containerName: string;
|
|
54
|
+
displayName: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function artifacts(overrides: Partial<IClientArtifactsProvider>): IClientArtifactsProvider {
|
|
58
|
+
return {
|
|
59
|
+
eventTypes: [AccountOpened],
|
|
60
|
+
readModels: [],
|
|
61
|
+
reactors: [],
|
|
62
|
+
reducers: [],
|
|
63
|
+
seeders: [],
|
|
64
|
+
constraints: [],
|
|
65
|
+
projections: [],
|
|
66
|
+
webhooks: [],
|
|
67
|
+
eventTypeMigrations: [],
|
|
68
|
+
globalForHandlers: [],
|
|
69
|
+
...overrides
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Definition {
|
|
74
|
+
Type: { Identifier: string };
|
|
75
|
+
ContainerName: string;
|
|
76
|
+
DisplayName: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toRegistered(registerMany: ReturnType<typeof vi.fn>): RegisteredReadModel[] {
|
|
80
|
+
return registerMany.mock.calls.flatMap(call => (call[0].ReadModels as Definition[]).map(definition => ({
|
|
81
|
+
identifier: definition.Type.Identifier,
|
|
82
|
+
containerName: definition.ContainerName,
|
|
83
|
+
displayName: definition.DisplayName
|
|
84
|
+
})));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Registers a declarative projection through {@link Projections} and returns what reached the kernel. */
|
|
88
|
+
export async function registerDeclarativeProjection(policy?: ReadModelNamingPolicy): Promise<RegisteredReadModel[]> {
|
|
89
|
+
const registerMany = vi.fn().mockResolvedValue(undefined);
|
|
90
|
+
const connection = { projections: { register: vi.fn().mockResolvedValue(undefined) }, readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
91
|
+
const provider = artifacts({ projections: [AccountSummaryProjection], readModels: [AccountSummary] });
|
|
92
|
+
await new Projections('store', 'Default', connection, provider, 'sink', policy).register();
|
|
93
|
+
return toRegistered(registerMany);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Registers a model-bound projection through {@link Projections} and returns what reached the kernel. */
|
|
97
|
+
export async function registerModelBoundProjection(policy?: ReadModelNamingPolicy): Promise<RegisteredReadModel[]> {
|
|
98
|
+
const registerMany = vi.fn().mockResolvedValue(undefined);
|
|
99
|
+
const connection = { projections: { register: vi.fn().mockResolvedValue(undefined) }, readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
100
|
+
const provider = artifacts({ readModels: [AccountBalance] });
|
|
101
|
+
await new Projections('store', 'Default', connection, provider, 'sink', policy).register();
|
|
102
|
+
return toRegistered(registerMany);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Registers a reducer's read model through {@link Reducers} and returns what reached the kernel. */
|
|
106
|
+
export async function registerReducer(policy?: ReadModelNamingPolicy): Promise<RegisteredReadModel[]> {
|
|
107
|
+
const registerMany = vi.fn().mockResolvedValue({});
|
|
108
|
+
const observe = vi.fn().mockImplementation(async function* (queue: AsyncIterable<unknown>) {
|
|
109
|
+
for await (const _message of queue) break;
|
|
110
|
+
});
|
|
111
|
+
const connection = { reducers: { observe }, readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
112
|
+
const provider = artifacts({ reducers: [AccountActivityReducer], readModels: [AccountActivity] });
|
|
113
|
+
const lifecycle = new ConnectionLifecycle();
|
|
114
|
+
const reducers = new Reducers(provider, connection, 'store', 'Default', lifecycle, 'sink', undefined, undefined, policy);
|
|
115
|
+
try {
|
|
116
|
+
await reducers.register();
|
|
117
|
+
} finally {
|
|
118
|
+
reducers.dispose();
|
|
119
|
+
}
|
|
120
|
+
return toRegistered(registerMany);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Registers read models directly through {@link ReadModels.register} and returns what reached the kernel. */
|
|
124
|
+
export async function registerDirectly(readModelType: Constructor, artifactOverrides: Partial<IClientArtifactsProvider>, policy?: ReadModelNamingPolicy): Promise<RegisteredReadModel[]> {
|
|
125
|
+
const registerMany = vi.fn().mockResolvedValue(undefined);
|
|
126
|
+
const connection = { readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
127
|
+
const readModels = new ReadModels('store', 'Default', connection, artifacts(artifactOverrides), 'sink', undefined, policy);
|
|
128
|
+
await readModels.register(readModelType);
|
|
129
|
+
return toRegistered(registerMany);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const directProjection = (policy?: ReadModelNamingPolicy) =>
|
|
133
|
+
registerDirectly(AccountSummary, { projections: [AccountSummaryProjection], readModels: [AccountSummary] }, policy);
|
|
134
|
+
export const directModelBound = (policy?: ReadModelNamingPolicy) =>
|
|
135
|
+
registerDirectly(AccountBalance, { readModels: [AccountBalance] }, policy);
|
|
136
|
+
export const directReducer = (policy?: ReadModelNamingPolicy) =>
|
|
137
|
+
registerDirectly(AccountActivity, { reducers: [AccountActivityReducer], readModels: [AccountActivity] }, policy);
|
package/readModels/for_ReadModelNamingPolicy/when_configuring_options/with_a_naming_policy.spec.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 { beforeEach, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
6
|
+
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
7
|
+
import { EventStore } from '../../../EventStore.js';
|
|
8
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
9
|
+
import { AccountBalance, AccountOpened, upperCasePolicy } from '../given/registered_read_models.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
|
|
13
|
+
const transport = vi.hoisted(() => ({
|
|
14
|
+
connect: vi.fn(),
|
|
15
|
+
getVersionInfo: vi.fn(),
|
|
16
|
+
ensureEventStore: vi.fn(),
|
|
17
|
+
registerMany: vi.fn()
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../../../connection/ChronicleConnection', () => ({
|
|
21
|
+
ChronicleConnection: class {
|
|
22
|
+
resetChannel = vi.fn().mockResolvedValue(undefined);
|
|
23
|
+
connect = transport.connect;
|
|
24
|
+
disconnect = vi.fn();
|
|
25
|
+
server = { getVersionInfo: transport.getVersionInfo };
|
|
26
|
+
eventStores = { ensureEventStore: transport.ensureEventStore };
|
|
27
|
+
readModels = { registerMany: transport.registerMany };
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('../../../connection/KernelKeepAlive', () => ({
|
|
32
|
+
KernelKeepAlive: class {
|
|
33
|
+
async start() {}
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const artifacts: IClientArtifactsProvider = {
|
|
38
|
+
eventTypes: [AccountOpened],
|
|
39
|
+
readModels: [AccountBalance],
|
|
40
|
+
reactors: [],
|
|
41
|
+
reducers: [],
|
|
42
|
+
seeders: [],
|
|
43
|
+
constraints: [],
|
|
44
|
+
projections: [],
|
|
45
|
+
webhooks: [],
|
|
46
|
+
eventTypeMigrations: [],
|
|
47
|
+
globalForHandlers: []
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const connectionString = 'chronicle://localhost:35000';
|
|
51
|
+
|
|
52
|
+
describe('when configuring options without a read model naming policy', () => {
|
|
53
|
+
it('should not carry a naming policy', () => {
|
|
54
|
+
(ChronicleOptions.fromConnectionString(connectionString).readModelNamingPolicy === undefined).should.be.true;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('when configuring options with a read model naming policy', () => {
|
|
59
|
+
it('should carry the policy from the connection string factory', () => {
|
|
60
|
+
ChronicleOptions.fromConnectionString(connectionString, { readModelNamingPolicy: upperCasePolicy })
|
|
61
|
+
.readModelNamingPolicy!.should.equal(upperCasePolicy);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should carry the policy from the development factory', () => {
|
|
65
|
+
ChronicleOptions.development({ readModelNamingPolicy: upperCasePolicy })
|
|
66
|
+
.readModelNamingPolicy!.should.equal(upperCasePolicy);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('when an event store registers read models for a client configured with a naming policy', () => {
|
|
71
|
+
let containerName: string;
|
|
72
|
+
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
// Registration of every artifact kind is covered elsewhere; only the store's read-model wiring matters here.
|
|
76
|
+
vi.spyOn(EventStore.prototype, 'registerArtifacts').mockResolvedValue(undefined);
|
|
77
|
+
transport.connect.mockResolvedValue(undefined);
|
|
78
|
+
transport.getVersionInfo.mockResolvedValue({});
|
|
79
|
+
transport.ensureEventStore.mockResolvedValue({ IsSuccess: true });
|
|
80
|
+
transport.registerMany.mockResolvedValue(undefined);
|
|
81
|
+
const client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString, {
|
|
82
|
+
discoveryPatterns: [],
|
|
83
|
+
clientArtifactsProvider: artifacts,
|
|
84
|
+
readModelNamingPolicy: upperCasePolicy
|
|
85
|
+
}));
|
|
86
|
+
try {
|
|
87
|
+
const store = await client.getEventStore('store');
|
|
88
|
+
await store.readModels.register(AccountBalance);
|
|
89
|
+
containerName = transport.registerMany.mock.calls[0][0].ReadModels[0].ContainerName;
|
|
90
|
+
} finally {
|
|
91
|
+
client.dispose();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should name the container with the policy result', () => containerName.should.equal('ACCOUNTBALANCE'));
|
|
96
|
+
});
|
package/readModels/for_ReadModelNamingPolicy/when_registering/a_declarative_projection.spec.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { classNamePolicy, registerDeclarativeProjection, upperCasePolicy, type RegisteredReadModel } from '../given/registered_read_models.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when registering a declarative projection without a naming policy', () => {
|
|
10
|
+
let registered: RegisteredReadModel;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[registered] = await registerDeclarativeProjection();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountSummary'));
|
|
17
|
+
it('should name the container by the identifier', () => registered.containerName.should.equal('AccountSummary'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when registering a declarative projection with a naming policy', () => {
|
|
21
|
+
let registered: RegisteredReadModel;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
[registered] = await registerDeclarativeProjection(upperCasePolicy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountSummary'));
|
|
28
|
+
it('should keep the display name', () => registered.displayName.should.equal('AccountSummary'));
|
|
29
|
+
it('should name the container with the policy result', () => registered.containerName.should.equal('ACCOUNTSUMMARY'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when registering a declarative projection with a naming policy that uses the class', () => {
|
|
33
|
+
let registered: RegisteredReadModel;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
[registered] = await registerDeclarativeProjection(classNamePolicy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should hand the read model class to the policy', () => registered.containerName.should.equal('AccountSummary:AccountSummary'));
|
|
40
|
+
});
|
package/readModels/for_ReadModelNamingPolicy/when_registering/a_model_bound_projection.spec.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { classNamePolicy, registerModelBoundProjection, upperCasePolicy, type RegisteredReadModel } from '../given/registered_read_models.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when registering a model-bound projection without a naming policy', () => {
|
|
10
|
+
let registered: RegisteredReadModel;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[registered] = await registerModelBoundProjection();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountBalance'));
|
|
17
|
+
it('should name the container by the identifier', () => registered.containerName.should.equal('AccountBalance'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when registering a model-bound projection with a naming policy', () => {
|
|
21
|
+
let registered: RegisteredReadModel;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
[registered] = await registerModelBoundProjection(upperCasePolicy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountBalance'));
|
|
28
|
+
it('should keep the display name', () => registered.displayName.should.equal('AccountBalance'));
|
|
29
|
+
it('should name the container with the policy result', () => registered.containerName.should.equal('ACCOUNTBALANCE'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when registering a model-bound projection with a naming policy that uses the class', () => {
|
|
33
|
+
let registered: RegisteredReadModel;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
[registered] = await registerModelBoundProjection(classNamePolicy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should hand the read model class to the policy', () => registered.containerName.should.equal('AccountBalance:AccountBalance'));
|
|
40
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { classNamePolicy, directModelBound, upperCasePolicy, type RegisteredReadModel } from '../given/registered_read_models.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when registering a model-bound read model directly without a naming policy', () => {
|
|
10
|
+
let registered: RegisteredReadModel;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[registered] = await directModelBound();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountBalance'));
|
|
17
|
+
it('should name the container by the identifier', () => registered.containerName.should.equal('AccountBalance'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when registering a model-bound read model directly with a naming policy', () => {
|
|
21
|
+
let registered: RegisteredReadModel;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
[registered] = await directModelBound(upperCasePolicy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountBalance'));
|
|
28
|
+
it('should keep the display name', () => registered.displayName.should.equal('AccountBalance'));
|
|
29
|
+
it('should name the container with the policy result', () => registered.containerName.should.equal('ACCOUNTBALANCE'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when registering a model-bound read model directly with a naming policy that uses the class', () => {
|
|
33
|
+
let registered: RegisteredReadModel;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
[registered] = await directModelBound(classNamePolicy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should hand the read model class to the policy', () => registered.containerName.should.equal('AccountBalance:AccountBalance'));
|
|
40
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { classNamePolicy, directProjection, upperCasePolicy, type RegisteredReadModel } from '../given/registered_read_models.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when registering a projection read model directly without a naming policy', () => {
|
|
10
|
+
let registered: RegisteredReadModel;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[registered] = await directProjection();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountSummary'));
|
|
17
|
+
it('should name the container by the identifier', () => registered.containerName.should.equal('AccountSummary'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when registering a projection read model directly with a naming policy', () => {
|
|
21
|
+
let registered: RegisteredReadModel;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
[registered] = await directProjection(upperCasePolicy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountSummary'));
|
|
28
|
+
it('should keep the display name', () => registered.displayName.should.equal('AccountSummary'));
|
|
29
|
+
it('should name the container with the policy result', () => registered.containerName.should.equal('ACCOUNTSUMMARY'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when registering a projection read model directly with a naming policy that uses the class', () => {
|
|
33
|
+
let registered: RegisteredReadModel;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
[registered] = await directProjection(classNamePolicy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should hand the read model class to the policy', () => registered.containerName.should.equal('AccountSummary:AccountSummary'));
|
|
40
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { classNamePolicy, registerReducer, upperCasePolicy, type RegisteredReadModel } from '../given/registered_read_models.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when registering a reducer without a naming policy', () => {
|
|
10
|
+
let registered: RegisteredReadModel;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[registered] = await registerReducer();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountActivity'));
|
|
17
|
+
it('should name the container by the identifier', () => registered.containerName.should.equal('AccountActivity'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when registering a reducer with a naming policy', () => {
|
|
21
|
+
let registered: RegisteredReadModel;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
[registered] = await registerReducer(upperCasePolicy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountActivity'));
|
|
28
|
+
it('should keep the display name', () => registered.displayName.should.equal('AccountActivity'));
|
|
29
|
+
it('should name the container with the policy result', () => registered.containerName.should.equal('ACCOUNTACTIVITY'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when registering a reducer with a naming policy that uses the class', () => {
|
|
33
|
+
let registered: RegisteredReadModel;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
[registered] = await registerReducer(classNamePolicy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should hand the read model class to the policy', () => registered.containerName.should.equal('AccountActivity:AccountActivity'));
|
|
40
|
+
});
|
package/readModels/for_ReadModelNamingPolicy/when_registering/a_reducer_read_model_directly.spec.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { classNamePolicy, directReducer, upperCasePolicy, type RegisteredReadModel } from '../given/registered_read_models.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when registering a reducer read model directly without a naming policy', () => {
|
|
10
|
+
let registered: RegisteredReadModel;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[registered] = await directReducer();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountActivity'));
|
|
17
|
+
it('should name the container by the identifier', () => registered.containerName.should.equal('AccountActivity'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('when registering a reducer read model directly with a naming policy', () => {
|
|
21
|
+
let registered: RegisteredReadModel;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
[registered] = await directReducer(upperCasePolicy);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should keep the identifier', () => registered.identifier.should.equal('AccountActivity'));
|
|
28
|
+
it('should keep the display name', () => registered.displayName.should.equal('AccountActivity'));
|
|
29
|
+
it('should name the container with the policy result', () => registered.containerName.should.equal('ACCOUNTACTIVITY'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when registering a reducer read model directly with a naming policy that uses the class', () => {
|
|
33
|
+
let registered: RegisteredReadModel;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
[registered] = await directReducer(classNamePolicy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should hand the read model class to the policy', () => registered.containerName.should.equal('AccountActivity:AccountActivity'));
|
|
40
|
+
});
|
package/readModels/index.ts
CHANGED
|
@@ -10,5 +10,6 @@ export { MaterializedReadModels } from './MaterializedReadModels.js';
|
|
|
10
10
|
export { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
|
|
11
11
|
export type { ReadModelChangeset } from './ReadModelChangeset.js';
|
|
12
12
|
export type { ReadModelSnapshot } from './ReadModelSnapshot.js';
|
|
13
|
+
export type { ReadModelNamingPolicy } from './ReadModelNamingPolicy.js';
|
|
13
14
|
export { readModel, getReadModelMetadata, isReadModel } from './readModel.js';
|
|
14
15
|
export type { ReadModelMetadata } from './readModel.js';
|