// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { createLogger } from '../logging/createLogger.js'; import { IClientArtifactsProvider } from '../artifacts/index.js'; import { ChronicleConnection } from '../connection/index.js'; import { ensureCommandSuccess } from '../connection/callResults.js'; import { EventStoreNamespaceName } from '../EventStoreNamespaceName.js'; import { getEventTypeFor } from '../events/eventTypeDecorator.js'; import { getTagsFor } from '../events/tagDecorator.js'; import { ICanSeedEvents } from './ICanSeedEvents.js'; import { IEventSeeding } from './IEventSeeding.js'; import { IEventSeedingBuilder } from './IEventSeedingBuilder.js'; import { IEventSeedingScopeBuilder } from './IEventSeedingScopeBuilder.js'; interface SeedingEntry { readonly eventSourceId: string; readonly eventTypeId: string; readonly content: string; readonly tags: string[]; readonly isGlobal: boolean; readonly targetNamespace: string; } /** * Implements {@link IEventSeeding}, managing discovery and registration of event seeders * with the Chronicle Kernel. */ export class EventSeeding implements IEventSeeding { private readonly _logger: ReturnType; private readonly _entries: SeedingEntry[] = []; /** * Creates a new {@link EventSeeding} instance. * @param _eventStoreName - The name of the event store. * @param _connection - The Chronicle gRPC connection. * @param _clientArtifacts - Provider for discovered client artifact types. */ constructor( private readonly _eventStoreName: string, private readonly _connection: ChronicleConnection, private readonly _clientArtifacts: IClientArtifactsProvider ) { this._logger = createLogger('@cratis/chronicle/EventSeeding', _connection.logger); } /** @inheritdoc */ for(eventSourceId: string, events: Iterable): IEventSeedingBuilder { this.addEntries(eventSourceId, events, true, ''); return this; } /** @inheritdoc */ forEventSource(eventSourceId: string, events: Iterable): IEventSeedingBuilder { this.addEntries(eventSourceId, events, true, ''); return this; } /** @inheritdoc */ forNamespace(namespace: string | EventStoreNamespaceName): IEventSeedingScopeBuilder { const targetNamespace = typeof namespace === 'string' ? new EventStoreNamespaceName(namespace).value : namespace.value; return new EventSeedingScopeBuilder(this, targetNamespace); } /** @inheritdoc */ async discover(): Promise { for (const seederType of this._clientArtifacts.seeders) { try { const seeder = new (seederType as new () => ICanSeedEvents)(); await seeder.seed(this); } catch (error) { this._logger.warn('Failed to activate or execute event seeder - skipping seeder', { seederType: seederType.name, error }); } } } /** @inheritdoc */ async register(): Promise { if (this._entries.length === 0) { return; } const globalEntries = this._entries.filter(entry => entry.isGlobal); const namespacedEntriesByNamespace = this.groupBy(this._entries.filter(entry => !entry.isGlobal), entry => entry.targetNamespace); const request = { EventStore: this._eventStoreName, GlobalByEventType: this.toEventTypeSeedEntries(globalEntries), GlobalByEventSource: this.toEventSourceSeedEntries(globalEntries), NamespacedEntries: Array.from(namespacedEntriesByNamespace.entries()).map(([namespaceName, entries]) => ({ Namespace: namespaceName, ByEventType: this.toEventTypeSeedEntries(entries), ByEventSource: this.toEventSourceSeedEntries(entries) })) }; ensureCommandSuccess('seed events', await this._connection.eventSeeding.seedEvents(request)); this._entries.length = 0; } addScoped(eventSourceId: string, events: Iterable, targetNamespace: string): void { this.addEntries(eventSourceId, events, false, targetNamespace); } private addEntries(eventSourceId: string, events: Iterable, isGlobal: boolean, targetNamespace: string): void { for (const event of events) { const eventType = getEventTypeFor(event.constructor as Function); const tags = getTagsFor(event.constructor as Function).map(t => t.value); this._entries.push({ eventSourceId, eventTypeId: eventType.id.value, content: JSON.stringify(event), tags, isGlobal, targetNamespace }); } } private groupBy( values: TValue[], keySelector: (value: TValue) => TKey ): Map { const grouped = new Map(); for (const value of values) { const key = keySelector(value); const existing = grouped.get(key); if (existing) { existing.push(value); } else { grouped.set(key, [value]); } } return grouped; } private toEventTypeSeedEntries(entries: SeedingEntry[]): { EventTypeId: string; Entries: object[] }[] { const byEventType = this.groupBy(entries, entry => entry.eventTypeId); return Array.from(byEventType.entries()).map(([eventTypeId, groupedEntries]) => ({ EventTypeId: eventTypeId, Entries: groupedEntries.map(entry => ({ EventSourceId: entry.eventSourceId, EventTypeId: entry.eventTypeId, Content: entry.content, Tags: entry.tags })) })); } private toEventSourceSeedEntries(entries: SeedingEntry[]): { EventSourceId: string; Entries: object[] }[] { const byEventSource = this.groupBy(entries, entry => entry.eventSourceId); return Array.from(byEventSource.entries()).map(([eventSourceId, groupedEntries]) => ({ EventSourceId: eventSourceId, Entries: groupedEntries.map(entry => ({ EventSourceId: entry.eventSourceId, EventTypeId: entry.eventTypeId, Content: entry.content, Tags: entry.tags })) })); } } class EventSeedingScopeBuilder implements IEventSeedingScopeBuilder { constructor(private readonly _parent: EventSeeding, private readonly _targetNamespace: string) {} /** @inheritdoc */ for(eventSourceId: string, events: Iterable): IEventSeedingScopeBuilder { this._parent.addScoped(eventSourceId, events, this._targetNamespace); return this; } /** @inheritdoc */ forEventSource(eventSourceId: string, events: Iterable): IEventSeedingScopeBuilder { this._parent.addScoped(eventSourceId, events, this._targetNamespace); return this; } }