This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / cards / application / useCases / commands / UpdateUrlCardAssociationsUseCase.ts
14 kB 411 lines
1import { Result, ok, err } from '../../../../../shared/core/Result'; 2import { BaseUseCase } from '../../../../../shared/core/UseCase'; 3import { UseCaseError } from '../../../../../shared/core/UseCaseError'; 4import { AppError } from '../../../../../shared/core/AppError'; 5import { IEventPublisher } from '../../../../../shared/application/events/IEventPublisher'; 6import { ICardRepository } from '../../../domain/ICardRepository'; 7import { INoteCardInput } from '../../../domain/CardFactory'; 8import { CardId } from '../../../domain/value-objects/CardId'; 9import { CollectionId } from '../../../domain/value-objects/CollectionId'; 10import { CuratorId } from '../../../domain/value-objects/CuratorId'; 11import { CardTypeEnum } from '../../../domain/value-objects/CardType'; 12import { CardCollectionService } from '../../../domain/services/CardCollectionService'; 13import { CardContent } from '../../../domain/value-objects/CardContent'; 14import { CardFactory } from '../../../domain/CardFactory'; 15import { CardLibraryService } from '../../../domain/services/CardLibraryService'; 16import { PublishedRecordId } from '../../../domain/value-objects/PublishedRecordId'; 17import { AuthenticationError } from '../../../../../shared/core/AuthenticationError'; 18 19export enum OperationContext { 20 USER_INITIATED = 'user_initiated', 21 FIREHOSE_EVENT = 'firehose_event', 22 SYSTEM_MIGRATION = 'system_migration', 23} 24 25export interface UpdateUrlCardAssociationsDTO { 26 cardId: string; 27 curatorId: string; 28 note?: string; 29 addToCollections?: string[]; 30 removeFromCollections?: string[]; 31 context?: OperationContext; 32 publishedRecordIds?: { 33 noteCard?: PublishedRecordId; 34 collectionLinks?: Map<string, PublishedRecordId>; 35 }; 36} 37 38export interface UpdateUrlCardAssociationsResponseDTO { 39 urlCardId: string; 40 noteCardId?: string; 41 addedToCollections: string[]; 42 removedFromCollections: string[]; 43} 44 45export class ValidationError extends UseCaseError { 46 constructor(message: string) { 47 super(message); 48 } 49} 50 51export class UpdateUrlCardAssociationsUseCase extends BaseUseCase< 52 UpdateUrlCardAssociationsDTO, 53 Result< 54 UpdateUrlCardAssociationsResponseDTO, 55 ValidationError | AuthenticationError | AppError.UnexpectedError 56 > 57> { 58 constructor( 59 private cardRepository: ICardRepository, 60 private cardLibraryService: CardLibraryService, 61 private cardCollectionService: CardCollectionService, 62 eventPublisher: IEventPublisher, 63 ) { 64 super(eventPublisher); 65 } 66 67 async execute( 68 request: UpdateUrlCardAssociationsDTO, 69 ): Promise< 70 Result< 71 UpdateUrlCardAssociationsResponseDTO, 72 ValidationError | AuthenticationError | AppError.UnexpectedError 73 > 74 > { 75 try { 76 // Validate and create CuratorId 77 const curatorIdResult = CuratorId.create(request.curatorId); 78 if (curatorIdResult.isErr()) { 79 return err( 80 new ValidationError( 81 `Invalid curator ID: ${curatorIdResult.error.message}`, 82 ), 83 ); 84 } 85 const curatorId = curatorIdResult.value; 86 87 // Validate and create CardId 88 const cardIdResult = CardId.createFromString(request.cardId); 89 if (cardIdResult.isErr()) { 90 return err( 91 new ValidationError(`Invalid card ID: ${cardIdResult.error.message}`), 92 ); 93 } 94 const cardId = cardIdResult.value; 95 96 // Find the URL card - it must already exist 97 const existingUrlCardResult = await this.cardRepository.findById(cardId); 98 if (existingUrlCardResult.isErr()) { 99 return err( 100 AppError.UnexpectedError.create(existingUrlCardResult.error), 101 ); 102 } 103 104 const urlCard = existingUrlCardResult.value; 105 if (!urlCard) { 106 return err( 107 new ValidationError( 108 'URL card not found. Please add the URL to your library first.', 109 ), 110 ); 111 } 112 113 // Verify it's a URL card 114 if (!urlCard.isUrlCard) { 115 return err( 116 new ValidationError( 117 'Card must be a URL card to update associations.', 118 ), 119 ); 120 } 121 122 // Verify ownership 123 if (!urlCard.curatorId.equals(curatorId)) { 124 return err( 125 new ValidationError( 126 'You do not have permission to update this card.', 127 ), 128 ); 129 } 130 131 // Get the URL from the card for note operations 132 if (!urlCard.url) { 133 return err(new ValidationError('URL card must have a URL property.')); 134 } 135 const url = urlCard.url; 136 137 let noteCard; 138 139 // Handle note updates/creation 140 if (request.note !== undefined) { 141 // Check if note card already exists 142 const existingNoteCardResult = 143 await this.cardRepository.findUsersNoteCardByUrl(url, curatorId); 144 if (existingNoteCardResult.isErr()) { 145 return err( 146 AppError.UnexpectedError.create(existingNoteCardResult.error), 147 ); 148 } 149 150 noteCard = existingNoteCardResult.value; 151 152 if (noteCard) { 153 // Update existing note card 154 const newContentResult = CardContent.createNoteContent(request.note); 155 if (newContentResult.isErr()) { 156 return err(new ValidationError(newContentResult.error.message)); 157 } 158 159 const updateContentResult = noteCard.updateContent( 160 newContentResult.value, 161 ); 162 if (updateContentResult.isErr()) { 163 return err(new ValidationError(updateContentResult.error.message)); 164 } 165 166 // Determine service options based on context 167 const isFirehoseEvent = 168 request.context === OperationContext.FIREHOSE_EVENT; 169 const noteCardOptions = 170 isFirehoseEvent && request.publishedRecordIds?.noteCard 171 ? { 172 skipPublishing: true, 173 publishedRecordId: request.publishedRecordIds.noteCard, 174 } 175 : undefined; 176 177 // Update note card in library (handles save and republish) 178 const updateNoteResult = 179 await this.cardLibraryService.updateCardInLibrary( 180 noteCard, 181 curatorId, 182 noteCardOptions, 183 ); 184 if (updateNoteResult.isErr()) { 185 // Propagate authentication errors 186 if (updateNoteResult.error instanceof AuthenticationError) { 187 return err(updateNoteResult.error); 188 } 189 if (updateNoteResult.error instanceof AppError.UnexpectedError) { 190 return err(updateNoteResult.error); 191 } 192 return err(new ValidationError(updateNoteResult.error.message)); 193 } 194 195 // Update noteCard reference to the one returned by the service 196 noteCard = updateNoteResult.value; 197 } else { 198 // Create new note card 199 const noteCardInput: INoteCardInput = { 200 type: CardTypeEnum.NOTE, 201 text: request.note, 202 parentCardId: urlCard.cardId.getStringValue(), 203 url: url.value, 204 }; 205 206 const noteCardResult = CardFactory.create({ 207 curatorId: request.curatorId, 208 cardInput: noteCardInput, 209 }); 210 211 if (noteCardResult.isErr()) { 212 return err(new ValidationError(noteCardResult.error.message)); 213 } 214 215 noteCard = noteCardResult.value; 216 217 // Save note card 218 const saveNoteCardResult = await this.cardRepository.save(noteCard); 219 if (saveNoteCardResult.isErr()) { 220 return err( 221 AppError.UnexpectedError.create(saveNoteCardResult.error), 222 ); 223 } 224 225 // Determine service options based on context 226 const isFirehoseEvent = 227 request.context === OperationContext.FIREHOSE_EVENT; 228 const noteCardOptions = 229 isFirehoseEvent && request.publishedRecordIds?.noteCard 230 ? { 231 skipPublishing: true, 232 publishedRecordId: request.publishedRecordIds.noteCard, 233 } 234 : undefined; 235 236 // Add note card to library using domain service 237 const addNoteCardToLibraryResult = 238 await this.cardLibraryService.addCardToLibrary( 239 noteCard, 240 curatorId, 241 noteCardOptions, 242 ); 243 if (addNoteCardToLibraryResult.isErr()) { 244 // Propagate authentication errors 245 if ( 246 addNoteCardToLibraryResult.error instanceof AuthenticationError 247 ) { 248 return err(addNoteCardToLibraryResult.error); 249 } 250 if ( 251 addNoteCardToLibraryResult.error instanceof 252 AppError.UnexpectedError 253 ) { 254 return err(addNoteCardToLibraryResult.error); 255 } 256 return err( 257 new ValidationError(addNoteCardToLibraryResult.error.message), 258 ); 259 } 260 261 // Update noteCard reference to the one returned by the service 262 noteCard = addNoteCardToLibraryResult.value; 263 } 264 } 265 266 const addedToCollections: string[] = []; 267 const removedFromCollections: string[] = []; 268 269 // Handle adding to collections 270 if (request.addToCollections && request.addToCollections.length > 0) { 271 const collectionIds: CollectionId[] = []; 272 for (const collectionIdStr of request.addToCollections) { 273 const collectionIdResult = 274 CollectionId.createFromString(collectionIdStr); 275 if (collectionIdResult.isErr()) { 276 return err( 277 new ValidationError( 278 `Invalid collection ID: ${collectionIdResult.error.message}`, 279 ), 280 ); 281 } 282 collectionIds.push(collectionIdResult.value); 283 } 284 285 // Determine service options based on context 286 const isFirehoseEvent = 287 request.context === OperationContext.FIREHOSE_EVENT; 288 const collectionOptions = 289 isFirehoseEvent && request.publishedRecordIds?.collectionLinks 290 ? { 291 skipPublishing: true, 292 publishedRecordIds: request.publishedRecordIds.collectionLinks, 293 } 294 : undefined; 295 296 const addToCollectionsResult = 297 await this.cardCollectionService.addCardToCollections( 298 urlCard, 299 collectionIds, 300 curatorId, 301 collectionOptions, 302 ); 303 if (addToCollectionsResult.isErr()) { 304 // Propagate authentication errors 305 if (addToCollectionsResult.error instanceof AuthenticationError) { 306 return err(addToCollectionsResult.error); 307 } 308 if ( 309 addToCollectionsResult.error instanceof AppError.UnexpectedError 310 ) { 311 return err(addToCollectionsResult.error); 312 } 313 return err(new ValidationError(addToCollectionsResult.error.message)); 314 } 315 316 // Publish events for all affected collections 317 const updatedCollections = addToCollectionsResult.value; 318 for (const collection of updatedCollections) { 319 const publishResult = 320 await this.publishEventsForAggregate(collection); 321 if (publishResult.isErr()) { 322 console.error( 323 'Failed to publish events for collection:', 324 publishResult.error, 325 ); 326 // Don't fail the operation if event publishing fails 327 } 328 addedToCollections.push(collection.collectionId.getStringValue()); 329 } 330 } 331 332 // Handle removing from collections 333 if ( 334 request.removeFromCollections && 335 request.removeFromCollections.length > 0 336 ) { 337 const collectionIds: CollectionId[] = []; 338 for (const collectionIdStr of request.removeFromCollections) { 339 const collectionIdResult = 340 CollectionId.createFromString(collectionIdStr); 341 if (collectionIdResult.isErr()) { 342 return err( 343 new ValidationError( 344 `Invalid collection ID: ${collectionIdResult.error.message}`, 345 ), 346 ); 347 } 348 collectionIds.push(collectionIdResult.value); 349 } 350 351 // Determine service options based on context 352 const isFirehoseEvent = 353 request.context === OperationContext.FIREHOSE_EVENT; 354 const collectionOptions = isFirehoseEvent 355 ? { 356 skipPublishing: true, 357 } 358 : undefined; 359 360 const removeFromCollectionsResult = 361 await this.cardCollectionService.removeCardFromCollections( 362 urlCard, 363 collectionIds, 364 curatorId, 365 collectionOptions, 366 ); 367 if (removeFromCollectionsResult.isErr()) { 368 // Propagate authentication errors 369 if ( 370 removeFromCollectionsResult.error instanceof AuthenticationError 371 ) { 372 return err(removeFromCollectionsResult.error); 373 } 374 if ( 375 removeFromCollectionsResult.error instanceof 376 AppError.UnexpectedError 377 ) { 378 return err(removeFromCollectionsResult.error); 379 } 380 return err( 381 new ValidationError(removeFromCollectionsResult.error.message), 382 ); 383 } 384 385 // Publish events for all affected collections 386 const updatedCollections = removeFromCollectionsResult.value; 387 for (const collection of updatedCollections) { 388 const publishResult = 389 await this.publishEventsForAggregate(collection); 390 if (publishResult.isErr()) { 391 console.error( 392 'Failed to publish events for collection:', 393 publishResult.error, 394 ); 395 // Don't fail the operation if event publishing fails 396 } 397 removedFromCollections.push(collection.collectionId.getStringValue()); 398 } 399 } 400 401 return ok({ 402 urlCardId: urlCard.cardId.getStringValue(), 403 noteCardId: noteCard?.cardId.getStringValue(), 404 addedToCollections, 405 removedFromCollections, 406 }); 407 } catch (error) { 408 return err(AppError.UnexpectedError.create(error)); 409 } 410 } 411}