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