@cratis/chronicle 6.1.0 → 6.2.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.
Files changed (77) hide show
  1. package/artifacts/DefaultClientArtifactsProvider.ts +5 -0
  2. package/artifacts/IClientArtifactsProvider.ts +3 -0
  3. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +2 -0
  4. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  5. package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -0
  6. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  7. package/dist/artifacts/IClientArtifactsProvider.d.ts +2 -0
  8. package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
  9. package/dist/projections/GlobalHandlerPropertyNotOnVariant.d.ts +14 -0
  10. package/dist/projections/GlobalHandlerPropertyNotOnVariant.d.ts.map +1 -0
  11. package/dist/projections/GlobalHandlerPropertyNotOnVariant.js +19 -0
  12. package/dist/projections/GlobalHandlerPropertyNotOnVariant.js.map +1 -0
  13. package/dist/projections/Projections.d.ts +8 -0
  14. package/dist/projections/Projections.d.ts.map +1 -1
  15. package/dist/projections/Projections.js +81 -7
  16. package/dist/projections/Projections.js.map +1 -1
  17. package/dist/projections/VariantMustDeclareEntersOnEvent.d.ts +11 -0
  18. package/dist/projections/VariantMustDeclareEntersOnEvent.d.ts.map +1 -0
  19. package/dist/projections/VariantMustDeclareEntersOnEvent.js +16 -0
  20. package/dist/projections/VariantMustDeclareEntersOnEvent.js.map +1 -0
  21. package/dist/projections/VariantReclassifier.d.ts +63 -0
  22. package/dist/projections/VariantReclassifier.d.ts.map +1 -0
  23. package/dist/projections/VariantReclassifier.js +126 -0
  24. package/dist/projections/VariantReclassifier.js.map +1 -0
  25. package/dist/projections/declarative/IProjectionBuilderFor.d.ts +20 -0
  26. package/dist/projections/declarative/IProjectionBuilderFor.d.ts.map +1 -1
  27. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +17 -1
  28. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  29. package/dist/projections/declarative/ProjectionBuilderFor.js +34 -0
  30. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  31. package/dist/projections/index.d.ts +2 -0
  32. package/dist/projections/index.d.ts.map +1 -1
  33. package/dist/projections/index.js +2 -0
  34. package/dist/projections/index.js.map +1 -1
  35. package/dist/projections/modelBound/entersOn.d.ts +25 -0
  36. package/dist/projections/modelBound/entersOn.d.ts.map +1 -0
  37. package/dist/projections/modelBound/entersOn.js +28 -0
  38. package/dist/projections/modelBound/entersOn.js.map +1 -0
  39. package/dist/projections/modelBound/globalFor.d.ts +24 -0
  40. package/dist/projections/modelBound/globalFor.d.ts.map +1 -0
  41. package/dist/projections/modelBound/globalFor.js +31 -0
  42. package/dist/projections/modelBound/globalFor.js.map +1 -0
  43. package/dist/projections/modelBound/index.d.ts +6 -0
  44. package/dist/projections/modelBound/index.d.ts.map +1 -1
  45. package/dist/projections/modelBound/index.js +3 -0
  46. package/dist/projections/modelBound/index.js.map +1 -1
  47. package/dist/projections/modelBound/variantOf.d.ts +24 -0
  48. package/dist/projections/modelBound/variantOf.d.ts.map +1 -0
  49. package/dist/projections/modelBound/variantOf.js +27 -0
  50. package/dist/projections/modelBound/variantOf.js.map +1 -0
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/dist/types/DecoratorType.d.ts +3 -1
  53. package/dist/types/DecoratorType.d.ts.map +1 -1
  54. package/dist/types/DecoratorType.js +2 -0
  55. package/dist/types/DecoratorType.js.map +1 -1
  56. package/events/for_EventContext/when_receiving/with_kernel_metadata.spec.ts +1 -1
  57. package/package.json +2 -2
  58. package/projections/GlobalHandlerPropertyNotOnVariant.ts +19 -0
  59. package/projections/Projections.childrenAndNested.spec.ts +2 -1
  60. package/projections/Projections.declarativeVariants.spec.ts +157 -0
  61. package/projections/Projections.modelBoundCompleteness.spec.ts +2 -1
  62. package/projections/Projections.operationalSurface.spec.ts +2 -1
  63. package/projections/Projections.spec.ts +2 -1
  64. package/projections/Projections.ts +98 -9
  65. package/projections/Projections.variants.spec.ts +253 -0
  66. package/projections/VariantMustDeclareEntersOnEvent.ts +16 -0
  67. package/projections/VariantReclassifier.ts +177 -0
  68. package/projections/declarative/IProjectionBuilderFor.ts +22 -0
  69. package/projections/declarative/ProjectionBuilderFor.ts +40 -1
  70. package/projections/index.ts +2 -0
  71. package/projections/modelBound/entersOn.ts +39 -0
  72. package/projections/modelBound/globalFor.ts +41 -0
  73. package/projections/modelBound/index.ts +6 -0
  74. package/projections/modelBound/variantOf.ts +38 -0
  75. package/readModels/ReadModels.spec.ts +2 -1
  76. package/reducers/Reducers.spec.ts +2 -1
  77. package/types/DecoratorType.ts +4 -1
@@ -0,0 +1,177 @@
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 { ContractEventType, FromRecord, JoinRecord } from './declarative/ProjectionBuilderCore';
5
+ import { GlobalHandlerPropertyNotOnVariant } from './GlobalHandlerPropertyNotOnVariant';
6
+ import { VariantMustDeclareEntersOnEvent } from './VariantMustDeclareEntersOnEvent';
7
+
8
+ const EVENT_SOURCE_ID_KEY = '$eventSourceId';
9
+
10
+ /**
11
+ * What one variant declared about being part of a mutually exclusive group, carried from the
12
+ * point its definition was built to the cross-wiring pass that runs once every variant in the
13
+ * group is known.
14
+ */
15
+ export interface VariantDeclaration {
16
+ /** The identity value used to group definitions for the cross-wiring pass. */
17
+ readonly identity: Function;
18
+ /** The variant's own key property name, used as the correlation property for reclassified joins. */
19
+ readonly key: string;
20
+ /** The event types that activate this variant. */
21
+ readonly enteringEventTypes: ContractEventType[];
22
+ }
23
+
24
+ /**
25
+ * A built projection definition, tagged with its variant declaration when it is one.
26
+ */
27
+ export interface BuiltProjection {
28
+ readonly typeName: string;
29
+ readonly definition: Record<string, unknown>;
30
+ readonly variant?: VariantDeclaration;
31
+ }
32
+
33
+ function eventTypeKey(eventType: ContractEventType): string {
34
+ return `${eventType.Id}:${eventType.Generation}:${eventType.Tombstone}`;
35
+ }
36
+
37
+ /**
38
+ * Merges every mapping a globalFor shared handler declares into a variant's From records, keyed
39
+ * by event type. Runs before {@link reclassify} so a shared mapping for a non-entering event is
40
+ * turned into an update-only join exactly like a mapping declared directly on the variant.
41
+ * @param variantTypeName - The name of the variant type, used for error reporting.
42
+ * @param variantMemberNames - The property names the variant actually has.
43
+ * @param from - The variant's own From records.
44
+ * @param globalHandlers - The shared handler From records applicable to this variant's identity, keyed by handler type name.
45
+ * @returns The merged From records.
46
+ * @throws {@link GlobalHandlerPropertyNotOnVariant} when a shared mapping targets a member the variant does not have.
47
+ */
48
+ export function mergeGlobalHandlers(
49
+ variantTypeName: string,
50
+ variantMemberNames: ReadonlySet<string>,
51
+ from: FromRecord[],
52
+ globalHandlers: ReadonlyMap<string, FromRecord[]>
53
+ ): FromRecord[] {
54
+ if (globalHandlers.size === 0) {
55
+ return from;
56
+ }
57
+
58
+ const byEventType = new Map<string, FromRecord>();
59
+ for (const record of from) {
60
+ byEventType.set(eventTypeKey(record.Key), record);
61
+ }
62
+
63
+ for (const [globalHandlerTypeName, globalRecords] of globalHandlers) {
64
+ for (const globalRecord of globalRecords) {
65
+ for (const propertyName of Object.keys(globalRecord.Value.Properties)) {
66
+ if (!variantMemberNames.has(propertyName)) {
67
+ throw new GlobalHandlerPropertyNotOnVariant(globalHandlerTypeName, variantTypeName, propertyName);
68
+ }
69
+ }
70
+
71
+ const key = eventTypeKey(globalRecord.Key);
72
+ const existing = byEventType.get(key);
73
+ byEventType.set(key, existing
74
+ ? { Key: existing.Key, Value: { ...existing.Value, Properties: { ...existing.Value.Properties, ...globalRecord.Value.Properties } } }
75
+ : globalRecord);
76
+ }
77
+ }
78
+
79
+ return Array.from(byEventType.values());
80
+ }
81
+
82
+ /**
83
+ * Reclassifies every From record that is not one of the entering event types into an update-only
84
+ * join on the variant's own key. The entering event(s) keep their ordinary create-or-update From
85
+ * handler; everything else becomes a self-referential join that can bring an already-active
86
+ * instance up to date but can never create one.
87
+ * @param variantTypeName - The name of the variant type, used for error reporting.
88
+ * @param from - The variant's own (possibly merged) From records.
89
+ * @param join - The variant's own Join records, unaffected by reclassification.
90
+ * @param enteringEventTypes - The event types that activate this variant.
91
+ * @param variantKey - The variant's own key property name.
92
+ * @returns The reclassified From and Join records.
93
+ * @throws {@link VariantMustDeclareEntersOnEvent} when enteringEventTypes is empty.
94
+ */
95
+ export function reclassify(
96
+ variantTypeName: string,
97
+ from: FromRecord[],
98
+ join: JoinRecord[],
99
+ enteringEventTypes: ContractEventType[],
100
+ variantKey: string
101
+ ): { from: FromRecord[]; join: JoinRecord[] } {
102
+ if (enteringEventTypes.length === 0) {
103
+ throw new VariantMustDeclareEntersOnEvent(variantTypeName);
104
+ }
105
+
106
+ const entering = new Set(enteringEventTypes.map(eventTypeKey));
107
+ const remainingFrom: FromRecord[] = [];
108
+ const reclassifiedJoins: JoinRecord[] = [];
109
+
110
+ for (const record of from) {
111
+ if (entering.has(eventTypeKey(record.Key))) {
112
+ remainingFrom.push(record);
113
+ } else {
114
+ reclassifiedJoins.push({
115
+ Key: record.Key,
116
+ Value: {
117
+ On: variantKey,
118
+ Key: record.Value.Key || EVENT_SOURCE_ID_KEY,
119
+ Properties: record.Value.Properties
120
+ }
121
+ });
122
+ }
123
+ }
124
+
125
+ return { from: remainingFrom, join: [...join, ...reclassifiedJoins] };
126
+ }
127
+
128
+ /**
129
+ * Gives every variant a RemovedWith entry for every sibling's entering event(s), and none for
130
+ * its own - the mutual-exclusion cross-wiring that can only run once every variant of a group
131
+ * has been built.
132
+ *
133
+ * A variant cannot be told what removes it while it is being built, because its siblings are
134
+ * not known yet. This runs once per registration call, over every variant discovered in that
135
+ * call.
136
+ * @param projections - Every built projection definition from this registration call, tagged with its variant declaration when it is one.
137
+ */
138
+ export function crossWireGroups(projections: BuiltProjection[]): void {
139
+ const variantEntries = projections.filter((entry): entry is BuiltProjection & { variant: VariantDeclaration } => entry.variant !== undefined);
140
+ if (variantEntries.length === 0) {
141
+ return;
142
+ }
143
+
144
+ const byIdentity = new Map<Function, typeof variantEntries>();
145
+ for (const entry of variantEntries) {
146
+ const group = byIdentity.get(entry.variant.identity) ?? [];
147
+ group.push(entry);
148
+ byIdentity.set(entry.variant.identity, group);
149
+ }
150
+
151
+ for (const entry of variantEntries) {
152
+ const siblings = (byIdentity.get(entry.variant.identity) ?? []).filter(sibling => sibling !== entry);
153
+ if (siblings.length === 0) {
154
+ continue;
155
+ }
156
+
157
+ const existingRemovedWith = (entry.definition.RemovedWith as Array<{ Key: ContractEventType; Value: { Key: string; ParentKey: string } }>) ?? [];
158
+ const removedWithByEventType = new Map<string, { Key: ContractEventType; Value: { Key: string; ParentKey: string } }>();
159
+ for (const record of existingRemovedWith) {
160
+ removedWithByEventType.set(eventTypeKey(record.Key), record);
161
+ }
162
+
163
+ for (const sibling of siblings) {
164
+ for (const eventType of sibling.variant.enteringEventTypes) {
165
+ const key = eventTypeKey(eventType);
166
+ if (!removedWithByEventType.has(key)) {
167
+ removedWithByEventType.set(key, {
168
+ Key: eventType,
169
+ Value: { Key: EVENT_SOURCE_ID_KEY, ParentKey: EVENT_SOURCE_ID_KEY }
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ entry.definition.RemovedWith = Array.from(removedWithByEventType.values());
176
+ }
177
+ }
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import { PropertyAccessor } from '@cratis/fundamentals';
4
5
  import { IProjectionBuilder } from './IProjectionBuilder';
5
6
 
6
7
  /**
@@ -35,4 +36,25 @@ export interface IProjectionBuilderFor<TReadModel>
35
36
  * @returns This builder for fluent chaining.
36
37
  */
37
38
  passive(): IProjectionBuilderFor<TReadModel>;
39
+
40
+ /**
41
+ * Declares this projection to be one of several mutually exclusive representations of the
42
+ * same logical entity. Entering one variant removes the entity from every sibling variant
43
+ * of the same identity.
44
+ * @param identity - The type anchoring the logical identity shared by every variant. Does not need to be a read model itself.
45
+ * @param keyAccessor - Accessor for the property on this variant used as its own key.
46
+ * @returns This builder for fluent chaining.
47
+ */
48
+ variantOf(identity: Function, keyAccessor: PropertyAccessor<TReadModel>): IProjectionBuilderFor<TReadModel>;
49
+
50
+ /**
51
+ * Names an event that may create or resurrect this variant. Repeatable - a variant may enter
52
+ * on more than one event. Every other event this projection is built from is automatically
53
+ * reclassified into an update-only join: it can bring an already-active instance up to date,
54
+ * but it can never create or resurrect one.
55
+ * @param eventType - The event constructor.
56
+ * @param key - Optional event property name used as the key. Defaults to the event source identifier.
57
+ * @returns This builder for fluent chaining.
58
+ */
59
+ entersOn(eventType: Function, key?: string): IProjectionBuilderFor<TReadModel>;
38
60
  }
@@ -9,7 +9,7 @@ import { IChildrenBuilder } from './IChildrenBuilder';
9
9
  import { INestedBuilder } from './INestedBuilder';
10
10
  import { IProjectionBuilderFor } from './IProjectionBuilderFor';
11
11
  import { NestedBuilder } from './NestedBuilder';
12
- import { ProjectionBuilderCore } from './ProjectionBuilderCore';
12
+ import { ContractEventType, ProjectionBuilderCore } from './ProjectionBuilderCore';
13
13
 
14
14
  /**
15
15
  * Concrete implementation of {@link IProjectionBuilderFor} that accumulates projection
@@ -21,6 +21,9 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
21
21
  private _containerName: string | undefined;
22
22
  private _rewindable: boolean = true;
23
23
  private _active: boolean = true;
24
+ private _variantIdentity: Function | undefined;
25
+ private _variantKey: string | undefined;
26
+ private readonly _enteringEventTypes: ContractEventType[] = [];
24
27
 
25
28
  constructor() {
26
29
  super();
@@ -51,6 +54,42 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
51
54
  return this;
52
55
  }
53
56
 
57
+ /** @inheritdoc */
58
+ variantOf(identity: Function, keyAccessor: PropertyAccessor<TReadModel>): this {
59
+ const handler = new PropertyPathResolverProxyHandler();
60
+ const proxy = new Proxy({}, handler);
61
+ keyAccessor(proxy as TReadModel);
62
+
63
+ this._variantIdentity = identity;
64
+ this._variantKey = handler.property;
65
+ return this;
66
+ }
67
+
68
+ /** @inheritdoc */
69
+ entersOn(eventType: Function, key?: string): this {
70
+ const contractType = this.toContractEventType(eventType);
71
+ this._enteringEventTypes.push(contractType);
72
+ if (key) {
73
+ const fromEntry = this._from.find(record => record.Key.Id === contractType.Id && record.Key.Generation === contractType.Generation);
74
+ if (fromEntry) {
75
+ fromEntry.Value.Key = key;
76
+ }
77
+ }
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Gets the variant declaration this builder accumulated, if any.
83
+ * @returns The variant declaration, or undefined when this projection is not a variant.
84
+ */
85
+ getVariantDeclaration(): { identity: Function; key: string; enteringEventTypes: ContractEventType[] } | undefined {
86
+ if (!this._variantIdentity || !this._variantKey) {
87
+ return undefined;
88
+ }
89
+
90
+ return { identity: this._variantIdentity, key: this._variantKey, enteringEventTypes: this._enteringEventTypes };
91
+ }
92
+
54
93
  /** @inheritdoc */
55
94
  children<TChildModel>(
56
95
  targetPropertyAccessor: PropertyAccessor<TReadModel>,
@@ -7,5 +7,7 @@ export { Projections } from './Projections';
7
7
  export type { ProjectionState } from './ProjectionState';
8
8
  export type { ProjectionQueryResult } from './ProjectionQueryResult';
9
9
  export { UnableToQueryProjection } from './UnableToQueryProjection';
10
+ export { VariantMustDeclareEntersOnEvent } from './VariantMustDeclareEntersOnEvent';
11
+ export { GlobalHandlerPropertyNotOnVariant } from './GlobalHandlerPropertyNotOnVariant';
10
12
  export * from './declarative';
11
13
  export * from './modelBound';
@@ -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 'reflect-metadata';
5
+
6
+ /** Metadata stored by the entersOn class decorator. */
7
+ export interface EntersOnMetadata {
8
+ /** The event constructor that may create or resurrect the variant. */
9
+ readonly eventType: Function;
10
+ /** The event property name used as the key. Defaults to the event source identifier. */
11
+ readonly key?: string;
12
+ }
13
+
14
+ const METADATA_KEY = 'chronicle:projection:entersOn';
15
+
16
+ /**
17
+ * Class decorator that names an event that may create or resurrect a read model variant.
18
+ * Repeatable - a variant may enter on more than one event. Every event a variant projects
19
+ * from that is not named here is automatically reclassified into an update-only join: it
20
+ * can bring an already-active instance up to date, but it can never create or resurrect one.
21
+ * @param eventType - The event constructor.
22
+ * @param key - Optional event property name used as the key. Defaults to the event source identifier.
23
+ * @returns A class decorator.
24
+ */
25
+ export function entersOn(eventType: Function, key?: string): ClassDecorator {
26
+ return (target: object) => {
27
+ const existing: EntersOnMetadata[] = Reflect.getMetadata(METADATA_KEY, target) ?? [];
28
+ Reflect.defineMetadata(METADATA_KEY, [...existing, { eventType, key }], target);
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Retrieves entersOn metadata stored on the given class constructor.
34
+ * @param target - The class constructor.
35
+ * @returns An array of entersOn metadata entries.
36
+ */
37
+ export function getEntersOnMetadata(target: Function): EntersOnMetadata[] {
38
+ return Reflect.getMetadata(METADATA_KEY, target) ?? [];
39
+ }
@@ -0,0 +1,41 @@
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 { DecoratorType, TypeDiscoverer } from '../../types';
7
+
8
+ /** Metadata stored by the globalFor class decorator. */
9
+ export interface GlobalForMetadata {
10
+ /** The type anchoring the logical identity this shared handler applies its mappings to. */
11
+ readonly identity: Function;
12
+ }
13
+
14
+ const METADATA_KEY = 'chronicle:projection:globalFor';
15
+
16
+ /**
17
+ * Class decorator that declares a shared handler class whose mappings apply to every variant
18
+ * of the given identity, without repeating them on each variant type. A mapping that targets
19
+ * a member some variant lacks is a declaration error, not a silently skipped mapping.
20
+ *
21
+ * A class decorated only with globalFor is never registered as a projection on its own - it is
22
+ * merged into every variant of its identity before those variants are registered.
23
+ * @param identity - The type anchoring the logical identity this handler's mappings apply to.
24
+ * @returns A class decorator.
25
+ */
26
+ export function globalFor(identity: Function): ClassDecorator {
27
+ return (target: object) => {
28
+ const constructor = target as Constructor;
29
+ Reflect.defineMetadata(METADATA_KEY, { identity }, target);
30
+ TypeDiscoverer.default.register(DecoratorType.GlobalForHandler, constructor, constructor.name);
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Retrieves globalFor metadata stored on the given class constructor.
36
+ * @param target - The class constructor.
37
+ * @returns The globalFor metadata, or undefined if the class is not a shared handler.
38
+ */
39
+ export function getGlobalForMetadata(target: Function): GlobalForMetadata | undefined {
40
+ return Reflect.getMetadata(METADATA_KEY, target);
41
+ }
@@ -39,3 +39,9 @@ export { fromAll, getFromAllMetadata } from './fromAll';
39
39
  export type { FromAllMetadata } from './fromAll';
40
40
  export { noAutoMap, isNoAutoMap, isPropertyNoAutoMap } from './noAutoMap';
41
41
  export { eventSequence, eventLog, getEventSequenceMetadata } from './eventSequence';
42
+ export { variantOf, getVariantOfMetadata } from './variantOf';
43
+ export type { VariantOfMetadata } from './variantOf';
44
+ export { entersOn, getEntersOnMetadata } from './entersOn';
45
+ export type { EntersOnMetadata } from './entersOn';
46
+ export { globalFor, getGlobalForMetadata } from './globalFor';
47
+ export type { GlobalForMetadata } from './globalFor';
@@ -0,0 +1,38 @@
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
+
6
+ /** Metadata stored by the variantOf class decorator. */
7
+ export interface VariantOfMetadata {
8
+ /** The type anchoring the logical identity every variant of this group shares. */
9
+ readonly identity: Function;
10
+ /** The property name on this variant used as its own key, and as the correlation property for reclassified joins. */
11
+ readonly key: string;
12
+ }
13
+
14
+ const METADATA_KEY = 'chronicle:projection:variantOf';
15
+
16
+ /**
17
+ * Class decorator that declares a read model to be one of several mutually exclusive
18
+ * representations of the same logical entity. Entering one variant removes the entity
19
+ * from every sibling variant of the same identity.
20
+ * @param identity - The type anchoring the logical identity shared by every variant. Does not need to be a read model itself.
21
+ * @param key - The property name on this variant used as its own key.
22
+ * @returns A class decorator.
23
+ */
24
+ export function variantOf(identity: Function, key: string): ClassDecorator {
25
+ return (target: object) => {
26
+ const metadata: VariantOfMetadata = { identity, key };
27
+ Reflect.defineMetadata(METADATA_KEY, metadata, target);
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Retrieves variantOf metadata stored on the given class constructor.
33
+ * @param target - The class constructor.
34
+ * @returns The variantOf metadata, or undefined if the class is not a variant.
35
+ */
36
+ export function getVariantOfMetadata(target: Function): VariantOfMetadata | undefined {
37
+ return Reflect.getMetadata(METADATA_KEY, target);
38
+ }
@@ -33,7 +33,8 @@ function createReadModels(readModelType: Constructor, releaseResponse: Record<st
33
33
  constraints: [],
34
34
  projections: [],
35
35
  webhooks: [],
36
- eventTypeMigrations: []
36
+ eventTypeMigrations: [],
37
+ globalForHandlers: []
37
38
  } as IClientArtifactsProvider;
38
39
 
39
40
  const readModels = new ReadModels('test-store', 'test-namespace', connection, clientArtifacts, 'default-sink');
@@ -62,7 +62,8 @@ function createArtifacts(reducers: Constructor[]): IClientArtifactsProvider {
62
62
  constraints: [],
63
63
  projections: [],
64
64
  webhooks: [],
65
- eventTypeMigrations: []
65
+ eventTypeMigrations: [],
66
+ globalForHandlers: []
66
67
  };
67
68
  }
68
69
 
@@ -30,5 +30,8 @@ export enum DecoratorType {
30
30
  Webhook = 'webhook',
31
31
 
32
32
  /** Event type migration artifacts discovered through the eventTypeMigration decorator. */
33
- EventTypeMigration = 'eventTypeMigration'
33
+ EventTypeMigration = 'eventTypeMigration',
34
+
35
+ /** Global read model variant handler artifacts discovered through the globalFor decorator. */
36
+ GlobalForHandler = 'globalForHandler'
34
37
  }