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