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 / GetCollectionPageUseCase.ts
7.2 kB 243 lines
1import { CollectionId } from 'src/modules/cards/domain/value-objects/CollectionId'; 2import { err, ok, Result } from 'src/shared/core/Result'; 3import { UseCase } from 'src/shared/core/UseCase'; 4import { 5 ICardQueryRepository, 6 CardSortField, 7 SortOrder, 8 UrlCardView, 9} from '../../../domain/ICardQueryRepository'; 10import { UrlType } from '../../../domain/value-objects/UrlType'; 11import { ICollectionRepository } from '../../../domain/ICollectionRepository'; 12import { IProfileService } from '../../../domain/services/IProfileService'; 13import { IFollowsRepository } from 'src/modules/user/domain/repositories/IFollowsRepository'; 14import { FollowTargetType } from 'src/modules/user/domain/value-objects/FollowTargetType'; 15import { ProfileEnricher } from '../../services/ProfileEnricher'; 16 17export interface GetCollectionPageQuery { 18 collectionId: string; 19 callingUserId?: string; 20 page?: number; 21 limit?: number; 22 sortBy?: CardSortField; 23 sortOrder?: SortOrder; 24 urlType?: UrlType; 25} 26 27export type CollectionPageUrlCardDTO = UrlCardView; 28export interface GetCollectionPageResult { 29 id: string; 30 uri?: string; 31 name: string; 32 description?: string; 33 author: { 34 id: string; 35 name: string; 36 handle: string; 37 avatarUrl?: string; 38 }; 39 urlCards: CollectionPageUrlCardDTO[]; 40 pagination: { 41 currentPage: number; 42 totalPages: number; 43 totalCount: number; 44 hasMore: boolean; 45 limit: number; 46 }; 47 sorting: { 48 sortBy: CardSortField; 49 sortOrder: SortOrder; 50 }; 51} 52 53export class ValidationError extends Error { 54 constructor(message: string) { 55 super(message); 56 this.name = 'ValidationError'; 57 } 58} 59 60export class CollectionNotFoundError extends Error { 61 constructor(message: string) { 62 super(message); 63 this.name = 'CollectionNotFoundError'; 64 } 65} 66 67export class GetCollectionPageUseCase 68 implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>> 69{ 70 constructor( 71 private collectionRepo: ICollectionRepository, 72 private cardQueryRepo: ICardQueryRepository, 73 private profileService: IProfileService, 74 private followsRepository: IFollowsRepository, 75 ) {} 76 77 async execute( 78 query: GetCollectionPageQuery, 79 ): Promise<Result<GetCollectionPageResult>> { 80 // Set defaults 81 const page = query.page || 1; 82 const limit = Math.min(query.limit || 20, 100); // Cap at 100 83 const sortBy = query.sortBy || CardSortField.UPDATED_AT; 84 const sortOrder = query.sortOrder || SortOrder.DESC; 85 const urlType = query.urlType; 86 87 // Validate collection ID 88 const collectionIdResult = CollectionId.createFromString( 89 query.collectionId, 90 ); 91 if (collectionIdResult.isErr()) { 92 return err(new ValidationError('Invalid collection ID')); 93 } 94 95 try { 96 // Get the collection 97 const collectionResult = await this.collectionRepo.findById( 98 collectionIdResult.value, 99 ); 100 101 if (collectionResult.isErr()) { 102 return err( 103 new Error( 104 `Failed to fetch collection: ${collectionResult.error instanceof Error ? collectionResult.error.message : 'Unknown error'}`, 105 ), 106 ); 107 } 108 109 const collection = collectionResult.value; 110 if (!collection) { 111 return err(new CollectionNotFoundError('Collection not found')); 112 } 113 114 const collectionPublishedRecordId = collection.publishedRecordId; 115 116 const collectionUri = collectionPublishedRecordId?.uri; 117 118 // Get author profile 119 const profileResult = await this.profileService.getProfile( 120 collection.authorId.value, 121 query.callingUserId, 122 ); 123 124 if (profileResult.isErr()) { 125 return err( 126 new Error( 127 `Failed to fetch author profile: ${profileResult.error instanceof Error ? profileResult.error.message : 'Unknown error'}`, 128 ), 129 ); 130 } 131 132 const authorProfile = profileResult.value; 133 134 // Get cards in the collection 135 const cardsResult = await this.cardQueryRepo.getCardsInCollection( 136 query.collectionId, 137 { 138 page, 139 limit, 140 sortBy, 141 sortOrder, 142 urlType, 143 }, 144 query.callingUserId, 145 ); 146 147 // Extract unique author IDs from all cards 148 const uniqueAuthorIds = Array.from( 149 new Set(cardsResult.items.map((card) => card.authorId)), 150 ); 151 152 // Fetch all author profiles using ProfileEnricher 153 const profileEnricher = new ProfileEnricher(this.profileService); 154 const authorProfileMapResult = await profileEnricher.buildProfileMap( 155 uniqueAuthorIds, 156 query.callingUserId, 157 { 158 skipFailures: true, // Skip cards with failed author profiles 159 mapToUser: false, // Use inline profile (without isFollowing) 160 }, 161 ); 162 163 if (authorProfileMapResult.isErr()) { 164 return err( 165 new Error( 166 `Failed to fetch card author profiles: ${authorProfileMapResult.error.message}`, 167 ), 168 ); 169 } 170 171 const authorProfileMap = authorProfileMapResult.value; 172 173 // Transform raw card data to enriched DTOs with author information 174 const enrichedCards: CollectionPageUrlCardDTO[] = cardsResult.items.map( 175 (card) => { 176 const author = authorProfileMap.get(card.authorId); 177 if (!author) { 178 throw new Error( 179 `Failed to fetch profile for card author: ${card.authorId}`, 180 ); 181 } 182 183 return { 184 ...card, 185 author, 186 }; 187 }, 188 ); 189 190 // Check if the calling user follows this collection 191 let isFollowing: boolean | undefined = undefined; 192 if (query.callingUserId) { 193 const followResult = 194 await this.followsRepository.findByFollowerAndTarget( 195 query.callingUserId, 196 collection.collectionId.getStringValue(), 197 FollowTargetType.COLLECTION, 198 ); 199 200 if (followResult.isOk()) { 201 isFollowing = followResult.value !== null; 202 } 203 } 204 205 return ok({ 206 id: collection.collectionId.getStringValue(), 207 uri: collectionUri, 208 name: collection.name.value, 209 description: collection.description?.value, 210 accessType: collection.accessType, 211 author: { 212 id: authorProfile.id, 213 name: authorProfile.name, 214 handle: authorProfile.handle, 215 avatarUrl: authorProfile.avatarUrl, 216 bannerUrl: authorProfile.bannerUrl, 217 }, 218 urlCards: enrichedCards, 219 cardCount: collection.cardCount, 220 createdAt: collection.createdAt.toISOString(), 221 updatedAt: collection.updatedAt.toISOString(), 222 isFollowing, 223 pagination: { 224 currentPage: page, 225 totalPages: Math.ceil(cardsResult.totalCount / limit), 226 totalCount: cardsResult.totalCount, 227 hasMore: page * limit < cardsResult.totalCount, 228 limit, 229 }, 230 sorting: { 231 sortBy, 232 sortOrder, 233 }, 234 }); 235 } catch (error) { 236 return err( 237 new Error( 238 `Failed to retrieve collection page: ${error instanceof Error ? error.message : 'Unknown error'}`, 239 ), 240 ); 241 } 242 } 243}