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(`Invalid card ID: ${cardIdResult.error.message}`), 80 ); 81 } 82 const cardId = cardIdResult.value; 83 84 // Find the URL card - it must already exist 85 const existingUrlCardResult = await this.cardRepository.findById(cardId); 86 if (existingUrlCardResult.isErr()) { 87 return err( 88 AppError.UnexpectedError.create(existingUrlCardResult.error), 89 ); 90 } 91 92 const urlCard = existingUrlCardResult.value; 93 if (!urlCard) { 94 return err( 95 new ValidationError( 96 'URL card not found. Please add the URL to your library first.', 97 ), 98 ); 99 } 100 101 // Verify it's a URL card 102 if (!urlCard.isUrlCard) { 103 return err( 104 new ValidationError( 105 'Card must be a URL card to update associations.', 106 ), 107 ); 108 } 109 110 // Verify ownership 111 if (!urlCard.curatorId.equals(curatorId)) { 112 return err( 113 new ValidationError( 114 'You do not have permission to update this card.', 115 ), 116 ); 117 } 118 119 // Get the URL from the card for note operations 120 if (!urlCard.url) { 121 return err(new ValidationError('URL card must have a URL property.')); 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 = await this.cardRepository.save(noteCard); 156 if (saveNoteCardResult.isErr()) { 157 return err( 158 AppError.UnexpectedError.create(saveNoteCardResult.error), 159 ); 160 } 161 } else { 162 // Create new note card 163 const noteCardInput: INoteCardInput = { 164 type: CardTypeEnum.NOTE, 165 text: request.note, 166 parentCardId: urlCard.cardId.getStringValue(), 167 url: url.value, 168 }; 169 170 const noteCardResult = CardFactory.create({ 171 curatorId: request.curatorId, 172 cardInput: noteCardInput, 173 }); 174 175 if (noteCardResult.isErr()) { 176 return err(new ValidationError(noteCardResult.error.message)); 177 } 178 179 noteCard = noteCardResult.value; 180 181 // Save note card 182 const saveNoteCardResult = await this.cardRepository.save(noteCard); 183 if (saveNoteCardResult.isErr()) { 184 return err( 185 AppError.UnexpectedError.create(saveNoteCardResult.error), 186 ); 187 } 188 189 // Add note card to library using domain service 190 const addNoteCardToLibraryResult = 191 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId); 192 if (addNoteCardToLibraryResult.isErr()) { 193 if ( 194 addNoteCardToLibraryResult.error instanceof 195 AppError.UnexpectedError 196 ) { 197 return err(addNoteCardToLibraryResult.error); 198 } 199 return err( 200 new ValidationError(addNoteCardToLibraryResult.error.message), 201 ); 202 } 203 204 // Update noteCard reference to the one returned by the service 205 noteCard = addNoteCardToLibraryResult.value; 206 } 207 } 208 209 const addedToCollections: string[] = []; 210 const removedFromCollections: string[] = []; 211 212 // Handle adding to collections 213 if (request.addToCollections && request.addToCollections.length > 0) { 214 const collectionIds: CollectionId[] = []; 215 for (const collectionIdStr of request.addToCollections) { 216 const collectionIdResult = 217 CollectionId.createFromString(collectionIdStr); 218 if (collectionIdResult.isErr()) { 219 return err( 220 new ValidationError( 221 `Invalid collection ID: ${collectionIdResult.error.message}`, 222 ), 223 ); 224 } 225 collectionIds.push(collectionIdResult.value); 226 } 227 228 const addToCollectionsResult = 229 await this.cardCollectionService.addCardToCollections( 230 urlCard, 231 collectionIds, 232 curatorId, 233 ); 234 if (addToCollectionsResult.isErr()) { 235 if ( 236 addToCollectionsResult.error instanceof AppError.UnexpectedError 237 ) { 238 return err(addToCollectionsResult.error); 239 } 240 return err(new ValidationError(addToCollectionsResult.error.message)); 241 } 242 243 // Publish events for all affected collections 244 const updatedCollections = addToCollectionsResult.value; 245 for (const collection of updatedCollections) { 246 const publishResult = 247 await this.publishEventsForAggregate(collection); 248 if (publishResult.isErr()) { 249 console.error( 250 'Failed to publish events for collection:', 251 publishResult.error, 252 ); 253 // Don't fail the operation if event publishing fails 254 } 255 addedToCollections.push(collection.collectionId.getStringValue()); 256 } 257 } 258 259 // Handle removing from collections 260 if ( 261 request.removeFromCollections && 262 request.removeFromCollections.length > 0 263 ) { 264 const collectionIds: CollectionId[] = []; 265 for (const collectionIdStr of request.removeFromCollections) { 266 const collectionIdResult = 267 CollectionId.createFromString(collectionIdStr); 268 if (collectionIdResult.isErr()) { 269 return err( 270 new ValidationError( 271 `Invalid collection ID: ${collectionIdResult.error.message}`, 272 ), 273 ); 274 } 275 collectionIds.push(collectionIdResult.value); 276 } 277 278 const removeFromCollectionsResult = 279 await this.cardCollectionService.removeCardFromCollections( 280 urlCard, 281 collectionIds, 282 curatorId, 283 ); 284 if (removeFromCollectionsResult.isErr()) { 285 if ( 286 removeFromCollectionsResult.error instanceof 287 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}