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 628 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: { 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}