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 / queries / GetUrlStatusForMyLibraryUseCase.ts
8.6 kB 228 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 { ICardQueryRepository } from '../../../domain/ICardQueryRepository'; 8import { ICollectionQueryRepository } from '../../../domain/ICollectionQueryRepository'; 9import { ICollectionRepository } from '../../../domain/ICollectionRepository'; 10import { IProfileService } from '../../../domain/services/IProfileService'; 11import { CuratorId } from '../../../domain/value-objects/CuratorId'; 12import { URL } from '../../../domain/value-objects/URL'; 13import { CollectionId } from '../../../domain/value-objects/CollectionId'; 14import { CollectionDTO, UrlCard } from '@semble/types'; 15import { AuthenticationError } from '../../../../../shared/core/AuthenticationError'; 16 17export interface GetUrlStatusForMyLibraryQuery { 18 url: string; 19 curatorId: string; 20} 21 22export interface GetUrlStatusForMyLibraryResult { 23 card?: UrlCard; 24 collections?: CollectionDTO[]; 25} 26 27export class ValidationError extends UseCaseError { 28 constructor(message: string) { 29 super(message); 30 } 31} 32 33export class GetUrlStatusForMyLibraryUseCase extends BaseUseCase< 34 GetUrlStatusForMyLibraryQuery, 35 Result< 36 GetUrlStatusForMyLibraryResult, 37 ValidationError | AuthenticationError | AppError.UnexpectedError 38 > 39> { 40 constructor( 41 private cardRepository: ICardRepository, 42 private cardQueryRepository: ICardQueryRepository, 43 private collectionQueryRepository: ICollectionQueryRepository, 44 private collectionRepo: ICollectionRepository, 45 private profileService: IProfileService, 46 eventPublisher: IEventPublisher, 47 ) { 48 super(eventPublisher); 49 } 50 51 async execute( 52 query: GetUrlStatusForMyLibraryQuery, 53 ): Promise< 54 Result< 55 GetUrlStatusForMyLibraryResult, 56 ValidationError | AuthenticationError | AppError.UnexpectedError 57 > 58 > { 59 try { 60 // Validate and create CuratorId 61 const curatorIdResult = CuratorId.create(query.curatorId); 62 if (curatorIdResult.isErr()) { 63 return err( 64 new ValidationError( 65 `Invalid curator ID: ${curatorIdResult.error.message}`, 66 ), 67 ); 68 } 69 const curatorId = curatorIdResult.value; 70 71 // Validate URL 72 const urlResult = URL.create(query.url); 73 if (urlResult.isErr()) { 74 return err( 75 new ValidationError(`Invalid URL: ${urlResult.error.message}`), 76 ); 77 } 78 const url = urlResult.value; 79 80 // Check if user has a URL card with this URL 81 const existingCardResult = 82 await this.cardRepository.findUsersUrlCardByUrl(url, curatorId); 83 if (existingCardResult.isErr()) { 84 return err(AppError.UnexpectedError.create(existingCardResult.error)); 85 } 86 87 const card = existingCardResult.value; 88 const result: GetUrlStatusForMyLibraryResult = {}; 89 90 if (card) { 91 // Get enriched card data with note 92 const cardView = await this.cardQueryRepository.getUrlCardBasic( 93 card.cardId.getStringValue(), 94 curatorId.value, 95 ); 96 97 if (cardView) { 98 // Get card author profile 99 const authorProfileResult = await this.profileService.getProfile( 100 cardView.authorId, 101 curatorId.value, 102 ); 103 104 if (authorProfileResult.isErr()) { 105 // Propagate authentication errors 106 if (authorProfileResult.error instanceof AuthenticationError) { 107 return err(authorProfileResult.error); 108 } 109 return err( 110 AppError.UnexpectedError.create(authorProfileResult.error), 111 ); 112 } 113 114 const authorProfile = authorProfileResult.value; 115 116 // Transform to UrlCard format 117 result.card = { 118 id: cardView.id, 119 type: 'URL', 120 url: cardView.url, 121 uri: cardView.uri, 122 cardContent: { 123 url: cardView.cardContent.url, 124 title: cardView.cardContent.title, 125 description: cardView.cardContent.description, 126 author: cardView.cardContent.author, 127 publishedDate: cardView.cardContent.publishedDate?.toISOString(), 128 siteName: cardView.cardContent.siteName, 129 imageUrl: cardView.cardContent.imageUrl, 130 type: cardView.cardContent.type, 131 retrievedAt: cardView.cardContent.retrievedAt?.toISOString(), 132 doi: cardView.cardContent.doi, 133 isbn: cardView.cardContent.isbn, 134 }, 135 libraryCount: cardView.libraryCount, 136 urlLibraryCount: cardView.urlLibraryCount, 137 urlInLibrary: cardView.urlInLibrary, 138 createdAt: cardView.createdAt.toISOString(), 139 updatedAt: cardView.updatedAt.toISOString(), 140 author: { 141 id: authorProfile.id, 142 name: authorProfile.name, 143 handle: authorProfile.handle, 144 avatarUrl: authorProfile.avatarUrl, 145 description: authorProfile.bio, 146 }, 147 note: cardView.note, // This includes the note if it exists 148 }; 149 150 // Get collections containing this card for the user 151 try { 152 const collections = 153 await this.collectionQueryRepository.getCollectionsContainingCardForUser( 154 card.cardId.getStringValue(), 155 curatorId.value, 156 ); 157 158 // Enrich collections with full data 159 result.collections = await Promise.all( 160 collections.map(async (collection): Promise<CollectionDTO> => { 161 // Fetch full collection to get dates and cardCount 162 const collectionIdResult = CollectionId.createFromString( 163 collection.id, 164 ); 165 if (collectionIdResult.isErr()) { 166 throw new Error(`Invalid collection ID: ${collection.id}`); 167 } 168 const collectionResult = await this.collectionRepo.findById( 169 collectionIdResult.value, 170 ); 171 if (collectionResult.isErr() || !collectionResult.value) { 172 throw new Error(`Collection not found: ${collection.id}`); 173 } 174 const fullCollection = collectionResult.value; 175 176 // Fetch author profile 177 const authorProfileResult = 178 await this.profileService.getProfile( 179 fullCollection.authorId.value, 180 ); 181 if (authorProfileResult.isErr()) { 182 // Propagate authentication errors 183 if ( 184 authorProfileResult.error instanceof AuthenticationError 185 ) { 186 throw authorProfileResult.error; 187 } 188 throw new Error( 189 `Failed to fetch author profile: ${authorProfileResult.error.message}`, 190 ); 191 } 192 const authorProfile = authorProfileResult.value; 193 194 return { 195 id: collection.id, 196 uri: collection.uri, 197 name: collection.name, 198 description: collection.description, 199 accessType: fullCollection.accessType, 200 author: { 201 id: authorProfile.id, 202 name: authorProfile.name, 203 handle: authorProfile.handle, 204 avatarUrl: authorProfile.avatarUrl, 205 description: authorProfile.bio, 206 }, 207 cardCount: fullCollection.cardCount, 208 createdAt: fullCollection.createdAt.toISOString(), 209 updatedAt: fullCollection.updatedAt.toISOString(), 210 }; 211 }), 212 ); 213 } catch (error) { 214 // Propagate authentication errors 215 if (error instanceof AuthenticationError) { 216 return err(error); 217 } 218 return err(AppError.UnexpectedError.create(error)); 219 } 220 } 221 } 222 223 return ok(result); 224 } catch (error) { 225 return err(AppError.UnexpectedError.create(error)); 226 } 227 } 228}