This repository has no description
1import { Result, ok, err } from '../../../../shared/core/Result';
2import { IProfileService } from '../../domain/services/IProfileService';
3import { User } from '@semble/types';
4import { BatchProfileFetcher } from './BatchProfileFetcher';
5import { ProfileMapper } from '../mappers/ProfileMapper';
6
7/**
8 * ProfileEnricher - High-level service for enriching items with author profiles
9 *
10 * Provides a simple API to replace the extract → fetch → map pattern
11 * used across all query use cases
12 */
13export class ProfileEnricher {
14 private batchFetcher: BatchProfileFetcher;
15
16 constructor(private profileService: IProfileService) {
17 this.batchFetcher = new BatchProfileFetcher(profileService);
18 }
19
20 /**
21 * Enrich a collection of items with author profiles
22 *
23 * @param items - Array of items to enrich
24 * @param getAuthorId - Function to extract author ID from each item
25 * @param callingUserId - Optional calling user ID for isFollowing status
26 * @param options - Fetch options (skipFailures, includeFallback)
27 * @returns Result with items enriched with 'author' field
28 *
29 * @example
30 * const enriched = await enricher.enrichWithAuthors(
31 * cards,
32 * (card) => card.authorId,
33 * callingUserId
34 * );
35 */
36 async enrichWithAuthors<T extends Record<string, any>>(
37 items: T[],
38 getAuthorId: (item: T) => string,
39 callingUserId?: string,
40 options?: {
41 skipFailures?: boolean;
42 includeFallback?: boolean;
43 mapToUser?: boolean; // If true, map to User (with isFollowing), else use inline profile
44 },
45 ): Promise<Result<(T & { author: User })[]>> {
46 // Extract unique author IDs
47 const authorIds = Array.from(new Set(items.map(getAuthorId)));
48
49 // Fetch all profiles
50 const profileMapResult = await this.batchFetcher.fetchProfileMap(
51 authorIds,
52 callingUserId,
53 {
54 skipFailures: options?.skipFailures ?? false,
55 includeFallback: options?.includeFallback ?? false,
56 },
57 );
58
59 if (profileMapResult.isErr()) {
60 return err(profileMapResult.error);
61 }
62
63 const profileMap = profileMapResult.value;
64 const mapToUser = options?.mapToUser ?? true;
65
66 // Enrich items with author profiles
67 const enrichedItems: (T & { author: User })[] = [];
68
69 for (const item of items) {
70 const authorId = getAuthorId(item);
71 const profile = profileMap.get(authorId);
72
73 if (!profile) {
74 if (options?.skipFailures) {
75 // Skip this item if we're allowing failures
76 continue;
77 }
78 return err(new Error(`Profile not found for author ${authorId}`));
79 }
80
81 const author = mapToUser
82 ? ProfileMapper.toUser(profile)
83 : (ProfileMapper.toInlineProfile(profile) as User);
84
85 enrichedItems.push({
86 ...item,
87 author,
88 });
89 }
90
91 return ok(enrichedItems);
92 }
93
94 /**
95 * Build a profile map for later enrichment
96 * Useful when you need to enrich multiple different collections with the same profiles
97 *
98 * @param userIds - Array of user IDs to fetch
99 * @param callingUserId - Optional calling user ID
100 * @param options - Fetch options
101 * @returns Map of userId -> User (API DTO)
102 */
103 async buildProfileMap(
104 userIds: string[],
105 callingUserId?: string,
106 options?: {
107 skipFailures?: boolean;
108 includeFallback?: boolean;
109 mapToUser?: boolean;
110 },
111 ): Promise<Result<Map<string, User>>> {
112 const profileMapResult = await this.batchFetcher.fetchProfileMap(
113 userIds,
114 callingUserId,
115 {
116 skipFailures: options?.skipFailures ?? false,
117 includeFallback: options?.includeFallback ?? false,
118 },
119 );
120
121 if (profileMapResult.isErr()) {
122 return err(profileMapResult.error);
123 }
124
125 const profileMap = profileMapResult.value;
126 const mapToUser = options?.mapToUser ?? true;
127 const userMap = new Map<string, User>();
128
129 for (const [userId, profile] of profileMap.entries()) {
130 const user = mapToUser
131 ? ProfileMapper.toUser(profile)
132 : (ProfileMapper.toInlineProfile(profile) as User);
133 userMap.set(userId, user);
134 }
135
136 return ok(userMap);
137 }
138}