A social knowledge tool for researchers built on ATProto
at development 260 lines 8.6 kB view raw
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 { 8 CardFactory, 9 IUrlCardInput, 10 INoteCardInput, 11} from '../../../domain/CardFactory'; 12import { CollectionId } from '../../../domain/value-objects/CollectionId'; 13import { CuratorId } from '../../../domain/value-objects/CuratorId'; 14import { IMetadataService } from '../../../domain/services/IMetadataService'; 15import { CardTypeEnum } from '../../../domain/value-objects/CardType'; 16import { URL } from '../../../domain/value-objects/URL'; 17import { CardLibraryService } from '../../../domain/services/CardLibraryService'; 18import { CardCollectionService } from '../../../domain/services/CardCollectionService'; 19 20export interface AddUrlToLibraryDTO { 21 url: string; 22 note?: string; 23 collectionIds?: string[]; 24 curatorId: string; 25} 26 27export interface AddUrlToLibraryResponseDTO { 28 urlCardId: string; 29 noteCardId?: string; 30} 31 32export class ValidationError extends UseCaseError { 33 constructor(message: string) { 34 super(message); 35 } 36} 37 38export class AddUrlToLibraryUseCase extends BaseUseCase< 39 AddUrlToLibraryDTO, 40 Result<AddUrlToLibraryResponseDTO, ValidationError | AppError.UnexpectedError> 41> { 42 constructor( 43 private cardRepository: ICardRepository, 44 private metadataService: IMetadataService, 45 private cardLibraryService: CardLibraryService, 46 private cardCollectionService: CardCollectionService, 47 eventPublisher: IEventPublisher, 48 ) { 49 super(eventPublisher); 50 } 51 52 async execute( 53 request: AddUrlToLibraryDTO, 54 ): Promise< 55 Result< 56 AddUrlToLibraryResponseDTO, 57 ValidationError | AppError.UnexpectedError 58 > 59 > { 60 try { 61 // Validate and create CuratorId 62 const curatorIdResult = CuratorId.create(request.curatorId); 63 if (curatorIdResult.isErr()) { 64 return err( 65 new ValidationError( 66 `Invalid curator ID: ${curatorIdResult.error.message}`, 67 ), 68 ); 69 } 70 const curatorId = curatorIdResult.value; 71 72 // Validate URL 73 const urlResult = URL.create(request.url); 74 if (urlResult.isErr()) { 75 return err( 76 new ValidationError(`Invalid URL: ${urlResult.error.message}`), 77 ); 78 } 79 const url = urlResult.value; 80 81 // Check if URL card already exists 82 const existingUrlCardResult = 83 await this.cardRepository.findUsersUrlCardByUrl(url, curatorId); 84 if (existingUrlCardResult.isErr()) { 85 return err( 86 AppError.UnexpectedError.create(existingUrlCardResult.error), 87 ); 88 } 89 90 let urlCard = existingUrlCardResult.value; 91 if (!urlCard) { 92 // Fetch metadata for URL 93 const metadataResult = await this.metadataService.fetchMetadata(url); 94 if (metadataResult.isErr()) { 95 return err( 96 new ValidationError( 97 `Failed to fetch metadata: ${metadataResult.error.message}`, 98 ), 99 ); 100 } 101 102 // Create URL card 103 const urlCardInput: IUrlCardInput = { 104 type: CardTypeEnum.URL, 105 url: request.url, 106 metadata: metadataResult.value, 107 }; 108 109 const urlCardResult = CardFactory.create({ 110 curatorId: request.curatorId, 111 cardInput: urlCardInput, 112 }); 113 114 if (urlCardResult.isErr()) { 115 return err(new ValidationError(urlCardResult.error.message)); 116 } 117 118 urlCard = urlCardResult.value; 119 120 // Save URL card 121 const saveUrlCardResult = await this.cardRepository.save(urlCard); 122 if (saveUrlCardResult.isErr()) { 123 return err(AppError.UnexpectedError.create(saveUrlCardResult.error)); 124 } 125 } 126 127 // Add URL card to library using domain service 128 const addUrlCardToLibraryResult = 129 await this.cardLibraryService.addCardToLibrary(urlCard, curatorId); 130 if (addUrlCardToLibraryResult.isErr()) { 131 if ( 132 addUrlCardToLibraryResult.error instanceof AppError.UnexpectedError 133 ) { 134 return err(addUrlCardToLibraryResult.error); 135 } 136 return err( 137 new ValidationError(addUrlCardToLibraryResult.error.message), 138 ); 139 } 140 141 // Update urlCard reference to the one returned by the service 142 urlCard = addUrlCardToLibraryResult.value; 143 144 let noteCard; 145 146 // Create note card if note is provided 147 if (request.note) { 148 const noteCardInput: INoteCardInput = { 149 type: CardTypeEnum.NOTE, 150 text: request.note, 151 parentCardId: urlCard.cardId.getStringValue(), 152 url: request.url, 153 }; 154 155 const noteCardResult = CardFactory.create({ 156 curatorId: request.curatorId, 157 cardInput: noteCardInput, 158 }); 159 160 if (noteCardResult.isErr()) { 161 return err(new ValidationError(noteCardResult.error.message)); 162 } 163 164 noteCard = noteCardResult.value; 165 166 // Save note card 167 const saveNoteCardResult = await this.cardRepository.save(noteCard); 168 if (saveNoteCardResult.isErr()) { 169 return err(AppError.UnexpectedError.create(saveNoteCardResult.error)); 170 } 171 172 // Add note card to library using domain service 173 const addNoteCardToLibraryResult = 174 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId); 175 if (addNoteCardToLibraryResult.isErr()) { 176 if ( 177 addNoteCardToLibraryResult.error instanceof AppError.UnexpectedError 178 ) { 179 return err(addNoteCardToLibraryResult.error); 180 } 181 return err( 182 new ValidationError(addNoteCardToLibraryResult.error.message), 183 ); 184 } 185 186 // Update noteCard reference to the one returned by the service 187 noteCard = addNoteCardToLibraryResult.value; 188 } 189 190 // Handle collection additions if specified 191 if (request.collectionIds && request.collectionIds.length > 0) { 192 // Always add the URL card to collections 193 const cardToAdd = urlCard; 194 195 // Validate and create CollectionIds 196 const collectionIds: CollectionId[] = []; 197 for (const collectionIdStr of request.collectionIds) { 198 const collectionIdResult = 199 CollectionId.createFromString(collectionIdStr); 200 if (collectionIdResult.isErr()) { 201 return err( 202 new ValidationError( 203 `Invalid collection ID: ${collectionIdResult.error.message}`, 204 ), 205 ); 206 } 207 collectionIds.push(collectionIdResult.value); 208 } 209 210 // Add card to collections using domain service 211 const addToCollectionsResult = 212 await this.cardCollectionService.addCardToCollections( 213 cardToAdd, 214 collectionIds, 215 curatorId, 216 ); 217 if (addToCollectionsResult.isErr()) { 218 if ( 219 addToCollectionsResult.error instanceof AppError.UnexpectedError 220 ) { 221 return err(addToCollectionsResult.error); 222 } 223 return err(new ValidationError(addToCollectionsResult.error.message)); 224 } 225 226 // Publish events for all affected collections 227 const updatedCollections = addToCollectionsResult.value; 228 for (const collection of updatedCollections) { 229 const publishResult = 230 await this.publishEventsForAggregate(collection); 231 if (publishResult.isErr()) { 232 console.error( 233 'Failed to publish events for collection:', 234 publishResult.error, 235 ); 236 // Don't fail the operation if event publishing fails 237 } 238 } 239 } 240 241 // Publish events for URL card (events are raised in addToLibrary method) 242 const publishUrlCardResult = 243 await this.publishEventsForAggregate(urlCard); 244 if (publishUrlCardResult.isErr()) { 245 console.error( 246 'Failed to publish events for URL card:', 247 publishUrlCardResult.error, 248 ); 249 // Don't fail the operation if event publishing fails 250 } 251 252 return ok({ 253 urlCardId: urlCard.cardId.getStringValue(), 254 noteCardId: noteCard?.cardId.getStringValue(), 255 }); 256 } catch (error) { 257 return err(AppError.UnexpectedError.create(error)); 258 } 259 } 260}