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 / GetUrlCardViewUseCase.ts
7.6 kB 240 lines
1import { CardId } from 'src/modules/cards/domain/value-objects/CardId'; 2import { err, ok, Result } from 'src/shared/core/Result'; 3import { UseCase } from 'src/shared/core/UseCase'; 4import { 5 ICardQueryRepository, 6 UrlCardView, 7 WithCollections, 8} from '../../../domain/ICardQueryRepository'; 9import { IProfileService } from '../../../domain/services/IProfileService'; 10import { ICollectionRepository } from '../../../domain/ICollectionRepository'; 11import { CollectionId } from '../../../domain/value-objects/CollectionId'; 12import { UserProfileDTO, CollectionDTO } from '@semble/types'; 13import { ProfileEnricher } from '../../services/ProfileEnricher'; 14 15export interface GetUrlCardViewQuery { 16 cardId: string; 17 callingUserId?: string; 18} 19 20// Enriched data for the final use case result 21export type UrlCardViewResult = UrlCardView & { 22 author: UserProfileDTO; 23 collections: CollectionDTO[]; 24 libraries: UserProfileDTO[]; 25}; 26 27export class ValidationError extends Error { 28 constructor(message: string) { 29 super(message); 30 this.name = 'ValidationError'; 31 } 32} 33 34export class CardNotFoundError extends Error { 35 constructor(message: string) { 36 super(message); 37 this.name = 'CardNotFoundError'; 38 } 39} 40 41export class GetUrlCardViewUseCase 42 implements UseCase<GetUrlCardViewQuery, Result<UrlCardViewResult>> 43{ 44 constructor( 45 private cardQueryRepo: ICardQueryRepository, 46 private profileService: IProfileService, 47 private collectionRepo: ICollectionRepository, 48 ) {} 49 50 async execute( 51 query: GetUrlCardViewQuery, 52 ): Promise<Result<UrlCardViewResult>> { 53 // Validate card ID 54 const cardIdResult = CardId.createFromString(query.cardId); 55 if (cardIdResult.isErr()) { 56 return err(new ValidationError('Invalid card ID')); 57 } 58 59 try { 60 // Get the URL card view data 61 const cardView = await this.cardQueryRepo.getUrlCardView( 62 query.cardId, 63 query.callingUserId, 64 ); 65 66 if (!cardView) { 67 return err(new CardNotFoundError('URL card not found')); 68 } 69 70 // Fetch card author profile 71 const cardAuthorResult = await this.profileService.getProfile( 72 cardView.authorId, 73 query.callingUserId, 74 ); 75 76 if (cardAuthorResult.isErr()) { 77 return err( 78 new Error( 79 `Failed to fetch card author: ${cardAuthorResult.error.message}`, 80 ), 81 ); 82 } 83 84 const cardAuthor = cardAuthorResult.value; 85 86 // Get profiles for all users in libraries using ProfileEnricher 87 const profileEnricher = new ProfileEnricher(this.profileService); 88 const userIds = cardView.libraries.map((lib) => lib.userId); 89 const userProfilesResult = await profileEnricher.buildProfileMap( 90 userIds, 91 query.callingUserId, 92 { 93 skipFailures: true, // Skip users with failed profiles 94 mapToUser: false, // Use inline profile (without isFollowing) 95 }, 96 ); 97 98 if (userProfilesResult.isErr()) { 99 return err( 100 new Error( 101 `Failed to fetch user profiles: ${userProfilesResult.error.message}`, 102 ), 103 ); 104 } 105 106 const userProfileMap = userProfilesResult.value; 107 108 // Transform to result format with enriched profile data 109 const enrichedLibraries = cardView.libraries 110 .map((lib) => { 111 const profile = userProfileMap.get(lib.userId); 112 if (!profile) { 113 return null; // Skip if profile not found 114 } 115 116 return { 117 id: profile.id, 118 name: profile.name, 119 handle: profile.handle, 120 avatarUrl: profile.avatarUrl, 121 bannerUrl: profile.bannerUrl, 122 description: profile.description, 123 }; 124 }) 125 .filter((lib): lib is NonNullable<typeof lib> => lib !== null); 126 127 // Fetch all collections first (without author profiles) - FIX: Parallel fetch 128 const collectionsWithData = await Promise.all( 129 cardView.collections.map(async (collection) => { 130 const collectionIdResult = CollectionId.createFromString( 131 collection.id, 132 ); 133 if (collectionIdResult.isErr()) { 134 return null; 135 } 136 const collectionResult = await this.collectionRepo.findById( 137 collectionIdResult.value, 138 ); 139 if (collectionResult.isErr() || !collectionResult.value) { 140 return null; 141 } 142 const fullCollection = collectionResult.value; 143 144 return { 145 id: collection.id, 146 name: collection.name, 147 uri: fullCollection.publishedRecordId?.uri, 148 description: fullCollection.description?.value, 149 accessType: fullCollection.accessType, 150 authorId: fullCollection.authorId.value, 151 cardCount: fullCollection.cardCount, 152 createdAt: fullCollection.createdAt.toISOString(), 153 updatedAt: fullCollection.updatedAt.toISOString(), 154 }; 155 }), 156 ); 157 158 const validCollections = collectionsWithData.filter( 159 (c): c is NonNullable<typeof c> => c !== null, 160 ); 161 162 // Extract unique collection author IDs 163 const collectionAuthorIds = [ 164 ...new Set(validCollections.map((c) => c.authorId)), 165 ]; 166 167 // Batch fetch all collection author profiles - FIX: Batch instead of sequential 168 const collectionAuthorProfilesResult = 169 await profileEnricher.buildProfileMap( 170 collectionAuthorIds, 171 query.callingUserId, 172 { 173 skipFailures: true, 174 mapToUser: false, 175 }, 176 ); 177 178 if (collectionAuthorProfilesResult.isErr()) { 179 return err( 180 new Error( 181 `Failed to fetch collection author profiles: ${collectionAuthorProfilesResult.error.message}`, 182 ), 183 ); 184 } 185 186 const collectionAuthorProfiles = collectionAuthorProfilesResult.value; 187 188 // Build enriched collections with author profiles 189 const enrichedCollections: CollectionDTO[] = validCollections 190 .map((collection) => { 191 const author = collectionAuthorProfiles.get(collection.authorId); 192 if (!author) { 193 return null; // Skip collections with missing author 194 } 195 196 return { 197 id: collection.id, 198 uri: collection.uri, 199 name: collection.name, 200 description: collection.description, 201 accessType: collection.accessType, 202 author: { 203 id: author.id, 204 name: author.name, 205 handle: author.handle, 206 avatarUrl: author.avatarUrl, 207 bannerUrl: author.bannerUrl, 208 description: author.description, 209 }, 210 cardCount: collection.cardCount, 211 createdAt: collection.createdAt, 212 updatedAt: collection.updatedAt, 213 }; 214 }) 215 .filter((c): c is NonNullable<typeof c> => c !== null); 216 217 const result: UrlCardViewResult = { 218 ...cardView, 219 author: { 220 id: cardAuthor.id, 221 name: cardAuthor.name, 222 handle: cardAuthor.handle, 223 avatarUrl: cardAuthor.avatarUrl, 224 bannerUrl: cardAuthor.bannerUrl, 225 description: cardAuthor.bio, 226 }, 227 collections: enrichedCollections, 228 libraries: enrichedLibraries, 229 }; 230 231 return ok(result); 232 } catch (error) { 233 return err( 234 new Error( 235 `Failed to retrieve URL card view: ${error instanceof Error ? error.message : 'Unknown error'}`, 236 ), 237 ); 238 } 239 } 240}