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 394 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 = request.context === OperationContext.FIREHOSE_EVENT; 168 const noteCardOptions = isFirehoseEvent && request.publishedRecordIds?.noteCard ? { 169 skipPublishing: true, 170 publishedRecordId: request.publishedRecordIds.noteCard, 171 } : undefined; 172 173 // Update note card in library (handles save and republish) 174 const updateNoteResult = 175 await this.cardLibraryService.updateCardInLibrary(noteCard, curatorId, noteCardOptions); 176 if (updateNoteResult.isErr()) { 177 // Propagate authentication errors 178 if ( 179 updateNoteResult.error instanceof AuthenticationError 180 ) { 181 return err(updateNoteResult.error); 182 } 183 if ( 184 updateNoteResult.error instanceof AppError.UnexpectedError 185 ) { 186 return err(updateNoteResult.error); 187 } 188 return err( 189 new ValidationError(updateNoteResult.error.message), 190 ); 191 } 192 193 // Update noteCard reference to the one returned by the service 194 noteCard = updateNoteResult.value; 195 } else { 196 // Create new note card 197 const noteCardInput: INoteCardInput = { 198 type: CardTypeEnum.NOTE, 199 text: request.note, 200 parentCardId: urlCard.cardId.getStringValue(), 201 url: url.value, 202 }; 203 204 const noteCardResult = CardFactory.create({ 205 curatorId: request.curatorId, 206 cardInput: noteCardInput, 207 }); 208 209 if (noteCardResult.isErr()) { 210 return err(new ValidationError(noteCardResult.error.message)); 211 } 212 213 noteCard = noteCardResult.value; 214 215 // Save note card 216 const saveNoteCardResult = await this.cardRepository.save(noteCard); 217 if (saveNoteCardResult.isErr()) { 218 return err( 219 AppError.UnexpectedError.create(saveNoteCardResult.error), 220 ); 221 } 222 223 // Determine service options based on context 224 const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 225 const noteCardOptions = isFirehoseEvent && request.publishedRecordIds?.noteCard ? { 226 skipPublishing: true, 227 publishedRecordId: request.publishedRecordIds.noteCard, 228 } : undefined; 229 230 // Add note card to library using domain service 231 const addNoteCardToLibraryResult = 232 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId, noteCardOptions); 233 if (addNoteCardToLibraryResult.isErr()) { 234 // Propagate authentication errors 235 if ( 236 addNoteCardToLibraryResult.error instanceof AuthenticationError 237 ) { 238 return err(addNoteCardToLibraryResult.error); 239 } 240 if ( 241 addNoteCardToLibraryResult.error instanceof 242 AppError.UnexpectedError 243 ) { 244 return err(addNoteCardToLibraryResult.error); 245 } 246 return err( 247 new ValidationError(addNoteCardToLibraryResult.error.message), 248 ); 249 } 250 251 // Update noteCard reference to the one returned by the service 252 noteCard = addNoteCardToLibraryResult.value; 253 } 254 } 255 256 const addedToCollections: string[] = []; 257 const removedFromCollections: string[] = []; 258 259 // Handle adding to collections 260 if (request.addToCollections && request.addToCollections.length > 0) { 261 const collectionIds: CollectionId[] = []; 262 for (const collectionIdStr of request.addToCollections) { 263 const collectionIdResult = 264 CollectionId.createFromString(collectionIdStr); 265 if (collectionIdResult.isErr()) { 266 return err( 267 new ValidationError( 268 `Invalid collection ID: ${collectionIdResult.error.message}`, 269 ), 270 ); 271 } 272 collectionIds.push(collectionIdResult.value); 273 } 274 275 // Determine service options based on context 276 const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 277 const collectionOptions = isFirehoseEvent && request.publishedRecordIds?.collectionLinks ? { 278 skipPublishing: true, 279 publishedRecordIds: request.publishedRecordIds.collectionLinks, 280 } : undefined; 281 282 const addToCollectionsResult = 283 await this.cardCollectionService.addCardToCollections( 284 urlCard, 285 collectionIds, 286 curatorId, 287 collectionOptions, 288 ); 289 if (addToCollectionsResult.isErr()) { 290 // Propagate authentication errors 291 if (addToCollectionsResult.error instanceof AuthenticationError) { 292 return err(addToCollectionsResult.error); 293 } 294 if ( 295 addToCollectionsResult.error instanceof AppError.UnexpectedError 296 ) { 297 return err(addToCollectionsResult.error); 298 } 299 return err(new ValidationError(addToCollectionsResult.error.message)); 300 } 301 302 // Publish events for all affected collections 303 const updatedCollections = addToCollectionsResult.value; 304 for (const collection of updatedCollections) { 305 const publishResult = 306 await this.publishEventsForAggregate(collection); 307 if (publishResult.isErr()) { 308 console.error( 309 'Failed to publish events for collection:', 310 publishResult.error, 311 ); 312 // Don't fail the operation if event publishing fails 313 } 314 addedToCollections.push(collection.collectionId.getStringValue()); 315 } 316 } 317 318 // Handle removing from collections 319 if ( 320 request.removeFromCollections && 321 request.removeFromCollections.length > 0 322 ) { 323 const collectionIds: CollectionId[] = []; 324 for (const collectionIdStr of request.removeFromCollections) { 325 const collectionIdResult = 326 CollectionId.createFromString(collectionIdStr); 327 if (collectionIdResult.isErr()) { 328 return err( 329 new ValidationError( 330 `Invalid collection ID: ${collectionIdResult.error.message}`, 331 ), 332 ); 333 } 334 collectionIds.push(collectionIdResult.value); 335 } 336 337 // Determine service options based on context 338 const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 339 const collectionOptions = isFirehoseEvent ? { 340 skipPublishing: true, 341 } : undefined; 342 343 const removeFromCollectionsResult = 344 await this.cardCollectionService.removeCardFromCollections( 345 urlCard, 346 collectionIds, 347 curatorId, 348 collectionOptions, 349 ); 350 if (removeFromCollectionsResult.isErr()) { 351 // Propagate authentication errors 352 if ( 353 removeFromCollectionsResult.error instanceof AuthenticationError 354 ) { 355 return err(removeFromCollectionsResult.error); 356 } 357 if ( 358 removeFromCollectionsResult.error instanceof 359 AppError.UnexpectedError 360 ) { 361 return err(removeFromCollectionsResult.error); 362 } 363 return err( 364 new ValidationError(removeFromCollectionsResult.error.message), 365 ); 366 } 367 368 // Publish events for all affected collections 369 const updatedCollections = removeFromCollectionsResult.value; 370 for (const collection of updatedCollections) { 371 const publishResult = 372 await this.publishEventsForAggregate(collection); 373 if (publishResult.isErr()) { 374 console.error( 375 'Failed to publish events for collection:', 376 publishResult.error, 377 ); 378 // Don't fail the operation if event publishing fails 379 } 380 removedFromCollections.push(collection.collectionId.getStringValue()); 381 } 382 } 383 384 return ok({ 385 urlCardId: urlCard.cardId.getStringValue(), 386 noteCardId: noteCard?.cardId.getStringValue(), 387 addedToCollections, 388 removedFromCollections, 389 }); 390 } catch (error) { 391 return err(AppError.UnexpectedError.create(error)); 392 } 393 } 394}