A social knowledge tool for researchers built on ATProto

feat: add CardAddedToCollection and CollectionCreated domain events

Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>

+174 -2
+21 -1
src/modules/cards/domain/Collection.ts
··· 7 7 import { CollectionName } from './value-objects/CollectionName'; 8 8 import { CollectionDescription } from './value-objects/CollectionDescription'; 9 9 import { PublishedRecordId } from './value-objects/PublishedRecordId'; 10 + import { CardAddedToCollectionEvent } from './events/CardAddedToCollectionEvent'; 11 + import { CollectionCreatedEvent } from './events/CollectionCreatedEvent'; 10 12 11 13 export interface CardLink { 12 14 cardId: CardId; ··· 163 165 cardCount: props.cardCount ?? (props.cardLinks || []).length, 164 166 }; 165 167 166 - return ok(new Collection(collectionProps, id)); 168 + const collection = new Collection(collectionProps, id); 169 + 170 + // Raise domain event for new collections (when no id is provided) 171 + if (!id) { 172 + collection.addDomainEvent( 173 + CollectionCreatedEvent.create( 174 + collection.collectionId, 175 + collection.authorId, 176 + collection.name.value 177 + ).unwrap() 178 + ); 179 + } 180 + 181 + return ok(collection); 167 182 } 168 183 169 184 public canAddCard(userId: CuratorId): boolean { ··· 213 228 this.props.cardLinks.push(newLink); 214 229 this.props.cardCount = this.props.cardLinks.length; 215 230 this.props.updatedAt = new Date(); 231 + 232 + // Raise domain event 233 + this.addDomainEvent( 234 + CardAddedToCollectionEvent.create(cardId, this.collectionId, userId).unwrap() 235 + ); 216 236 217 237 return ok(newLink); 218 238 }
+42
src/modules/cards/domain/events/CardAddedToCollectionEvent.ts
··· 1 + import { IDomainEvent } from '../../../../shared/domain/events/IDomainEvent'; 2 + import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; 3 + import { CardId } from '../value-objects/CardId'; 4 + import { CollectionId } from '../value-objects/CollectionId'; 5 + import { CuratorId } from '../value-objects/CuratorId'; 6 + import { EventNames } from '../../../../shared/infrastructure/events/EventConfig'; 7 + import { Result, ok } from '../../../../shared/core/Result'; 8 + 9 + export class CardAddedToCollectionEvent implements IDomainEvent { 10 + public readonly eventName = EventNames.CARD_ADDED_TO_COLLECTION; 11 + public readonly dateTimeOccurred: Date; 12 + 13 + private constructor( 14 + public readonly cardId: CardId, 15 + public readonly collectionId: CollectionId, 16 + public readonly addedBy: CuratorId, 17 + dateTimeOccurred?: Date, 18 + ) { 19 + this.dateTimeOccurred = dateTimeOccurred || new Date(); 20 + } 21 + 22 + public static create( 23 + cardId: CardId, 24 + collectionId: CollectionId, 25 + addedBy: CuratorId, 26 + ): Result<CardAddedToCollectionEvent> { 27 + return ok(new CardAddedToCollectionEvent(cardId, collectionId, addedBy)); 28 + } 29 + 30 + public static reconstruct( 31 + cardId: CardId, 32 + collectionId: CollectionId, 33 + addedBy: CuratorId, 34 + dateTimeOccurred: Date, 35 + ): Result<CardAddedToCollectionEvent> { 36 + return ok(new CardAddedToCollectionEvent(cardId, collectionId, addedBy, dateTimeOccurred)); 37 + } 38 + 39 + getAggregateId(): UniqueEntityID { 40 + return this.collectionId.getValue(); 41 + } 42 + }
+41
src/modules/cards/domain/events/CollectionCreatedEvent.ts
··· 1 + import { IDomainEvent } from '../../../../shared/domain/events/IDomainEvent'; 2 + import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; 3 + import { CollectionId } from '../value-objects/CollectionId'; 4 + import { CuratorId } from '../value-objects/CuratorId'; 5 + import { EventNames } from '../../../../shared/infrastructure/events/EventConfig'; 6 + import { Result, ok } from '../../../../shared/core/Result'; 7 + 8 + export class CollectionCreatedEvent implements IDomainEvent { 9 + public readonly eventName = EventNames.COLLECTION_CREATED; 10 + public readonly dateTimeOccurred: Date; 11 + 12 + private constructor( 13 + public readonly collectionId: CollectionId, 14 + public readonly authorId: CuratorId, 15 + public readonly collectionName: string, 16 + dateTimeOccurred?: Date, 17 + ) { 18 + this.dateTimeOccurred = dateTimeOccurred || new Date(); 19 + } 20 + 21 + public static create( 22 + collectionId: CollectionId, 23 + authorId: CuratorId, 24 + collectionName: string, 25 + ): Result<CollectionCreatedEvent> { 26 + return ok(new CollectionCreatedEvent(collectionId, authorId, collectionName)); 27 + } 28 + 29 + public static reconstruct( 30 + collectionId: CollectionId, 31 + authorId: CuratorId, 32 + collectionName: string, 33 + dateTimeOccurred: Date, 34 + ): Result<CollectionCreatedEvent> { 35 + return ok(new CollectionCreatedEvent(collectionId, authorId, collectionName, dateTimeOccurred)); 36 + } 37 + 38 + getAggregateId(): UniqueEntityID { 39 + return this.collectionId.getValue(); 40 + } 41 + }
+2
src/shared/infrastructure/events/EventConfig.ts
··· 1 1 export const EventNames = { 2 2 CARD_ADDED_TO_LIBRARY: 'CardAddedToLibraryEvent', 3 + CARD_ADDED_TO_COLLECTION: 'CardAddedToCollectionEvent', 4 + COLLECTION_CREATED: 'CollectionCreatedEvent', 3 5 } as const; 4 6 5 7 export type EventName = (typeof EventNames)[keyof typeof EventNames];
+68 -1
src/shared/infrastructure/events/EventMapper.ts
··· 1 1 import { IDomainEvent } from '../../domain/events/IDomainEvent'; 2 2 import { CardAddedToLibraryEvent } from '../../../modules/cards/domain/events/CardAddedToLibraryEvent'; 3 + import { CardAddedToCollectionEvent } from '../../../modules/cards/domain/events/CardAddedToCollectionEvent'; 4 + import { CollectionCreatedEvent } from '../../../modules/cards/domain/events/CollectionCreatedEvent'; 3 5 import { CardId } from '../../../modules/cards/domain/value-objects/CardId'; 6 + import { CollectionId } from '../../../modules/cards/domain/value-objects/CollectionId'; 4 7 import { CuratorId } from '../../../modules/cards/domain/value-objects/CuratorId'; 5 8 import { EventNames } from './EventConfig'; 6 9 ··· 16 19 curatorId: string; 17 20 } 18 21 19 - export type SerializedEventUnion = SerializedCardAddedToLibraryEvent; 22 + export interface SerializedCardAddedToCollectionEvent extends SerializedEvent { 23 + eventType: typeof EventNames.CARD_ADDED_TO_COLLECTION; 24 + cardId: string; 25 + collectionId: string; 26 + addedBy: string; 27 + } 28 + 29 + export interface SerializedCollectionCreatedEvent extends SerializedEvent { 30 + eventType: typeof EventNames.COLLECTION_CREATED; 31 + collectionId: string; 32 + authorId: string; 33 + collectionName: string; 34 + } 35 + 36 + export type SerializedEventUnion = 37 + | SerializedCardAddedToLibraryEvent 38 + | SerializedCardAddedToCollectionEvent 39 + | SerializedCollectionCreatedEvent; 20 40 21 41 export class EventMapper { 22 42 static toSerialized(event: IDomainEvent): SerializedEventUnion { ··· 30 50 }; 31 51 } 32 52 53 + if (event instanceof CardAddedToCollectionEvent) { 54 + return { 55 + eventType: EventNames.CARD_ADDED_TO_COLLECTION, 56 + aggregateId: event.getAggregateId().toString(), 57 + dateTimeOccurred: event.dateTimeOccurred.toISOString(), 58 + cardId: event.cardId.getValue().toString(), 59 + collectionId: event.collectionId.getValue().toString(), 60 + addedBy: event.addedBy.value, 61 + }; 62 + } 63 + 64 + if (event instanceof CollectionCreatedEvent) { 65 + return { 66 + eventType: EventNames.COLLECTION_CREATED, 67 + aggregateId: event.getAggregateId().toString(), 68 + dateTimeOccurred: event.dateTimeOccurred.toISOString(), 69 + collectionId: event.collectionId.getValue().toString(), 70 + authorId: event.authorId.value, 71 + collectionName: event.collectionName, 72 + }; 73 + } 74 + 33 75 throw new Error( 34 76 `Unknown event type for serialization: ${event.constructor.name}`, 35 77 ); ··· 45 87 return CardAddedToLibraryEvent.reconstruct( 46 88 cardId, 47 89 curatorId, 90 + dateTimeOccurred, 91 + ).unwrap(); 92 + } 93 + case EventNames.CARD_ADDED_TO_COLLECTION: { 94 + const cardId = CardId.createFromString(eventData.cardId).unwrap(); 95 + const collectionId = CollectionId.createFromString(eventData.collectionId).unwrap(); 96 + const addedBy = CuratorId.create(eventData.addedBy).unwrap(); 97 + const dateTimeOccurred = new Date(eventData.dateTimeOccurred); 98 + 99 + return CardAddedToCollectionEvent.reconstruct( 100 + cardId, 101 + collectionId, 102 + addedBy, 103 + dateTimeOccurred, 104 + ).unwrap(); 105 + } 106 + case EventNames.COLLECTION_CREATED: { 107 + const collectionId = CollectionId.createFromString(eventData.collectionId).unwrap(); 108 + const authorId = CuratorId.create(eventData.authorId).unwrap(); 109 + const dateTimeOccurred = new Date(eventData.dateTimeOccurred); 110 + 111 + return CollectionCreatedEvent.reconstruct( 112 + collectionId, 113 + authorId, 114 + eventData.collectionName, 48 115 dateTimeOccurred, 49 116 ).unwrap(); 50 117 }