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
11 kB 322 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 { URL } from '../../../domain/value-objects/URL'; 13import { CardCollectionService } from '../../../domain/services/CardCollectionService'; 14import { CardContent } from '../../../domain/value-objects/CardContent'; 15import { CardFactory } from '../../../domain/CardFactory'; 16import { CardLibraryService } from '../../../domain/services/CardLibraryService'; 17 18export interface UpdateUrlCardAssociationsDTO { 19 cardId: string; 20 curatorId: string; 21 note?: string; 22 addToCollections?: string[]; 23 removeFromCollections?: string[]; 24} 25 26export interface UpdateUrlCardAssociationsResponseDTO { 27 urlCardId: string; 28 noteCardId?: string; 29 addedToCollections: string[]; 30 removedFromCollections: string[]; 31} 32 33export class ValidationError extends UseCaseError { 34 constructor(message: string) { 35 super(message); 36 } 37} 38 39export class UpdateUrlCardAssociationsUseCase extends BaseUseCase< 40 UpdateUrlCardAssociationsDTO, 41 Result< 42 UpdateUrlCardAssociationsResponseDTO, 43 ValidationError | AppError.UnexpectedError 44 > 45> { 46 constructor( 47 private cardRepository: ICardRepository, 48 private cardLibraryService: CardLibraryService, 49 private cardCollectionService: CardCollectionService, 50 eventPublisher: IEventPublisher, 51 ) { 52 super(eventPublisher); 53 } 54 55 async execute( 56 request: UpdateUrlCardAssociationsDTO, 57 ): Promise< 58 Result< 59 UpdateUrlCardAssociationsResponseDTO, 60 ValidationError | AppError.UnexpectedError 61 > 62 > { 63 try { 64 // Validate and create CuratorId 65 const curatorIdResult = CuratorId.create(request.curatorId); 66 if (curatorIdResult.isErr()) { 67 return err( 68 new ValidationError( 69 `Invalid curator ID: ${curatorIdResult.error.message}`, 70 ), 71 ); 72 } 73 const curatorId = curatorIdResult.value; 74 75 // Validate and create CardId 76 const cardIdResult = CardId.createFromString(request.cardId); 77 if (cardIdResult.isErr()) { 78 return err( 79 new ValidationError( 80 `Invalid card ID: ${cardIdResult.error.message}`, 81 ), 82 ); 83 } 84 const cardId = cardIdResult.value; 85 86 // Find the URL card - it must already exist 87 const existingUrlCardResult = await this.cardRepository.findById(cardId); 88 if (existingUrlCardResult.isErr()) { 89 return err( 90 AppError.UnexpectedError.create(existingUrlCardResult.error), 91 ); 92 } 93 94 const urlCard = existingUrlCardResult.value; 95 if (!urlCard) { 96 return err( 97 new ValidationError( 98 'URL card not found. Please add the URL to your library first.', 99 ), 100 ); 101 } 102 103 // Verify it's a URL card 104 if (!urlCard.isUrlCard) { 105 return err( 106 new ValidationError('Card must be a URL card to update associations.'), 107 ); 108 } 109 110 // Verify ownership 111 if (!urlCard.curatorId.equals(curatorId)) { 112 return err( 113 new ValidationError('You do not have permission to update this card.'), 114 ); 115 } 116 117 // Get the URL from the card for note operations 118 if (!urlCard.url) { 119 return err( 120 new ValidationError('URL card must have a URL property.'), 121 ); 122 } 123 const url = urlCard.url; 124 125 let noteCard; 126 127 // Handle note updates/creation 128 if (request.note !== undefined) { 129 // Check if note card already exists 130 const existingNoteCardResult = 131 await this.cardRepository.findUsersNoteCardByUrl(url, curatorId); 132 if (existingNoteCardResult.isErr()) { 133 return err( 134 AppError.UnexpectedError.create(existingNoteCardResult.error), 135 ); 136 } 137 138 noteCard = existingNoteCardResult.value; 139 140 if (noteCard) { 141 // Update existing note card 142 const newContentResult = CardContent.createNoteContent(request.note); 143 if (newContentResult.isErr()) { 144 return err(new ValidationError(newContentResult.error.message)); 145 } 146 147 const updateContentResult = noteCard.updateContent( 148 newContentResult.value, 149 ); 150 if (updateContentResult.isErr()) { 151 return err(new ValidationError(updateContentResult.error.message)); 152 } 153 154 // Save updated note card 155 const saveNoteCardResult = 156 await this.cardRepository.save(noteCard); 157 if (saveNoteCardResult.isErr()) { 158 return err( 159 AppError.UnexpectedError.create(saveNoteCardResult.error), 160 ); 161 } 162 } else { 163 // Create new note card 164 const noteCardInput: INoteCardInput = { 165 type: CardTypeEnum.NOTE, 166 text: request.note, 167 parentCardId: urlCard.cardId.getStringValue(), 168 url: url.value, 169 }; 170 171 const noteCardResult = CardFactory.create({ 172 curatorId: request.curatorId, 173 cardInput: noteCardInput, 174 }); 175 176 if (noteCardResult.isErr()) { 177 return err(new ValidationError(noteCardResult.error.message)); 178 } 179 180 noteCard = noteCardResult.value; 181 182 // Save note card 183 const saveNoteCardResult = await this.cardRepository.save(noteCard); 184 if (saveNoteCardResult.isErr()) { 185 return err( 186 AppError.UnexpectedError.create(saveNoteCardResult.error), 187 ); 188 } 189 190 // Add note card to library using domain service 191 const addNoteCardToLibraryResult = 192 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId); 193 if (addNoteCardToLibraryResult.isErr()) { 194 if ( 195 addNoteCardToLibraryResult.error instanceof 196 AppError.UnexpectedError 197 ) { 198 return err(addNoteCardToLibraryResult.error); 199 } 200 return err( 201 new ValidationError(addNoteCardToLibraryResult.error.message), 202 ); 203 } 204 205 // Update noteCard reference to the one returned by the service 206 noteCard = addNoteCardToLibraryResult.value; 207 } 208 } 209 210 const addedToCollections: string[] = []; 211 const removedFromCollections: string[] = []; 212 213 // Handle adding to collections 214 if (request.addToCollections && request.addToCollections.length > 0) { 215 const collectionIds: CollectionId[] = []; 216 for (const collectionIdStr of request.addToCollections) { 217 const collectionIdResult = 218 CollectionId.createFromString(collectionIdStr); 219 if (collectionIdResult.isErr()) { 220 return err( 221 new ValidationError( 222 `Invalid collection ID: ${collectionIdResult.error.message}`, 223 ), 224 ); 225 } 226 collectionIds.push(collectionIdResult.value); 227 } 228 229 const addToCollectionsResult = 230 await this.cardCollectionService.addCardToCollections( 231 urlCard, 232 collectionIds, 233 curatorId, 234 ); 235 if (addToCollectionsResult.isErr()) { 236 if ( 237 addToCollectionsResult.error instanceof AppError.UnexpectedError 238 ) { 239 return err(addToCollectionsResult.error); 240 } 241 return err(new ValidationError(addToCollectionsResult.error.message)); 242 } 243 244 // Publish events for all affected collections 245 const updatedCollections = addToCollectionsResult.value; 246 for (const collection of updatedCollections) { 247 const publishResult = 248 await this.publishEventsForAggregate(collection); 249 if (publishResult.isErr()) { 250 console.error( 251 'Failed to publish events for collection:', 252 publishResult.error, 253 ); 254 // Don't fail the operation if event publishing fails 255 } 256 addedToCollections.push(collection.collectionId.getStringValue()); 257 } 258 } 259 260 // Handle removing from collections 261 if ( 262 request.removeFromCollections && 263 request.removeFromCollections.length > 0 264 ) { 265 const collectionIds: CollectionId[] = []; 266 for (const collectionIdStr of request.removeFromCollections) { 267 const collectionIdResult = 268 CollectionId.createFromString(collectionIdStr); 269 if (collectionIdResult.isErr()) { 270 return err( 271 new ValidationError( 272 `Invalid collection ID: ${collectionIdResult.error.message}`, 273 ), 274 ); 275 } 276 collectionIds.push(collectionIdResult.value); 277 } 278 279 const removeFromCollectionsResult = 280 await this.cardCollectionService.removeCardFromCollections( 281 urlCard, 282 collectionIds, 283 curatorId, 284 ); 285 if (removeFromCollectionsResult.isErr()) { 286 if ( 287 removeFromCollectionsResult.error instanceof AppError.UnexpectedError 288 ) { 289 return err(removeFromCollectionsResult.error); 290 } 291 return err( 292 new ValidationError(removeFromCollectionsResult.error.message), 293 ); 294 } 295 296 // Publish events for all affected collections 297 const updatedCollections = removeFromCollectionsResult.value; 298 for (const collection of updatedCollections) { 299 const publishResult = 300 await this.publishEventsForAggregate(collection); 301 if (publishResult.isErr()) { 302 console.error( 303 'Failed to publish events for collection:', 304 publishResult.error, 305 ); 306 // Don't fail the operation if event publishing fails 307 } 308 removedFromCollections.push(collection.collectionId.getStringValue()); 309 } 310 } 311 312 return ok({ 313 urlCardId: urlCard.cardId.getStringValue(), 314 noteCardId: noteCard?.cardId.getStringValue(), 315 addedToCollections, 316 removedFromCollections, 317 }); 318 } catch (error) { 319 return err(AppError.UnexpectedError.create(error)); 320 } 321 } 322}