This repository has no description
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}