// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { AutoMap } from '@cratis/chronicle.contracts'; import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals'; import { EventSequenceId } from '../../eventSequences/EventSequenceId.js'; import { canonicalStringify } from '../canonicalStringify.js'; import { ChildrenBuilder } from './ChildrenBuilder.js'; import { IChildrenBuilder } from './IChildrenBuilder.js'; import { INestedBuilder } from './INestedBuilder.js'; import { IProjectionBuilderFor } from './IProjectionBuilderFor.js'; import type { IFromAllBuilder } from './IFromAllBuilder.js'; import { FromAllBuilder } from './FromAllBuilder.js'; import { NestedBuilder } from './NestedBuilder.js'; import { ChildrenDefinitionLike, ContractEventType, ProjectionBuilderCore } from './ProjectionBuilderCore.js'; /** * Concrete implementation of {@link IProjectionBuilderFor} that accumulates projection * configuration and produces a Chronicle-compatible projection definition payload. * @template TReadModel - The read model type this projection produces. */ export class ProjectionBuilderFor extends ProjectionBuilderCore> implements IProjectionBuilderFor { private _eventSequenceId: string = EventSequenceId.eventLog.value; private _containerName: string | undefined; private _rewindable: boolean = true; private _active: boolean = true; private _subscribesToAllEvents = false; private _variantIdentity: Function | undefined; private _variantKey: string | undefined; private readonly _enteringEventTypes: ContractEventType[] = []; constructor() { super(); this._autoMap = AutoMap.Enabled; } /** @inheritdoc */ fromAll(builderCallback: (builder: IFromAllBuilder) => void): this { const builder = new FromAllBuilder(); builderCallback(builder); this._subscribesToAllEvents = true; this._all = { Properties: { ...this._all.Properties, ...builder.properties }, IncludeChildren: true, AutoMap: AutoMap.Inherit }; return this; } /** @inheritdoc */ fromEventSequence(eventSequenceId: string): this { this._eventSequenceId = eventSequenceId; return this; } /** @inheritdoc */ containerName(name: string): this { this._containerName = name; return this; } /** @inheritdoc */ notRewindable(): this { this._rewindable = false; return this; } /** @inheritdoc */ passive(): this { this._active = false; return this; } /** @inheritdoc */ variantOf(identity: Function, keyAccessor: PropertyAccessor): this { const handler = new PropertyPathResolverProxyHandler(); const proxy = new Proxy({}, handler); keyAccessor(proxy as TReadModel); this._variantIdentity = identity; this._variantKey = handler.property; return this; } /** @inheritdoc */ entersOn(eventType: Function, key?: string): this { const contractType = this.toContractEventType(eventType); this._enteringEventTypes.push(contractType); if (key) { const fromEntry = this._from.find(record => record.Key.Id === contractType.Id && record.Key.Generation === contractType.Generation); if (fromEntry) { fromEntry.Value.Key = key; } } return this; } /** * Gets the variant declaration this builder accumulated, if any. * @returns The variant declaration, or undefined when this projection is not a variant. */ getVariantDeclaration(): { identity: Function; key: string; enteringEventTypes: ContractEventType[] } | undefined { if (!this._variantIdentity || !this._variantKey) { return undefined; } return { identity: this._variantIdentity, key: this._variantKey, enteringEventTypes: this._enteringEventTypes }; } /** @inheritdoc */ children( targetPropertyAccessor: PropertyAccessor, builderCallback: (builder: IChildrenBuilder) => void ): this { const handler = new PropertyPathResolverProxyHandler(); const proxy = new Proxy({}, handler); targetPropertyAccessor(proxy as TReadModel); const builder = new ChildrenBuilder(); builderCallback(builder); this._children[handler.property] = builder.buildDefinition(); return this; } /** @inheritdoc */ nested( targetPropertyAccessor: PropertyAccessor, builderCallback: (builder: INestedBuilder) => void ): this { const handler = new PropertyPathResolverProxyHandler(); const proxy = new Proxy({}, handler); targetPropertyAccessor(proxy as TReadModel); const builder = new NestedBuilder(); builderCallback(builder); this._nested[handler.property] = builder.buildDefinition(); return this; } /** Names of children and nested members configured by declarative builders. */ getSubobjectDefinitions(): { children: Record; nested: Record } { return { children: this._children, nested: this._nested }; } /** * Builds the contract-compatible projection definition payload. * @param identifier - The projection identifier. * @param readModelName - The read model container name. * @returns The projection definition object ready to send to the kernel. */ build(identifier: string, readModelName: string): Record { this.resolveJoins(); const definition: Record = { EventSequenceId: this._eventSequenceId, Identifier: identifier, ReadModel: this._containerName ?? readModelName, IsActive: this._active, IsRewindable: this._rewindable, InitialModelState: this._initialState, From: this._from, Join: this._join, Children: this._children, FromEvery: [], All: this._all, ...(this._subscribesToAllEvents ? { SubscribesToAllEvents: true } : {}), FromEventProperty: undefined, RemovedWith: this._removedWith, RemovedWithJoin: this._removedWithJoin, LastUpdated: { Value: '' }, Tags: [], AutoMap: this._autoMap, Nested: this._nested }; definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) }; return definition; } /** * Gets the set of read model properties referenced by configured mappings. * @returns Unique read model property names used by from/join/fromEvery mappings. */ getMappedReadModelProperties(): string[] { const mappedProperties = new Set(); for (const from of this._from) { for (const property of Object.keys(from.Value.Properties)) { mappedProperties.add(property); } } for (const join of this._join) { for (const property of Object.keys(join.Value.Properties)) { mappedProperties.add(property); } } for (const property of Object.keys(this._all.Properties)) { mappedProperties.add(property); } return Array.from(mappedProperties.values()); } /** * Computes a stable, deterministic ISO timestamp from the projection definition content, * excluding the LastUpdated field itself. */ private computeStableLastUpdated(definition: Record): string { const { LastUpdated: _omit, ...rest } = definition; const content = canonicalStringify(rest); let hash = 5381; for (let i = 0; i < content.length; i++) { hash = ((hash << 5) + hash + content.charCodeAt(i)) >>> 0; } return new Date(hash * 1000).toISOString(); } }