UNPKG

@dugongjs/core

Version:

118 lines (117 loc) 7.12 kB
import { aggregateDomainEventApplier } from "../../domain/aggregate-domain-event-applier/aggregate-domain-event-applier.js"; import { AbstractAggregateHandler } from "../abstract-aggregate-handler/abstract-aggregate-handler.js"; import { aggregateSnapshotTransformer } from "../aggregate-snapshot-transformer/aggregate-snapshot-transformer.js"; import { MissingProducerOrMapperError } from "./errors/missing-producer-or-mapper.error.js"; /** * Manager for handling aggregates in the application layer. * Provides methods for applying and committing domain events, creating snapshots, and publishing domain events as messages. */ export class AggregateManager extends AbstractAggregateHandler { constructor(options) { super(options); this.domainEventRepository = options.domainEventRepository; this.snapshotRepository = options.snapshotRepository; this.messageProducer = options.messageProducer; this.outboundMessageMapper = options.outboundMessageMapper; if ((this.messageProducer && !this.outboundMessageMapper) || (!this.messageProducer && this.outboundMessageMapper)) { throw new MissingProducerOrMapperError(); } } /** * Applies staged domain events to the aggregate. * @param aggregate The aggregate instance to which the domain events will be applied. */ applyStagedDomainEvents(aggregate) { aggregateDomainEventApplier.applyStagedDomainEventsToAggregate(aggregate); } /** * Commits staged domain events to the event log and publishes them as messages if necessary. * @param aggregate The aggregate instance whose staged domain events will be committed. * @param options Options for committing domain events, such as correlation ID, triggered by user ID, and metadata. */ async commitStagedDomainEvents(aggregate, options = {}) { const aggregateId = aggregate.getId(); const logCtx = this.getLogContext(aggregateId); const stagedDomainEvents = aggregate.getStagedDomainEvents(); if (stagedDomainEvents.length === 0) { this.logger.verbose(logCtx, "No staged domain events to commit"); return; } for (const domainEvent of stagedDomainEvents) { this.injectDomainEventMetadata(domainEvent, options); } const domainEvents = stagedDomainEvents.map((domainEvent) => domainEvent.serialize()); this.logger.verbose(logCtx, `Committing ${domainEvents.length} staged domain events to event log`); await this.domainEventRepository.saveDomainEvents(this.getTransactionContext(), domainEvents); await this.publishDomainEventsAsMessagesIfNecessary(aggregateId, domainEvents); aggregate.clearStagedDomainEvents(); await this.createSnapshotIfNecessary(aggregate); } /** * Applies and commits staged domain events for the given aggregate instance. * This method combines the application and committing of staged domain events into a single operation. * @param aggregate The aggregate instance for which the staged domain events will be applied and committed. * @param options The options for committing the domain events, such as correlation ID, triggered by user ID, and metadata. */ async applyAndCommitStagedDomainEvents(aggregate, options = {}) { this.applyStagedDomainEvents(aggregate); await this.commitStagedDomainEvents(aggregate, options); } injectDomainEventMetadata(domainEvent, options) { if (this.tenantId) { domainEvent.setTenantId(this.tenantId); } if (options.correlationId) { domainEvent.setCorrelationId(options.correlationId); } if (options.triggeredByUserId) { domainEvent.setTriggeredByUserId(options.triggeredByUserId); } if (options.triggeredByEventId) { domainEvent.setTriggeredByEventId(options.triggeredByEventId); } if (options.metadata) { domainEvent.setMetadata(options.metadata); } } async publishDomainEventsAsMessagesIfNecessary(aggregateId, domainEvents) { const logCtx = this.getLogContext(aggregateId); if (this.messageProducer && this.outboundMessageMapper) { const channelId = this.messageProducer.generateMessageChannelIdForAggregate(this.aggregateOrigin, this.aggregateType); this.logger.verbose(logCtx, `Publishing ${domainEvents.length} staged domain events to message broker on channel ${channelId}`); const messages = domainEvents.map((domainEvent) => this.outboundMessageMapper.map(domainEvent)); await this.messageProducer.publishMessages(this.getTransactionContext(), channelId, messages); this.logger.verbose(logCtx, `${domainEvents.length} staged domain events published to message broker on channel ${channelId}`); } } async createSnapshotIfNecessary(aggregate) { const aggregateId = aggregate.getId(); const logContext = this.getLogContext(aggregateId); if (!this.isSnapshotable) { return; } const currentDomainEventSequenceNumber = aggregate.getCurrentDomainEventSequenceNumber(); const shouldCreateSnapshot = currentDomainEventSequenceNumber > 0 && currentDomainEventSequenceNumber % this.snapshotInterval === 0; if (!shouldCreateSnapshot) { return; } this.logger.verbose(logContext, `Creating snapshot for ${this.aggregateType} aggregate ${aggregateId} at sequence number ${currentDomainEventSequenceNumber}`); const snapshotTestResult = aggregateSnapshotTransformer.canBeRestoredFromSnapshot(this.aggregateClass, aggregate); if (!snapshotTestResult.isEqual) { this.logger.warn(logContext, `Snapshotting of aggregate ${this.aggregateClass.name} was skipped because it cannot be fully restored from snapshot. Make sure the aggregate is properly decorated for snapshotting.`); // NOTE: This is logged directly to the console to make the differences, including class types, more visible. console.log("============================================================"); console.log("Compare the original aggregate and the restored aggregate below to identify the differences:"); console.log("Before taking snapshot:"); console.dir(aggregate, { depth: null }); console.log("After restoring from snapshot:"); console.dir(snapshotTestResult.restored, { depth: null }); console.log("============================================================"); return; } const snapshot = aggregateSnapshotTransformer.takeSnapshot(this.aggregateOrigin, this.aggregateType, aggregate, this.tenantId); await this.snapshotRepository.saveSnapshot(this.getTransactionContext(), snapshot); this.logger.verbose(logContext, `Snapshot for ${this.aggregateType} aggregate ${aggregateId} created at sequence number ${currentDomainEventSequenceNumber}`); } }