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