This repository has no description
1import {
2 ICardQueryRepository,
3 CardQueryOptions,
4 UrlCardQueryResultDTO,
5 CollectionCardQueryResultDTO,
6 UrlCardViewDTO,
7 PaginatedQueryResult,
8 CardSortField,
9 SortOrder,
10 LibraryForUrlDTO,
11 NoteCardForUrlRawDTO,
12} from '../../domain/ICardQueryRepository';
13import { CardTypeEnum } from '../../domain/value-objects/CardType';
14import { InMemoryCardRepository } from './InMemoryCardRepository';
15import { InMemoryCollectionRepository } from './InMemoryCollectionRepository';
16import { Card } from '../../domain/Card';
17import { CollectionId } from '../../domain/value-objects/CollectionId';
18import { CuratorId } from '../../domain/value-objects/CuratorId';
19
20export class InMemoryCardQueryRepository implements ICardQueryRepository {
21 constructor(
22 private cardRepository: InMemoryCardRepository,
23 private collectionRepository: InMemoryCollectionRepository,
24 ) {}
25
26 async getUrlCardsOfUser(
27 userId: string,
28 options: CardQueryOptions,
29 callingUserId?: string,
30 ): Promise<PaginatedQueryResult<UrlCardQueryResultDTO>> {
31 try {
32 // Get all cards and filter by user's library membership
33 const allCards = this.cardRepository.getAllCards();
34 const userCards = allCards
35 .filter(
36 (card) =>
37 card.isUrlCard &&
38 card.isInLibrary(CuratorId.create(userId).unwrap()),
39 )
40 .map((card) => this.cardToUrlCardQueryResult(card, callingUserId));
41
42 // Sort cards
43 const sortedCards = this.sortCards(
44 userCards,
45 options.sortBy,
46 options.sortOrder,
47 );
48
49 // Apply pagination
50 const startIndex = (options.page - 1) * options.limit;
51 const endIndex = startIndex + options.limit;
52 const paginatedCards = sortedCards.slice(startIndex, endIndex);
53
54 return {
55 items: paginatedCards,
56 totalCount: userCards.length,
57 hasMore: endIndex < userCards.length,
58 };
59 } catch (error) {
60 throw new Error(
61 `Failed to query URL cards: ${error instanceof Error ? error.message : String(error)}`,
62 );
63 }
64 }
65
66 private sortCards(
67 cards: UrlCardQueryResultDTO[],
68 sortBy: CardSortField,
69 sortOrder: SortOrder,
70 ): UrlCardQueryResultDTO[] {
71 const sorted = [...cards].sort((a, b) => {
72 let comparison = 0;
73
74 switch (sortBy) {
75 case CardSortField.CREATED_AT:
76 comparison = a.createdAt.getTime() - b.createdAt.getTime();
77 break;
78 case CardSortField.UPDATED_AT:
79 comparison = a.updatedAt.getTime() - b.updatedAt.getTime();
80 break;
81 case CardSortField.LIBRARY_COUNT:
82 comparison = a.libraryCount - b.libraryCount;
83 break;
84 default:
85 comparison = 0;
86 }
87
88 return sortOrder === SortOrder.DESC ? -comparison : comparison;
89 });
90
91 return sorted;
92 }
93
94 private cardToUrlCardQueryResult(
95 card: Card,
96 callingUserId?: string,
97 ): UrlCardQueryResultDTO {
98 if (!card.isUrlCard || !card.content.urlContent) {
99 throw new Error('Card is not a URL card');
100 }
101
102 // Find collections this card belongs to by querying the collection repository
103 const allCollections = this.collectionRepository.getAllCollections();
104 const collections: { id: string; name: string; authorId: string }[] = [];
105
106 for (const collection of allCollections) {
107 if (
108 collection.cardIds.some(
109 (cardId) => cardId.getStringValue() === card.cardId.getStringValue(),
110 )
111 ) {
112 collections.push({
113 id: collection.collectionId.getStringValue(),
114 name: collection.name.value,
115 authorId: collection.authorId.value,
116 });
117 }
118 }
119
120 // Find note cards with matching URL
121 const allCards = this.cardRepository.getAllCards();
122 const noteCard = allCards.find(
123 (c) => c.type.value === 'NOTE' && c.url?.value === card.url?.value,
124 );
125
126 const note = noteCard
127 ? {
128 id: noteCard.cardId.getStringValue(),
129 text: noteCard.content.noteContent?.text || '',
130 }
131 : undefined;
132
133 // Compute urlInLibrary if callingUserId is provided
134 const urlInLibrary = callingUserId
135 ? this.isUrlInUserLibrary(
136 card.content.urlContent.url.value,
137 callingUserId,
138 )
139 : undefined;
140
141 return {
142 id: card.cardId.getStringValue(),
143 type: CardTypeEnum.URL,
144 url: card.content.urlContent.url.value,
145 cardContent: {
146 url: card.content.urlContent.url.value,
147 title: card.content.urlContent.metadata?.title,
148 description: card.content.urlContent.metadata?.description,
149 author: card.content.urlContent.metadata?.author,
150 thumbnailUrl: card.content.urlContent.metadata?.imageUrl,
151 },
152 libraryCount: this.getLibraryCountForCard(card.cardId.getStringValue()),
153 urlLibraryCount: this.getUrlLibraryCount(
154 card.content.urlContent.url.value,
155 ),
156 urlInLibrary,
157 createdAt: card.createdAt,
158 updatedAt: card.updatedAt,
159 authorId: card.curatorId.value,
160 collections,
161 note,
162 };
163 }
164
165 private getLibraryCountForCard(cardId: string): number {
166 const card = this.cardRepository.getStoredCard({
167 getStringValue: () => cardId,
168 } as any);
169 return card ? card.libraryMembershipCount : 0;
170 }
171
172 private getUrlLibraryCount(url: string): number {
173 // Get all URL cards with this URL and count unique library memberships
174 const allCards = this.cardRepository.getAllCards();
175 const urlCards = allCards.filter(
176 (card) => card.isUrlCard && card.url?.value === url,
177 );
178
179 // Get all unique user IDs who have any card with this URL
180 const uniqueUserIds = new Set<string>();
181 for (const card of urlCards) {
182 for (const membership of card.libraryMemberships) {
183 uniqueUserIds.add(membership.curatorId.value);
184 }
185 }
186
187 return uniqueUserIds.size;
188 }
189
190 private isUrlInUserLibrary(url: string, userId: string): boolean {
191 // Check if the user has any URL card with this URL (by checking authorId)
192 const allCards = this.cardRepository.getAllCards();
193 return allCards.some(
194 (card) =>
195 card.isUrlCard &&
196 card.url?.value === url &&
197 card.curatorId.value === userId,
198 );
199 }
200
201 async getCardsInCollection(
202 collectionId: string,
203 options: CardQueryOptions,
204 callingUserId?: string,
205 ): Promise<PaginatedQueryResult<CollectionCardQueryResultDTO>> {
206 try {
207 // Get the collection from the repository
208 const collectionIdObj = CollectionId.createFromString(collectionId);
209 if (collectionIdObj.isErr()) {
210 throw new Error(`Invalid collection ID: ${collectionId}`);
211 }
212
213 const collectionResult = await this.collectionRepository.findById(
214 collectionIdObj.value,
215 );
216 if (collectionResult.isErr()) {
217 throw collectionResult.error;
218 }
219
220 const collection = collectionResult.value;
221 if (!collection) {
222 return {
223 items: [],
224 totalCount: 0,
225 hasMore: false,
226 };
227 }
228
229 // Get cards that are in this collection
230 const allCards = this.cardRepository.getAllCards();
231 const collectionCardIds = new Set(
232 collection.cardIds.map((id) => id.getStringValue()),
233 );
234 const collectionCards = allCards
235 .filter(
236 (card) =>
237 collectionCardIds.has(card.cardId.getStringValue()) &&
238 card.isUrlCard,
239 )
240 .map((card) =>
241 this.toCollectionCardQueryResult(
242 this.cardToUrlCardQueryResult(card, callingUserId),
243 ),
244 );
245
246 // Sort cards
247 const sortedCards = this.sortCollectionCards(
248 collectionCards,
249 options.sortBy,
250 options.sortOrder,
251 );
252
253 // Apply pagination
254 const startIndex = (options.page - 1) * options.limit;
255 const endIndex = startIndex + options.limit;
256 const paginatedCards = sortedCards.slice(startIndex, endIndex);
257
258 return {
259 items: paginatedCards,
260 totalCount: collectionCards.length,
261 hasMore: endIndex < collectionCards.length,
262 };
263 } catch (error) {
264 throw new Error(
265 `Failed to query collection cards: ${error instanceof Error ? error.message : String(error)}`,
266 );
267 }
268 }
269
270 private sortCollectionCards(
271 cards: CollectionCardQueryResultDTO[],
272 sortBy: CardSortField,
273 sortOrder: SortOrder,
274 ): CollectionCardQueryResultDTO[] {
275 const sorted = [...cards].sort((a, b) => {
276 let comparison = 0;
277
278 switch (sortBy) {
279 case CardSortField.CREATED_AT:
280 comparison = a.createdAt.getTime() - b.createdAt.getTime();
281 break;
282 case CardSortField.UPDATED_AT:
283 comparison = a.updatedAt.getTime() - b.updatedAt.getTime();
284 break;
285 case CardSortField.LIBRARY_COUNT:
286 comparison = a.libraryCount - b.libraryCount;
287 break;
288 default:
289 comparison = 0;
290 }
291
292 return sortOrder === SortOrder.DESC ? -comparison : comparison;
293 });
294
295 return sorted;
296 }
297
298 private toCollectionCardQueryResult(
299 card: UrlCardQueryResultDTO,
300 ): CollectionCardQueryResultDTO {
301 return {
302 id: card.id,
303 type: CardTypeEnum.URL,
304 url: card.url,
305 cardContent: card.cardContent,
306 libraryCount: card.libraryCount,
307 urlLibraryCount: card.urlLibraryCount,
308 urlInLibrary: card.urlInLibrary,
309 createdAt: card.createdAt,
310 updatedAt: card.updatedAt,
311 authorId: card.authorId,
312 note: card.note,
313 };
314 }
315
316 async getUrlCardView(
317 cardId: string,
318 callingUserId?: string,
319 ): Promise<UrlCardViewDTO | null> {
320 const allCards = this.cardRepository.getAllCards();
321 const card = allCards.find((c) => c.cardId.getStringValue() === cardId);
322 if (!card || !card.isUrlCard) {
323 return null;
324 }
325
326 const urlCardResult = this.cardToUrlCardQueryResult(card, callingUserId);
327
328 // Get library memberships from the card itself
329 const libraries = card.libraryMemberships.map((membership) => ({
330 userId: membership.curatorId.value,
331 }));
332
333 // Find note cards with matching URL
334 const noteCard = allCards.find(
335 (c) => c.type.value === 'NOTE' && c.url?.value === card.url?.value,
336 );
337
338 const note = noteCard
339 ? {
340 id: noteCard.cardId.getStringValue(),
341 text: noteCard.content.noteContent?.text || '',
342 }
343 : undefined;
344
345 return {
346 ...urlCardResult,
347 libraries,
348 note,
349 };
350 }
351
352 async getLibrariesForCard(cardId: string): Promise<string[]> {
353 const allCards = this.cardRepository.getAllCards();
354 const card = allCards.find((c) => c.cardId.getStringValue() === cardId);
355
356 if (!card) {
357 return [];
358 }
359
360 return card.libraryMemberships.map(
361 (membership) => membership.curatorId.value,
362 );
363 }
364
365 async getLibrariesForUrl(
366 url: string,
367 options: CardQueryOptions,
368 ): Promise<PaginatedQueryResult<LibraryForUrlDTO>> {
369 try {
370 // Get all cards and filter by URL
371 const allCards = this.cardRepository.getAllCards();
372 const urlCards = allCards.filter(
373 (card) => card.isUrlCard && card.url?.value === url,
374 );
375
376 // Create library entries for each card
377 const libraries: LibraryForUrlDTO[] = [];
378 for (const card of urlCards) {
379 // Skip cards without urlContent (should not happen since we filtered for URL cards)
380 if (!card.content.urlContent) {
381 continue;
382 }
383
384 for (const membership of card.libraryMemberships) {
385 const noteCard = allCards.find(
386 (c) => c.isNoteCard && c.parentCardId?.equals(card.cardId),
387 );
388
389 const urlLibraryCount = this.getUrlLibraryCount(url);
390
391 libraries.push({
392 userId: membership.curatorId.value,
393 card: {
394 id: card.cardId.getStringValue(),
395 url: card.content.urlContent.url.value,
396 cardContent: {
397 url: card.content.urlContent.url.value,
398 title: card.content.urlContent.metadata?.title,
399 description: card.content.urlContent.metadata?.description,
400 author: card.content.urlContent.metadata?.author,
401 thumbnailUrl: card.content.urlContent.metadata?.imageUrl,
402 },
403 libraryCount: this.getLibraryCountForCard(
404 card.cardId.getStringValue(),
405 ),
406 urlLibraryCount,
407 urlInLibrary: true,
408 createdAt: card.createdAt,
409 updatedAt: card.updatedAt,
410 note: noteCard
411 ? {
412 id: noteCard.cardId.getStringValue(),
413 text: noteCard.content.noteContent?.text || '',
414 }
415 : undefined,
416 },
417 });
418 }
419 }
420
421 // Sort libraries (by userId for consistency)
422 const sortedLibraries = this.sortLibraries(
423 libraries,
424 options.sortBy,
425 options.sortOrder,
426 );
427
428 // Apply pagination
429 const startIndex = (options.page - 1) * options.limit;
430 const endIndex = startIndex + options.limit;
431 const paginatedLibraries = sortedLibraries.slice(startIndex, endIndex);
432
433 return {
434 items: paginatedLibraries,
435 totalCount: libraries.length,
436 hasMore: endIndex < libraries.length,
437 };
438 } catch (error) {
439 throw new Error(
440 `Failed to query libraries for URL: ${error instanceof Error ? error.message : String(error)}`,
441 );
442 }
443 }
444
445 private sortLibraries(
446 libraries: LibraryForUrlDTO[],
447 sortBy: CardSortField,
448 sortOrder: SortOrder,
449 ): LibraryForUrlDTO[] {
450 const sorted = [...libraries].sort((a, b) => {
451 let comparison = 0;
452
453 switch (sortBy) {
454 case CardSortField.CREATED_AT:
455 case CardSortField.UPDATED_AT:
456 case CardSortField.LIBRARY_COUNT:
457 // For libraries, we'll sort by userId as a fallback
458 comparison = a.userId.localeCompare(b.userId);
459 break;
460 default:
461 comparison = a.userId.localeCompare(b.userId);
462 }
463
464 return sortOrder === SortOrder.DESC ? -comparison : comparison;
465 });
466
467 return sorted;
468 }
469
470 async getNoteCardsForUrl(
471 url: string,
472 options: CardQueryOptions,
473 ): Promise<PaginatedQueryResult<NoteCardForUrlRawDTO>> {
474 try {
475 // Get all note cards with the specified URL
476 const allCards = this.cardRepository.getAllCards();
477 const noteCards = allCards
478 .filter((card) => card.isNoteCard && card.url?.value === url)
479 .map((card) => ({
480 id: card.cardId.getStringValue(),
481 note: card.content.noteContent?.text || '',
482 authorId: card.curatorId.value,
483 createdAt: card.createdAt,
484 updatedAt: card.updatedAt,
485 }));
486
487 // Sort note cards
488 const sortedNotes = this.sortNoteCards(
489 noteCards,
490 options.sortBy,
491 options.sortOrder,
492 );
493
494 // Apply pagination
495 const startIndex = (options.page - 1) * options.limit;
496 const endIndex = startIndex + options.limit;
497 const paginatedNotes = sortedNotes.slice(startIndex, endIndex);
498
499 return {
500 items: paginatedNotes,
501 totalCount: noteCards.length,
502 hasMore: endIndex < noteCards.length,
503 };
504 } catch (error) {
505 throw new Error(
506 `Failed to query note cards for URL: ${error instanceof Error ? error.message : String(error)}`,
507 );
508 }
509 }
510
511 private sortNoteCards(
512 notes: NoteCardForUrlRawDTO[],
513 sortBy: CardSortField,
514 sortOrder: SortOrder,
515 ): NoteCardForUrlRawDTO[] {
516 const sorted = [...notes].sort((a, b) => {
517 let comparison = 0;
518
519 switch (sortBy) {
520 case CardSortField.CREATED_AT:
521 comparison = a.createdAt.getTime() - b.createdAt.getTime();
522 break;
523 case CardSortField.UPDATED_AT:
524 comparison = a.updatedAt.getTime() - b.updatedAt.getTime();
525 break;
526 case CardSortField.LIBRARY_COUNT:
527 // For note cards, sort by authorId as fallback
528 comparison = a.authorId.localeCompare(b.authorId);
529 break;
530 default:
531 comparison = 0;
532 }
533
534 return sortOrder === SortOrder.DESC ? -comparison : comparison;
535 });
536
537 return sorted;
538 }
539
540 clear(): void {
541 // No separate state to clear
542 }
543}