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
19 kB 615 lines
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 thumbnailUrl: 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 thumbnailUrl: 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 thumbnailUrl: 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}