diff --git a/src/modules/cards/domain/Collection.ts b/src/modules/cards/domain/Collection.ts index 8ab8f3b1..cc370d4d 100644 --- a/src/modules/cards/domain/Collection.ts +++ b/src/modules/cards/domain/Collection.ts @@ -7,6 +7,8 @@ import { CuratorId } from './value-objects/CuratorId'; import { CollectionName } from './value-objects/CollectionName'; import { CollectionDescription } from './value-objects/CollectionDescription'; import { PublishedRecordId } from './value-objects/PublishedRecordId'; +import { CardAddedToCollectionEvent } from './events/CardAddedToCollectionEvent'; +import { CollectionCreatedEvent } from './events/CollectionCreatedEvent'; export interface CardLink { cardId: CardId; @@ -163,7 +165,20 @@ export class Collection extends AggregateRoot { cardCount: props.cardCount ?? (props.cardLinks || []).length, }; - return ok(new Collection(collectionProps, id)); + const collection = new Collection(collectionProps, id); + + // Raise domain event for new collections (when no id is provided) + if (!id) { + collection.addDomainEvent( + CollectionCreatedEvent.create( + collection.collectionId, + collection.authorId, + collection.name.value + ).unwrap() + ); + } + + return ok(collection); } public canAddCard(userId: CuratorId): boolean { @@ -214,6 +229,11 @@ export class Collection extends AggregateRoot { this.props.cardCount = this.props.cardLinks.length; this.props.updatedAt = new Date(); + // Raise domain event + this.addDomainEvent( + CardAddedToCollectionEvent.create(cardId, this.collectionId, userId).unwrap() + ); + return ok(newLink); } diff --git a/src/modules/cards/domain/events/CardAddedToCollectionEvent.ts b/src/modules/cards/domain/events/CardAddedToCollectionEvent.ts new file mode 100644 index 00000000..5696303e --- /dev/null +++ b/src/modules/cards/domain/events/CardAddedToCollectionEvent.ts @@ -0,0 +1,42 @@ +import { IDomainEvent } from '../../../../shared/domain/events/IDomainEvent'; +import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; +import { CardId } from '../value-objects/CardId'; +import { CollectionId } from '../value-objects/CollectionId'; +import { CuratorId } from '../value-objects/CuratorId'; +import { EventNames } from '../../../../shared/infrastructure/events/EventConfig'; +import { Result, ok } from '../../../../shared/core/Result'; + +export class CardAddedToCollectionEvent implements IDomainEvent { + public readonly eventName = EventNames.CARD_ADDED_TO_COLLECTION; + public readonly dateTimeOccurred: Date; + + private constructor( + public readonly cardId: CardId, + public readonly collectionId: CollectionId, + public readonly addedBy: CuratorId, + dateTimeOccurred?: Date, + ) { + this.dateTimeOccurred = dateTimeOccurred || new Date(); + } + + public static create( + cardId: CardId, + collectionId: CollectionId, + addedBy: CuratorId, + ): Result { + return ok(new CardAddedToCollectionEvent(cardId, collectionId, addedBy)); + } + + public static reconstruct( + cardId: CardId, + collectionId: CollectionId, + addedBy: CuratorId, + dateTimeOccurred: Date, + ): Result { + return ok(new CardAddedToCollectionEvent(cardId, collectionId, addedBy, dateTimeOccurred)); + } + + getAggregateId(): UniqueEntityID { + return this.collectionId.getValue(); + } +} diff --git a/src/modules/cards/domain/events/CollectionCreatedEvent.ts b/src/modules/cards/domain/events/CollectionCreatedEvent.ts new file mode 100644 index 00000000..6ea1ddf1 --- /dev/null +++ b/src/modules/cards/domain/events/CollectionCreatedEvent.ts @@ -0,0 +1,41 @@ +import { IDomainEvent } from '../../../../shared/domain/events/IDomainEvent'; +import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; +import { CollectionId } from '../value-objects/CollectionId'; +import { CuratorId } from '../value-objects/CuratorId'; +import { EventNames } from '../../../../shared/infrastructure/events/EventConfig'; +import { Result, ok } from '../../../../shared/core/Result'; + +export class CollectionCreatedEvent implements IDomainEvent { + public readonly eventName = EventNames.COLLECTION_CREATED; + public readonly dateTimeOccurred: Date; + + private constructor( + public readonly collectionId: CollectionId, + public readonly authorId: CuratorId, + public readonly collectionName: string, + dateTimeOccurred?: Date, + ) { + this.dateTimeOccurred = dateTimeOccurred || new Date(); + } + + public static create( + collectionId: CollectionId, + authorId: CuratorId, + collectionName: string, + ): Result { + return ok(new CollectionCreatedEvent(collectionId, authorId, collectionName)); + } + + public static reconstruct( + collectionId: CollectionId, + authorId: CuratorId, + collectionName: string, + dateTimeOccurred: Date, + ): Result { + return ok(new CollectionCreatedEvent(collectionId, authorId, collectionName, dateTimeOccurred)); + } + + getAggregateId(): UniqueEntityID { + return this.collectionId.getValue(); + } +} diff --git a/src/shared/infrastructure/events/EventConfig.ts b/src/shared/infrastructure/events/EventConfig.ts index 4e6ee422..f6a2fd79 100644 --- a/src/shared/infrastructure/events/EventConfig.ts +++ b/src/shared/infrastructure/events/EventConfig.ts @@ -1,5 +1,7 @@ export const EventNames = { CARD_ADDED_TO_LIBRARY: 'CardAddedToLibraryEvent', + CARD_ADDED_TO_COLLECTION: 'CardAddedToCollectionEvent', + COLLECTION_CREATED: 'CollectionCreatedEvent', } as const; export type EventName = (typeof EventNames)[keyof typeof EventNames]; diff --git a/src/shared/infrastructure/events/EventMapper.ts b/src/shared/infrastructure/events/EventMapper.ts index c1c43a38..ae2aedbd 100644 --- a/src/shared/infrastructure/events/EventMapper.ts +++ b/src/shared/infrastructure/events/EventMapper.ts @@ -1,6 +1,9 @@ import { IDomainEvent } from '../../domain/events/IDomainEvent'; import { CardAddedToLibraryEvent } from '../../../modules/cards/domain/events/CardAddedToLibraryEvent'; +import { CardAddedToCollectionEvent } from '../../../modules/cards/domain/events/CardAddedToCollectionEvent'; +import { CollectionCreatedEvent } from '../../../modules/cards/domain/events/CollectionCreatedEvent'; import { CardId } from '../../../modules/cards/domain/value-objects/CardId'; +import { CollectionId } from '../../../modules/cards/domain/value-objects/CollectionId'; import { CuratorId } from '../../../modules/cards/domain/value-objects/CuratorId'; import { EventNames } from './EventConfig'; @@ -16,7 +19,24 @@ export interface SerializedCardAddedToLibraryEvent extends SerializedEvent { curatorId: string; } -export type SerializedEventUnion = SerializedCardAddedToLibraryEvent; +export interface SerializedCardAddedToCollectionEvent extends SerializedEvent { + eventType: typeof EventNames.CARD_ADDED_TO_COLLECTION; + cardId: string; + collectionId: string; + addedBy: string; +} + +export interface SerializedCollectionCreatedEvent extends SerializedEvent { + eventType: typeof EventNames.COLLECTION_CREATED; + collectionId: string; + authorId: string; + collectionName: string; +} + +export type SerializedEventUnion = + | SerializedCardAddedToLibraryEvent + | SerializedCardAddedToCollectionEvent + | SerializedCollectionCreatedEvent; export class EventMapper { static toSerialized(event: IDomainEvent): SerializedEventUnion { @@ -30,6 +50,28 @@ export class EventMapper { }; } + if (event instanceof CardAddedToCollectionEvent) { + return { + eventType: EventNames.CARD_ADDED_TO_COLLECTION, + aggregateId: event.getAggregateId().toString(), + dateTimeOccurred: event.dateTimeOccurred.toISOString(), + cardId: event.cardId.getValue().toString(), + collectionId: event.collectionId.getValue().toString(), + addedBy: event.addedBy.value, + }; + } + + if (event instanceof CollectionCreatedEvent) { + return { + eventType: EventNames.COLLECTION_CREATED, + aggregateId: event.getAggregateId().toString(), + dateTimeOccurred: event.dateTimeOccurred.toISOString(), + collectionId: event.collectionId.getValue().toString(), + authorId: event.authorId.value, + collectionName: event.collectionName, + }; + } + throw new Error( `Unknown event type for serialization: ${event.constructor.name}`, ); @@ -48,6 +90,31 @@ export class EventMapper { dateTimeOccurred, ).unwrap(); } + case EventNames.CARD_ADDED_TO_COLLECTION: { + const cardId = CardId.createFromString(eventData.cardId).unwrap(); + const collectionId = CollectionId.createFromString(eventData.collectionId).unwrap(); + const addedBy = CuratorId.create(eventData.addedBy).unwrap(); + const dateTimeOccurred = new Date(eventData.dateTimeOccurred); + + return CardAddedToCollectionEvent.reconstruct( + cardId, + collectionId, + addedBy, + dateTimeOccurred, + ).unwrap(); + } + case EventNames.COLLECTION_CREATED: { + const collectionId = CollectionId.createFromString(eventData.collectionId).unwrap(); + const authorId = CuratorId.create(eventData.authorId).unwrap(); + const dateTimeOccurred = new Date(eventData.dateTimeOccurred); + + return CollectionCreatedEvent.reconstruct( + collectionId, + authorId, + eventData.collectionName, + dateTimeOccurred, + ).unwrap(); + } default: throw new Error( `Unknown event type for deserialization: ${eventData.eventType}`,