@dugongjs/core
Version:
118 lines (117 loc) • 7.12 kB
JavaScript
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}`);
}
}