This repository has no description
semble
/
src
/
modules
/
notifications
/
application
/
useCases
/
queries
/
GetMyNotificationsUseCase.ts
7.9 kB
232 lines
1import { Result, ok, err } from '../../../../../shared/core/Result';
2import { UseCase } from '../../../../../shared/core/UseCase';
3import { UseCaseError } from '../../../../../shared/core/UseCaseError';
4import { AppError } from '../../../../../shared/core/AppError';
5import { INotificationRepository } from '../../../domain/INotificationRepository';
6import { CuratorId } from '../../../../cards/domain/value-objects/CuratorId';
7import { IProfileService } from '../../../../cards/domain/services/IProfileService';
8import { ICardQueryRepository } from '../../../../cards/domain/ICardQueryRepository';
9import { ICollectionRepository } from '../../../../cards/domain/ICollectionRepository';
10import { CollectionId } from '../../../../cards/domain/value-objects/CollectionId';
11import { NotificationItem } from '@semble/types';
12
13export interface GetMyNotificationsDTO {
14 userId: string;
15 page?: number;
16 limit?: number;
17 unreadOnly?: boolean;
18}
19
20type NotificationItemDTO = NotificationItem;
21
22export interface GetMyNotificationsResponseDTO {
23 notifications: NotificationItemDTO[];
24 pagination: {
25 currentPage: number;
26 totalPages: number;
27 totalCount: number;
28 hasMore: boolean;
29 limit: number;
30 };
31 unreadCount: number;
32}
33
34export class ValidationError extends UseCaseError {
35 constructor(message: string) {
36 super(message);
37 }
38}
39
40export class GetMyNotificationsUseCase
41 implements
42 UseCase<
43 GetMyNotificationsDTO,
44 Result<
45 GetMyNotificationsResponseDTO,
46 ValidationError | AppError.UnexpectedError
47 >
48 >
49{
50 constructor(
51 private notificationRepository: INotificationRepository,
52 private profileService: IProfileService,
53 private cardQueryRepository: ICardQueryRepository,
54 private collectionRepository: ICollectionRepository,
55 ) {}
56
57 async execute(
58 request: GetMyNotificationsDTO,
59 ): Promise<
60 Result<
61 GetMyNotificationsResponseDTO,
62 ValidationError | AppError.UnexpectedError
63 >
64 > {
65 try {
66 const userIdResult = CuratorId.create(request.userId);
67 if (userIdResult.isErr()) {
68 return err(
69 new ValidationError(`Invalid user ID: ${userIdResult.error.message}`),
70 );
71 }
72
73 const page = request.page || 1;
74 const limit = request.limit || 20;
75
76 // Use enriched query to get notifications with card and collection data
77 const result = await this.notificationRepository.findByRecipientEnriched(
78 userIdResult.value,
79 {
80 page,
81 limit,
82 unreadOnly: request.unreadOnly,
83 },
84 );
85
86 if (result.isErr()) {
87 return err(new ValidationError(result.error.message));
88 }
89
90 const { notifications, totalCount, hasMore, unreadCount } = result.value;
91
92 // Collect all unique user IDs for bulk profile fetching
93 const userIds = new Set<string>();
94 notifications.forEach((notification) => {
95 userIds.add(notification.actorUserId);
96 userIds.add(notification.cardAuthorId);
97 notification.collections.forEach((collection) => {
98 userIds.add(collection.authorId);
99 });
100 });
101
102 // Bulk fetch all profiles
103 const profilePromises = Array.from(userIds).map((id) =>
104 this.profileService.getProfile(id),
105 );
106 const profileResults = await Promise.all(profilePromises);
107
108 // Build profile lookup map
109 const profileMap = new Map<string, any>();
110 profileResults.forEach((result) => {
111 if (result.isOk()) {
112 profileMap.set(result.value.id, result.value);
113 }
114 });
115
116 // Transform enriched notifications to DTOs
117 const notificationItems: NotificationItemDTO[] = [];
118
119 for (const notification of notifications) {
120 try {
121 const actorProfile = profileMap.get(notification.actorUserId);
122 const cardAuthorProfile = profileMap.get(notification.cardAuthorId);
123
124 if (!actorProfile || !cardAuthorProfile) {
125 // Skip notifications with missing profiles
126 continue;
127 }
128
129 // Transform collections with author profiles
130 const collections = notification.collections
131 .map((collection) => {
132 const collectionAuthorProfile = profileMap.get(
133 collection.authorId,
134 );
135 if (!collectionAuthorProfile) {
136 return null;
137 }
138
139 return {
140 id: collection.id,
141 uri: collection.uri,
142 name: collection.name,
143 author: {
144 id: collectionAuthorProfile.id,
145 name: collectionAuthorProfile.name,
146 handle: collectionAuthorProfile.handle,
147 avatarUrl: collectionAuthorProfile.avatarUrl,
148 description: collectionAuthorProfile.bio,
149 },
150 description: collection.description,
151 cardCount: collection.cardCount,
152 createdAt: collection.createdAt.toISOString(),
153 updatedAt: collection.updatedAt.toISOString(),
154 };
155 })
156 .filter(
157 (collection): collection is NonNullable<typeof collection> =>
158 collection !== null,
159 );
160
161 const notificationItem: NotificationItemDTO = {
162 id: notification.id,
163 user: {
164 id: actorProfile.id,
165 name: actorProfile.name,
166 handle: actorProfile.handle,
167 avatarUrl: actorProfile.avatarUrl,
168 description: actorProfile.bio,
169 },
170 card: {
171 id: notification.cardId,
172 type: 'URL' as const,
173 url: notification.cardUrl,
174 cardContent: {
175 url: notification.cardUrl,
176 title: notification.cardTitle,
177 description: notification.cardDescription,
178 author: notification.cardAuthor,
179 publishedDate: notification.cardPublishedDate?.toISOString(),
180 siteName: notification.cardSiteName,
181 imageUrl: notification.cardImageUrl,
182 type: notification.cardType,
183 retrievedAt: notification.cardRetrievedAt?.toISOString(),
184 doi: notification.cardDoi,
185 isbn: notification.cardIsbn,
186 },
187 libraryCount: notification.cardLibraryCount,
188 urlLibraryCount: notification.cardUrlLibraryCount,
189 urlInLibrary: notification.cardUrlInLibrary,
190 createdAt: notification.cardCreatedAt.toISOString(),
191 updatedAt: notification.cardUpdatedAt.toISOString(),
192 author: {
193 id: cardAuthorProfile.id,
194 name: cardAuthorProfile.name,
195 handle: cardAuthorProfile.handle,
196 avatarUrl: cardAuthorProfile.avatarUrl,
197 description: cardAuthorProfile.bio,
198 },
199 note: notification.cardNote,
200 },
201 createdAt: notification.createdAt.toISOString(),
202 collections,
203 type: notification.type as any, // Cast to NotificationType enum
204 read: notification.read,
205 };
206
207 notificationItems.push(notificationItem);
208 } catch (error) {
209 // Skip this notification if there's an error processing it
210 console.error('Error processing notification:', error);
211 continue;
212 }
213 }
214
215 const totalPages = Math.ceil(totalCount / limit);
216
217 return ok({
218 notifications: notificationItems,
219 pagination: {
220 currentPage: page,
221 totalPages,
222 totalCount,
223 hasMore,
224 limit,
225 },
226 unreadCount,
227 });
228 } catch (error) {
229 return err(AppError.UnexpectedError.create(error));
230 }
231 }
232}