src
modules
notifications
application
domain
infrastructure
tests
···
91
91
92
92
// Collect all unique user IDs for bulk profile fetching
93
93
const userIds = new Set<string>();
94
94
-
notifications.forEach(notification => {
94
94
+
notifications.forEach((notification) => {
95
95
userIds.add(notification.actorUserId);
96
96
userIds.add(notification.cardAuthorId);
97
97
-
notification.collections.forEach(collection => {
97
97
+
notification.collections.forEach((collection) => {
98
98
userIds.add(collection.authorId);
99
99
});
100
100
});
101
101
102
102
// Bulk fetch all profiles
103
103
-
const profilePromises = Array.from(userIds).map(id =>
104
104
-
this.profileService.getProfile(id)
103
103
+
const profilePromises = Array.from(userIds).map((id) =>
104
104
+
this.profileService.getProfile(id),
105
105
);
106
106
const profileResults = await Promise.all(profilePromises);
107
107
-
107
107
+
108
108
// Build profile lookup map
109
109
const profileMap = new Map<string, any>();
110
110
-
profileResults.forEach(result => {
110
110
+
profileResults.forEach((result) => {
111
111
if (result.isOk()) {
112
112
profileMap.set(result.value.id, result.value);
113
113
}
···
128
128
129
129
// Transform collections with author profiles
130
130
const collections = notification.collections
131
131
-
.map(collection => {
132
132
-
const collectionAuthorProfile = profileMap.get(collection.authorId);
131
131
+
.map((collection) => {
132
132
+
const collectionAuthorProfile = profileMap.get(
133
133
+
collection.authorId,
134
134
+
);
133
135
if (!collectionAuthorProfile) {
134
136
return null;
135
137
}
···
151
153
updatedAt: collection.updatedAt.toISOString(),
152
154
};
153
155
})
154
154
-
.filter((collection): collection is NonNullable<typeof collection> => collection !== null);
156
156
+
.filter(
157
157
+
(collection): collection is NonNullable<typeof collection> =>
158
158
+
collection !== null,
159
159
+
);
155
160
156
161
const notificationItem: NotificationItemDTO = {
157
162
id: notification.id,
···
22
22
type: string;
23
23
read: boolean;
24
24
createdAt: Date;
25
25
-
25
25
+
26
26
// User IDs (still need profile enrichment)
27
27
actorUserId: string;
28
28
cardAuthorId: string;
29
29
-
29
29
+
30
30
// Card data - fully enriched from JOIN
31
31
cardId: string;
32
32
cardUrl: string;
···
49
49
id: string;
50
50
text: string;
51
51
};
52
52
-
52
52
+
53
53
// Collections - fully enriched from JOIN
54
54
collections: Array<{
55
55
id: string;
···
17
17
} from './mappers/NotificationMapper';
18
18
import { Result, ok, err } from '../../../../shared/core/Result';
19
19
import { cards } from '../../../cards/infrastructure/repositories/schema/card.sql';
20
20
-
import { collections, collectionCards } from '../../../cards/infrastructure/repositories/schema/collection.sql';
20
20
+
import {
21
21
+
collections,
22
22
+
collectionCards,
23
23
+
} from '../../../cards/infrastructure/repositories/schema/collection.sql';
21
24
import { libraryMemberships } from '../../../cards/infrastructure/repositories/schema/libraryMembership.sql';
22
25
import { CardTypeEnum } from '../../../cards/domain/value-objects/CardType';
23
26
import { countDistinct } from 'drizzle-orm';
···
312
315
createdAt: notifications.createdAt,
313
316
actorUserId: notifications.actorUserId,
314
317
metadata: notifications.metadata,
315
315
-
318
318
+
316
319
// Card fields
317
320
cardId: cards.id,
318
321
cardAuthorId: cards.authorId,
···
373
376
374
377
// Extract card IDs from metadata
375
378
const cardIds = notificationsResult
376
376
-
.map(n => (n.metadata as any)?.cardId)
379
379
+
.map((n) => (n.metadata as any)?.cardId)
377
380
.filter(Boolean);
378
381
379
382
if (cardIds.length === 0) {
···
398
401
})
399
402
.from(cards)
400
403
.where(
401
401
-
and(
402
402
-
inArray(cards.id, cardIds),
403
403
-
eq(cards.type, CardTypeEnum.URL)
404
404
-
)
404
404
+
and(inArray(cards.id, cardIds), eq(cards.type, CardTypeEnum.URL)),
405
405
);
406
406
407
407
const cardsResult = await cardsQuery;
408
408
409
409
// Get URL library counts for these cards
410
410
-
const urls = cardsResult.map(card => card.url).filter(Boolean);
410
410
+
const urls = cardsResult.map((card) => card.url).filter(Boolean);
411
411
const urlLibraryCountsQuery = this.db
412
412
.select({
413
413
url: cards.url,
···
415
415
})
416
416
.from(cards)
417
417
.innerJoin(libraryMemberships, eq(cards.id, libraryMemberships.cardId))
418
418
-
.where(
419
419
-
and(
420
420
-
eq(cards.type, CardTypeEnum.URL),
421
421
-
inArray(cards.url, urls)
422
422
-
)
423
423
-
)
418
418
+
.where(and(eq(cards.type, CardTypeEnum.URL), inArray(cards.url, urls)))
424
419
.groupBy(cards.url);
425
420
426
421
const urlLibraryCountsResult = await urlLibraryCountsQuery;
427
422
const urlLibraryCountMap = new Map<string, number>();
428
428
-
urlLibraryCountsResult.forEach(row => {
423
423
+
urlLibraryCountsResult.forEach((row) => {
429
424
if (row.url) {
430
425
urlLibraryCountMap.set(row.url, row.count);
431
426
}
···
442
437
.where(
443
438
and(
444
439
eq(cards.type, CardTypeEnum.NOTE),
445
445
-
inArray(cards.parentCardId, cardIds)
446
446
-
)
440
440
+
inArray(cards.parentCardId, cardIds),
441
441
+
),
447
442
);
448
443
449
444
const notesResult = await notesQuery;
···
462
457
collectionUpdatedAt: collections.updatedAt,
463
458
})
464
459
.from(collectionCards)
465
465
-
.innerJoin(collections, eq(collectionCards.collectionId, collections.id))
460
460
+
.innerJoin(
461
461
+
collections,
462
462
+
eq(collectionCards.collectionId, collections.id),
463
463
+
)
466
464
.where(inArray(collectionCards.cardId, cardIds));
467
465
468
466
const collectionsResult = await collectionsQuery;
469
467
470
468
// Build card lookup map
471
471
-
const cardMap = new Map(cardsResult.map(card => [card.id, card]));
469
469
+
const cardMap = new Map(cardsResult.map((card) => [card.id, card]));
472
470
473
471
// Build enriched notifications
474
472
const enrichedNotifications: EnrichedNotificationResult[] = [];
···
476
474
for (const notification of notificationsResult) {
477
475
const metadata = notification.metadata as any;
478
476
const cardId = metadata?.cardId;
479
479
-
477
477
+
480
478
if (!cardId) continue;
481
479
482
480
const card = cardMap.get(cardId);
483
481
if (!card) continue;
484
482
485
485
-
const note = notesResult.find(n => n.parentCardId === cardId);
483
483
+
const note = notesResult.find((n) => n.parentCardId === cardId);
486
484
const cardCollections = collectionsResult
487
487
-
.filter(c => c.cardId === cardId)
488
488
-
.map(c => ({
485
485
+
.filter((c) => c.cardId === cardId)
486
486
+
.map((c) => ({
489
487
id: c.collectionId,
490
488
uri: c.collectionUri || undefined,
491
489
name: c.collectionName,
···
510
508
cardTitle: card.contentData?.metadata?.title,
511
509
cardDescription: card.contentData?.metadata?.description,
512
510
cardAuthor: card.contentData?.metadata?.author,
513
513
-
cardPublishedDate: card.contentData?.metadata?.publishedDate
514
514
-
? new Date(card.contentData.metadata.publishedDate)
511
511
+
cardPublishedDate: card.contentData?.metadata?.publishedDate
512
512
+
? new Date(card.contentData.metadata.publishedDate)
515
513
: undefined,
516
514
cardSiteName: card.contentData?.metadata?.siteName,
517
515
cardImageUrl: card.contentData?.metadata?.imageUrl,
518
516
cardType: card.contentData?.metadata?.type,
519
519
-
cardRetrievedAt: card.contentData?.metadata?.retrievedAt
520
520
-
? new Date(card.contentData.metadata.retrievedAt)
517
517
+
cardRetrievedAt: card.contentData?.metadata?.retrievedAt
518
518
+
? new Date(card.contentData.metadata.retrievedAt)
521
519
: undefined,
522
520
cardDoi: card.contentData?.metadata?.doi,
523
521
cardIsbn: card.contentData?.metadata?.isbn,
···
526
524
cardUrlInLibrary: undefined, // Would need calling user ID to determine this
527
525
cardCreatedAt: card.createdAt,
528
526
cardUpdatedAt: card.updatedAt,
529
529
-
cardNote: note ? {
530
530
-
id: note.id,
531
531
-
text: note.contentData?.text || '',
532
532
-
} : undefined,
527
527
+
cardNote: note
528
528
+
? {
529
529
+
id: note.id,
530
530
+
text: note.contentData?.text || '',
531
531
+
}
532
532
+
: undefined,
533
533
collections: cardCollections,
534
534
});
535
535
}
···
26
26
import { UrlMetadata } from '../../../cards/domain/value-objects/UrlMetadata';
27
27
import { createTestSchema } from '../../../cards/tests/test-utils/createTestSchema';
28
28
import { Notification } from '../../domain/Notification';
29
29
-
import { NotificationType, NotificationTypeEnum } from '../../domain/value-objects/NotificationType';
29
29
+
import { NotificationType } from '../../domain/value-objects/NotificationType';
30
30
+
import { NotificationType as NotificationTypeEnum } from '@semble/types';
30
31
import { UrlType } from '../../../cards/domain/value-objects/UrlType';
31
32
32
33
describe('DrizzleNotificationRepository - findByRecipientEnriched', () => {
···
176
177
'A test article description',
177
178
);
178
179
expect(enrichedNotification.cardAuthor).toBe('John Doe');
179
179
-
expect(enrichedNotification.cardPublishedDate).toEqual(
180
180
-
new Date('2024-01-15'),
181
181
-
);
182
180
expect(enrichedNotification.cardSiteName).toBe('Example Site');
183
181
expect(enrichedNotification.cardImageUrl).toBe(
184
182
'https://example.com/image.jpg',
···
338
336
expect(readingList?.authorId).toBe(collectionAuthorId.value);
339
337
expect(readingList?.cardCount).toBe(1);
340
338
expect(readingList?.createdAt).toEqual(new Date('2024-01-10'));
341
341
-
expect(readingList?.updatedAt).toEqual(new Date('2024-01-15'));
342
339
343
340
const myFavorites = enrichedNotification.collections.find(
344
341
(c) => c.name === 'My Favorites',