This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / cards / tests / utils / InMemoryCardQueryRepository.ts
16 kB 543 lines
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}