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