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 / infrastructure / DrizzleCardQueryRepository.getCardsInCollection.integration.test.ts
40 kB 1189 lines
1import { 2 PostgreSqlContainer, 3 StartedPostgreSqlContainer, 4} from '@testcontainers/postgresql'; 5import postgres from 'postgres'; 6import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; 7import { DrizzleCardQueryRepository } from '../../infrastructure/repositories/DrizzleCardQueryRepository'; 8import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository'; 9import { DrizzleCollectionRepository } from '../../infrastructure/repositories/DrizzleCollectionRepository'; 10import { CuratorId } from '../../domain/value-objects/CuratorId'; 11import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; 12import { cards } from '../../infrastructure/repositories/schema/card.sql'; 13import { 14 collections, 15 collectionCards, 16} from '../../infrastructure/repositories/schema/collection.sql'; 17import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql'; 18import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql'; 19import { Collection, CollectionAccessType } from '../../domain/Collection'; 20import { CardBuilder } from '../utils/builders/CardBuilder'; 21import { URL } from '../../domain/value-objects/URL'; 22import { UrlMetadata } from '../../domain/value-objects/UrlMetadata'; 23import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 24import { createTestSchema } from '../test-utils/createTestSchema'; 25import { CardTypeEnum } from '../../domain/value-objects/CardType'; 26import { UrlType } from '../../domain/value-objects/UrlType'; 27 28describe('DrizzleCardQueryRepository - getCardsInCollection', () => { 29 let container: StartedPostgreSqlContainer; 30 let db: PostgresJsDatabase; 31 let queryRepository: DrizzleCardQueryRepository; 32 let cardRepository: DrizzleCardRepository; 33 let collectionRepository: DrizzleCollectionRepository; 34 35 // Test data 36 let curatorId: CuratorId; 37 let otherCuratorId: CuratorId; 38 let thirdCuratorId: CuratorId; 39 40 // Setup before all tests 41 beforeAll(async () => { 42 // Start PostgreSQL container 43 container = await new PostgreSqlContainer('postgres:14').start(); 44 45 // Create database connection 46 const connectionString = container.getConnectionUri(); 47 process.env.DATABASE_URL = connectionString; 48 const client = postgres(connectionString); 49 db = drizzle(client); 50 51 // Create repositories 52 queryRepository = new DrizzleCardQueryRepository(db); 53 cardRepository = new DrizzleCardRepository(db); 54 collectionRepository = new DrizzleCollectionRepository(db); 55 56 // Create schema using helper function 57 await createTestSchema(db); 58 59 // Create test data 60 curatorId = CuratorId.create('did:plc:testcurator').unwrap(); 61 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); 62 thirdCuratorId = CuratorId.create('did:plc:thirdcurator').unwrap(); 63 }, 60000); // Increase timeout for container startup 64 65 // Cleanup after all tests 66 afterAll(async () => { 67 // Stop container 68 await container.stop(); 69 }); 70 71 // Clear data between tests 72 beforeEach(async () => { 73 await db.delete(collectionCards); 74 await db.delete(collections); 75 await db.delete(libraryMemberships); 76 await db.delete(cards); 77 await db.delete(publishedRecords); 78 }); 79 80 describe('getCardsInCollection', () => { 81 it('should return empty result when collection has no URL cards', async () => { 82 // Create empty collection 83 const collection = Collection.create( 84 { 85 authorId: curatorId, 86 name: 'Empty Collection', 87 accessType: CollectionAccessType.OPEN, 88 collaboratorIds: [], 89 createdAt: new Date(), 90 updatedAt: new Date(), 91 }, 92 new UniqueEntityID(), 93 ).unwrap(); 94 95 await collectionRepository.save(collection); 96 97 const result = await queryRepository.getCardsInCollection( 98 collection.collectionId.getStringValue(), 99 { 100 page: 1, 101 limit: 10, 102 sortBy: CardSortField.UPDATED_AT, 103 sortOrder: SortOrder.DESC, 104 }, 105 ); 106 107 expect(result.items).toHaveLength(0); 108 expect(result.totalCount).toBe(0); 109 expect(result.hasMore).toBe(false); 110 }); 111 112 it('should return URL cards in a collection', async () => { 113 // Create URL cards 114 const url1 = URL.create('https://example.com/article1').unwrap(); 115 const urlMetadata1 = UrlMetadata.create({ 116 url: url1.value, 117 title: 'Collection Article 1', 118 description: 'First article in collection', 119 author: 'John Doe', 120 imageUrl: 'https://example.com/image1.jpg', 121 }).unwrap(); 122 123 const urlCard1 = new CardBuilder() 124 .withCuratorId(curatorId.value) 125 .withUrlCard(url1, urlMetadata1) 126 .withCreatedAt(new Date('2023-01-01')) 127 .withUpdatedAt(new Date('2023-01-01')) 128 .buildOrThrow(); 129 130 const url2 = URL.create('https://example.com/article2').unwrap(); 131 const urlCard2 = new CardBuilder() 132 .withCuratorId(curatorId.value) 133 .withUrlCard(url2) 134 .withCreatedAt(new Date('2023-01-02')) 135 .withUpdatedAt(new Date('2023-01-02')) 136 .buildOrThrow(); 137 138 // Save cards 139 await cardRepository.save(urlCard1); 140 await cardRepository.save(urlCard2); 141 142 // Create collection and add cards 143 const collection = Collection.create( 144 { 145 authorId: curatorId, 146 name: 'Test Collection', 147 accessType: CollectionAccessType.OPEN, 148 collaboratorIds: [], 149 createdAt: new Date(), 150 updatedAt: new Date(), 151 }, 152 new UniqueEntityID(), 153 ).unwrap(); 154 155 collection.addCard(urlCard1.cardId, curatorId); 156 collection.addCard(urlCard2.cardId, curatorId); 157 158 await collectionRepository.save(collection); 159 160 // Query cards in collection 161 const result = await queryRepository.getCardsInCollection( 162 collection.collectionId.getStringValue(), 163 { 164 page: 1, 165 limit: 10, 166 sortBy: CardSortField.UPDATED_AT, 167 sortOrder: SortOrder.DESC, 168 }, 169 ); 170 171 expect(result.items).toHaveLength(2); 172 expect(result.totalCount).toBe(2); 173 expect(result.hasMore).toBe(false); 174 175 // Check URL card data 176 const card1Result = result.items.find((item) => item.url === url1.value); 177 const card2Result = result.items.find((item) => item.url === url2.value); 178 179 expect(card1Result).toBeDefined(); 180 expect(card1Result?.type).toBe(CardTypeEnum.URL); 181 expect(card1Result?.cardContent.title).toBe('Collection Article 1'); 182 expect(card1Result?.cardContent.description).toBe( 183 'First article in collection', 184 ); 185 expect(card1Result?.cardContent.author).toBe('John Doe'); 186 expect(card1Result?.cardContent.imageUrl).toBe( 187 'https://example.com/image1.jpg', 188 ); 189 190 expect(card2Result).toBeDefined(); 191 expect(card2Result?.type).toBe(CardTypeEnum.URL); 192 expect(card2Result?.cardContent.title).toBeUndefined(); // No metadata provided 193 }); 194 195 it('should only include notes by collection author, not notes by other users', async () => { 196 // Create URL card 197 const url = URL.create('https://example.com/shared-article').unwrap(); 198 const urlCard = new CardBuilder() 199 .withCuratorId(curatorId.value) 200 .withUrlCard(url) 201 .buildOrThrow(); 202 203 urlCard.addToLibrary(curatorId); 204 205 await cardRepository.save(urlCard); 206 207 // Create note by collection author 208 const authorNote = new CardBuilder() 209 .withCuratorId(curatorId.value) 210 .withNoteCard('Note by collection author') 211 .withParentCard(urlCard.cardId) 212 .buildOrThrow(); 213 214 authorNote.addToLibrary(curatorId); 215 216 await cardRepository.save(authorNote); 217 218 // Create note by different user on the same URL card 219 const otherUserNote = new CardBuilder() 220 .withCuratorId(otherCuratorId.value) 221 .withNoteCard('Note by other user') 222 .withParentCard(urlCard.cardId) 223 .buildOrThrow(); 224 225 otherUserNote.addToLibrary(otherCuratorId); 226 227 await cardRepository.save(otherUserNote); 228 229 // Create collection authored by curatorId and add URL card 230 const collection = Collection.create( 231 { 232 authorId: curatorId, 233 name: 'Collection by First User', 234 accessType: CollectionAccessType.OPEN, 235 collaboratorIds: [], 236 createdAt: new Date(), 237 updatedAt: new Date(), 238 }, 239 new UniqueEntityID(), 240 ).unwrap(); 241 242 collection.addCard(urlCard.cardId, curatorId); 243 await collectionRepository.save(collection); 244 245 // Query cards in collection 246 const result = await queryRepository.getCardsInCollection( 247 collection.collectionId.getStringValue(), 248 { 249 page: 1, 250 limit: 10, 251 sortBy: CardSortField.UPDATED_AT, 252 sortOrder: SortOrder.DESC, 253 }, 254 ); 255 256 expect(result.items).toHaveLength(1); 257 const urlCardResult = result.items[0]; 258 259 // Should only include the note by the collection author, not the other user's note 260 expect(urlCardResult?.note).toBeDefined(); 261 expect(urlCardResult?.note?.id).toBe(authorNote.cardId.getStringValue()); 262 expect(urlCardResult?.note?.text).toBe('Note by collection author'); 263 }); 264 265 it('should not include notes when only other users have notes, not collection author', async () => { 266 // Create URL card 267 const url = URL.create( 268 'https://example.com/article-with-other-notes', 269 ).unwrap(); 270 const urlCard = new CardBuilder() 271 .withCuratorId(curatorId.value) 272 .withUrlCard(url) 273 .buildOrThrow(); 274 275 urlCard.addToLibrary(curatorId); 276 277 await cardRepository.save(urlCard); 278 279 // Create note by different user on the URL card (NO note by collection author) 280 const otherUserNote = new CardBuilder() 281 .withCuratorId(otherCuratorId.value) 282 .withNoteCard('Note by other user only') 283 .withParentCard(urlCard.cardId) 284 .buildOrThrow(); 285 286 otherUserNote.addToLibrary(otherCuratorId); 287 288 await cardRepository.save(otherUserNote); 289 290 // Create collection authored by curatorId and add URL card 291 const collection = Collection.create( 292 { 293 authorId: curatorId, 294 name: 'Collection with Other User Notes Only', 295 accessType: CollectionAccessType.OPEN, 296 collaboratorIds: [], 297 createdAt: new Date(), 298 updatedAt: new Date(), 299 }, 300 new UniqueEntityID(), 301 ).unwrap(); 302 303 collection.addCard(urlCard.cardId, curatorId); 304 await collectionRepository.save(collection); 305 306 // Query cards in collection 307 const result = await queryRepository.getCardsInCollection( 308 collection.collectionId.getStringValue(), 309 { 310 page: 1, 311 limit: 10, 312 sortBy: CardSortField.UPDATED_AT, 313 sortOrder: SortOrder.DESC, 314 }, 315 ); 316 317 expect(result.items).toHaveLength(1); 318 const urlCardResult = result.items[0]; 319 320 // Should not include any note since only other users have notes, not the collection author 321 expect(urlCardResult?.note).toBeUndefined(); 322 }); 323 324 it('should not include note cards that are not connected to collection URL cards', async () => { 325 // Create URL card in collection 326 const url = URL.create('https://example.com/collection-article').unwrap(); 327 const urlCard = new CardBuilder() 328 .withCuratorId(curatorId.value) 329 .withUrlCard(url) 330 .buildOrThrow(); 331 332 await cardRepository.save(urlCard); 333 334 // Create another URL card NOT in collection 335 const otherUrl = URL.create('https://example.com/other-article').unwrap(); 336 const otherUrlCard = new CardBuilder() 337 .withCuratorId(curatorId.value) 338 .withUrlCard(otherUrl) 339 .buildOrThrow(); 340 341 await cardRepository.save(otherUrlCard); 342 343 // Create note card connected to the OTHER URL card (not in collection) 344 const noteCard = new CardBuilder() 345 .withCuratorId(curatorId.value) 346 .withNoteCard('This note is for the other article') 347 .withParentCard(otherUrlCard.cardId) 348 .buildOrThrow(); 349 350 await cardRepository.save(noteCard); 351 352 // Create collection and add only the first URL card 353 const collection = Collection.create( 354 { 355 authorId: curatorId, 356 name: 'Selective Collection', 357 accessType: CollectionAccessType.OPEN, 358 collaboratorIds: [], 359 createdAt: new Date(), 360 updatedAt: new Date(), 361 }, 362 new UniqueEntityID(), 363 ).unwrap(); 364 365 collection.addCard(urlCard.cardId, curatorId); 366 await collectionRepository.save(collection); 367 368 // Query cards in collection 369 const result = await queryRepository.getCardsInCollection( 370 collection.collectionId.getStringValue(), 371 { 372 page: 1, 373 limit: 10, 374 sortBy: CardSortField.UPDATED_AT, 375 sortOrder: SortOrder.DESC, 376 }, 377 ); 378 379 expect(result.items).toHaveLength(1); 380 const urlCardResult = result.items[0]; 381 382 // Should not have a note since the note is connected to a different URL card 383 expect(urlCardResult?.note).toBeUndefined(); 384 }); 385 386 it('should handle collection with cards from multiple users', async () => { 387 // Create URL cards from different users 388 const url1 = URL.create('https://example.com/user1-article').unwrap(); 389 const urlCard1 = new CardBuilder() 390 .withCuratorId(curatorId.value) 391 .withUrlCard(url1) 392 .buildOrThrow(); 393 394 const url2 = URL.create('https://example.com/user2-article').unwrap(); 395 const urlCard2 = new CardBuilder() 396 .withCuratorId(otherCuratorId.value) 397 .withUrlCard(url2) 398 .buildOrThrow(); 399 400 await cardRepository.save(urlCard1); 401 await cardRepository.save(urlCard2); 402 403 // Create collection and add both cards 404 const collection = Collection.create( 405 { 406 authorId: curatorId, 407 name: 'Multi-User Collection', 408 accessType: CollectionAccessType.OPEN, 409 collaboratorIds: [], 410 createdAt: new Date(), 411 updatedAt: new Date(), 412 }, 413 new UniqueEntityID(), 414 ).unwrap(); 415 416 collection.addCard(urlCard1.cardId, curatorId); 417 collection.addCard(urlCard2.cardId, curatorId); 418 await collectionRepository.save(collection); 419 420 // Query cards in collection 421 const result = await queryRepository.getCardsInCollection( 422 collection.collectionId.getStringValue(), 423 { 424 page: 1, 425 limit: 10, 426 sortBy: CardSortField.UPDATED_AT, 427 sortOrder: SortOrder.DESC, 428 }, 429 ); 430 431 expect(result.items).toHaveLength(2); 432 433 const urls = result.items.map((item) => item.url); 434 expect(urls).toContain(url1.value); 435 expect(urls).toContain(url2.value); 436 }); 437 438 it('should not return note cards directly from collection', async () => { 439 // Create a standalone note card 440 const noteCard = new CardBuilder() 441 .withCuratorId(curatorId.value) 442 .withNoteCard('Standalone note in collection') 443 .buildOrThrow(); 444 445 await cardRepository.save(noteCard); 446 447 // Create collection and add note card directly 448 const collection = Collection.create( 449 { 450 authorId: curatorId, 451 name: 'Collection with Direct Note', 452 accessType: CollectionAccessType.OPEN, 453 collaboratorIds: [], 454 createdAt: new Date(), 455 updatedAt: new Date(), 456 }, 457 new UniqueEntityID(), 458 ).unwrap(); 459 460 collection.addCard(noteCard.cardId, curatorId); 461 await collectionRepository.save(collection); 462 463 // Query cards in collection 464 const result = await queryRepository.getCardsInCollection( 465 collection.collectionId.getStringValue(), 466 { 467 page: 1, 468 limit: 10, 469 sortBy: CardSortField.UPDATED_AT, 470 sortOrder: SortOrder.DESC, 471 }, 472 ); 473 474 // Should not return the note card since we only return URL cards 475 expect(result.items).toHaveLength(0); 476 expect(result.totalCount).toBe(0); 477 }); 478 479 it('should handle sorting by library count in collection', async () => { 480 // Create 3 URL cards with different popularity levels 481 const url1 = URL.create('https://example.com/most-popular').unwrap(); 482 const url2 = URL.create('https://example.com/medium-popular').unwrap(); 483 const url3 = URL.create('https://example.com/least-popular').unwrap(); 484 485 // Create URL cards from the first curator (collection author) 486 const urlCard1 = new CardBuilder() 487 .withCuratorId(curatorId.value) 488 .withUrlCard(url1) 489 .withCreatedAt(new Date('2023-01-01')) 490 .withUpdatedAt(new Date('2023-01-01')) 491 .buildOrThrow(); 492 493 const urlCard2 = new CardBuilder() 494 .withCuratorId(curatorId.value) 495 .withUrlCard(url2) 496 .withCreatedAt(new Date('2023-01-02')) 497 .withUpdatedAt(new Date('2023-01-02')) 498 .buildOrThrow(); 499 500 const urlCard3 = new CardBuilder() 501 .withCuratorId(curatorId.value) 502 .withUrlCard(url3) 503 .withCreatedAt(new Date('2023-01-03')) 504 .withUpdatedAt(new Date('2023-01-03')) 505 .buildOrThrow(); 506 507 await cardRepository.save(urlCard1); 508 await cardRepository.save(urlCard2); 509 await cardRepository.save(urlCard3); 510 511 // Add cards to creator's library 512 urlCard1.addToLibrary(curatorId); 513 urlCard2.addToLibrary(curatorId); 514 urlCard3.addToLibrary(curatorId); 515 516 await cardRepository.save(urlCard1); 517 await cardRepository.save(urlCard2); 518 await cardRepository.save(urlCard3); 519 520 // Create additional cards for the same URLs from other users to increase library counts 521 // URL 1 will be in 3 libraries (curatorId, otherCuratorId, thirdCuratorId) 522 const urlCard1_user2 = new CardBuilder() 523 .withCuratorId(otherCuratorId.value) 524 .withUrlCard(url1) 525 .buildOrThrow(); 526 527 const urlCard1_user3 = new CardBuilder() 528 .withCuratorId(thirdCuratorId.value) 529 .withUrlCard(url1) 530 .buildOrThrow(); 531 532 await cardRepository.save(urlCard1_user2); 533 await cardRepository.save(urlCard1_user3); 534 535 urlCard1_user2.addToLibrary(otherCuratorId); 536 urlCard1_user3.addToLibrary(thirdCuratorId); 537 538 await cardRepository.save(urlCard1_user2); 539 await cardRepository.save(urlCard1_user3); 540 541 // URL 2 will be in 2 libraries (curatorId, otherCuratorId) 542 const urlCard2_user2 = new CardBuilder() 543 .withCuratorId(otherCuratorId.value) 544 .withUrlCard(url2) 545 .buildOrThrow(); 546 547 await cardRepository.save(urlCard2_user2); 548 urlCard2_user2.addToLibrary(otherCuratorId); 549 await cardRepository.save(urlCard2_user2); 550 551 // URL 3 will be in 1 library (only curatorId) - no additional cards needed 552 553 // Create collection and add all three cards 554 const collection = Collection.create( 555 { 556 authorId: curatorId, 557 name: 'Popularity Collection', 558 accessType: CollectionAccessType.OPEN, 559 collaboratorIds: [], 560 createdAt: new Date(), 561 updatedAt: new Date(), 562 }, 563 new UniqueEntityID(), 564 ).unwrap(); 565 566 collection.addCard(urlCard1.cardId, curatorId); 567 collection.addCard(urlCard2.cardId, curatorId); 568 collection.addCard(urlCard3.cardId, curatorId); 569 await collectionRepository.save(collection); 570 571 // Query cards sorted by library count descending 572 const result = await queryRepository.getCardsInCollection( 573 collection.collectionId.getStringValue(), 574 { 575 page: 1, 576 limit: 10, 577 sortBy: CardSortField.LIBRARY_COUNT, 578 sortOrder: SortOrder.DESC, 579 }, 580 ); 581 582 expect(result.items).toHaveLength(3); 583 584 // Verify sorting by library count (descending) 585 expect(result.items[0]?.url).toBe(url1.value); // Most popular (3 libraries) 586 expect(result.items[0]?.urlLibraryCount).toBe(3); 587 588 expect(result.items[1]?.url).toBe(url2.value); // Medium popular (2 libraries) 589 expect(result.items[1]?.urlLibraryCount).toBe(2); 590 591 expect(result.items[2]?.url).toBe(url3.value); // Least popular (1 library) 592 expect(result.items[2]?.urlLibraryCount).toBe(1); 593 }); 594 595 it('should handle pagination for collection cards', async () => { 596 // Create multiple URL cards 597 const urlCards = []; 598 for (let i = 1; i <= 5; i++) { 599 const url = URL.create( 600 `https://example.com/collection-article${i}`, 601 ).unwrap(); 602 const urlCard = new CardBuilder() 603 .withCuratorId(curatorId.value) 604 .withUrlCard(url) 605 .withCreatedAt(new Date(`2023-01-${i.toString().padStart(2, '0')}`)) 606 .withUpdatedAt(new Date(`2023-01-${i.toString().padStart(2, '0')}`)) 607 .buildOrThrow(); 608 609 await cardRepository.save(urlCard); 610 urlCards.push(urlCard); 611 } 612 613 // Create collection and add all cards 614 const collection = Collection.create( 615 { 616 authorId: curatorId, 617 name: 'Large Collection', 618 accessType: CollectionAccessType.OPEN, 619 collaboratorIds: [], 620 createdAt: new Date(), 621 updatedAt: new Date(), 622 }, 623 new UniqueEntityID(), 624 ).unwrap(); 625 626 for (const urlCard of urlCards) { 627 collection.addCard(urlCard.cardId, curatorId); 628 } 629 await collectionRepository.save(collection); 630 631 // Test first page 632 const page1 = await queryRepository.getCardsInCollection( 633 collection.collectionId.getStringValue(), 634 { 635 page: 1, 636 limit: 2, 637 sortBy: CardSortField.UPDATED_AT, 638 sortOrder: SortOrder.ASC, 639 }, 640 ); 641 642 expect(page1.items).toHaveLength(2); 643 expect(page1.totalCount).toBe(5); 644 expect(page1.hasMore).toBe(true); 645 646 // Test second page 647 const page2 = await queryRepository.getCardsInCollection( 648 collection.collectionId.getStringValue(), 649 { 650 page: 2, 651 limit: 2, 652 sortBy: CardSortField.UPDATED_AT, 653 sortOrder: SortOrder.ASC, 654 }, 655 ); 656 657 expect(page2.items).toHaveLength(2); 658 expect(page2.totalCount).toBe(5); 659 expect(page2.hasMore).toBe(true); 660 661 // Test last page 662 const page3 = await queryRepository.getCardsInCollection( 663 collection.collectionId.getStringValue(), 664 { 665 page: 3, 666 limit: 2, 667 sortBy: CardSortField.UPDATED_AT, 668 sortOrder: SortOrder.ASC, 669 }, 670 ); 671 672 expect(page3.items).toHaveLength(1); 673 expect(page3.totalCount).toBe(5); 674 expect(page3.hasMore).toBe(false); 675 }); 676 }); 677 678 describe('urlInLibrary', () => { 679 it('should return urlInLibrary as undefined when callingUserId is not provided', async () => { 680 const url = URL.create('https://example.com/collection-url').unwrap(); 681 const urlCard = new CardBuilder() 682 .withCuratorId(curatorId.value) 683 .withUrlCard(url) 684 .buildOrThrow(); 685 686 await cardRepository.save(urlCard); 687 688 // Create collection and add card 689 const collection = Collection.create( 690 { 691 authorId: curatorId, 692 name: 'Test Collection', 693 accessType: CollectionAccessType.OPEN, 694 collaboratorIds: [], 695 createdAt: new Date(), 696 updatedAt: new Date(), 697 }, 698 new UniqueEntityID(), 699 ).unwrap(); 700 701 collection.addCard(urlCard.cardId, curatorId); 702 await collectionRepository.save(collection); 703 704 // Query without callingUserId 705 const result = await queryRepository.getCardsInCollection( 706 collection.collectionId.getStringValue(), 707 { 708 page: 1, 709 limit: 10, 710 sortBy: CardSortField.UPDATED_AT, 711 sortOrder: SortOrder.DESC, 712 }, 713 ); 714 715 expect(result.items).toHaveLength(1); 716 expect(result.items[0]?.urlInLibrary).toBeUndefined(); 717 }); 718 719 it('should return urlInLibrary as true when callingUserId has the URL in their library', async () => { 720 const sharedUrl = 'https://example.com/shared-collection-url'; 721 const url = URL.create(sharedUrl).unwrap(); 722 723 // Create URL card for first user and add to collection 724 const urlCard1 = new CardBuilder() 725 .withCuratorId(curatorId.value) 726 .withUrlCard(url) 727 .buildOrThrow(); 728 729 await cardRepository.save(urlCard1); 730 731 // Create collection and add card 732 const collection = Collection.create( 733 { 734 authorId: curatorId, 735 name: 'Shared Collection', 736 accessType: CollectionAccessType.OPEN, 737 collaboratorIds: [], 738 createdAt: new Date(), 739 updatedAt: new Date(), 740 }, 741 new UniqueEntityID(), 742 ).unwrap(); 743 744 collection.addCard(urlCard1.cardId, curatorId); 745 await collectionRepository.save(collection); 746 747 // Create URL card for second user with the same URL 748 const urlCard2 = new CardBuilder() 749 .withCuratorId(otherCuratorId.value) 750 .withUrlCard(url) 751 .buildOrThrow(); 752 753 await cardRepository.save(urlCard2); 754 urlCard2.addToLibrary(otherCuratorId); 755 await cardRepository.save(urlCard2); 756 757 // Query collection cards with second user as callingUserId 758 const result = await queryRepository.getCardsInCollection( 759 collection.collectionId.getStringValue(), 760 { 761 page: 1, 762 limit: 10, 763 sortBy: CardSortField.UPDATED_AT, 764 sortOrder: SortOrder.DESC, 765 }, 766 otherCuratorId.value, // callingUserId 767 ); 768 769 expect(result.items).toHaveLength(1); 770 expect(result.items[0]?.url).toBe(sharedUrl); 771 expect(result.items[0]?.urlInLibrary).toBe(true); // otherCurator has this URL 772 }); 773 774 it('should return urlInLibrary as false when callingUserId does not have the URL in their library', async () => { 775 const url = URL.create( 776 'https://example.com/unique-collection-url', 777 ).unwrap(); 778 779 // Create URL card for first user and add to collection 780 const urlCard = new CardBuilder() 781 .withCuratorId(curatorId.value) 782 .withUrlCard(url) 783 .buildOrThrow(); 784 785 await cardRepository.save(urlCard); 786 787 // Create collection and add card 788 const collection = Collection.create( 789 { 790 authorId: curatorId, 791 name: 'Unique Collection', 792 accessType: CollectionAccessType.OPEN, 793 collaboratorIds: [], 794 createdAt: new Date(), 795 updatedAt: new Date(), 796 }, 797 new UniqueEntityID(), 798 ).unwrap(); 799 800 collection.addCard(urlCard.cardId, curatorId); 801 await collectionRepository.save(collection); 802 803 // Query collection cards with second user as callingUserId (who doesn't have this URL) 804 const result = await queryRepository.getCardsInCollection( 805 collection.collectionId.getStringValue(), 806 { 807 page: 1, 808 limit: 10, 809 sortBy: CardSortField.UPDATED_AT, 810 sortOrder: SortOrder.DESC, 811 }, 812 otherCuratorId.value, // callingUserId who doesn't have this URL 813 ); 814 815 expect(result.items).toHaveLength(1); 816 expect(result.items[0]?.urlInLibrary).toBe(false); // otherCurator doesn't have this URL 817 }); 818 819 it('should return urlInLibrary as true when the collection author is the same as callingUserId', async () => { 820 const url = URL.create( 821 'https://example.com/author-collection-url', 822 ).unwrap(); 823 824 // Create URL card for curator (collection author) 825 const urlCard = new CardBuilder() 826 .withCuratorId(curatorId.value) 827 .withUrlCard(url) 828 .buildOrThrow(); 829 830 await cardRepository.save(urlCard); 831 urlCard.addToLibrary(curatorId); 832 await cardRepository.save(urlCard); 833 834 // Create collection and add card 835 const collection = Collection.create( 836 { 837 authorId: curatorId, 838 name: 'Author Collection', 839 accessType: CollectionAccessType.OPEN, 840 collaboratorIds: [], 841 createdAt: new Date(), 842 updatedAt: new Date(), 843 }, 844 new UniqueEntityID(), 845 ).unwrap(); 846 847 collection.addCard(urlCard.cardId, curatorId); 848 await collectionRepository.save(collection); 849 850 // Query collection cards with the author as callingUserId 851 const result = await queryRepository.getCardsInCollection( 852 collection.collectionId.getStringValue(), 853 { 854 page: 1, 855 limit: 10, 856 sortBy: CardSortField.UPDATED_AT, 857 sortOrder: SortOrder.DESC, 858 }, 859 curatorId.value, // same as collection author 860 ); 861 862 expect(result.items).toHaveLength(1); 863 expect(result.items[0]?.urlInLibrary).toBe(true); // curator has their own URL 864 }); 865 866 it('should return urlInLibrary correctly when user has multiple cards with the same URL', async () => { 867 const sharedUrl = 'https://example.com/multi-card-collection-url'; 868 const url = URL.create(sharedUrl).unwrap(); 869 870 // Create URL card for first user and add to collection 871 const urlCard1 = new CardBuilder() 872 .withCuratorId(curatorId.value) 873 .withUrlCard(url) 874 .buildOrThrow(); 875 876 await cardRepository.save(urlCard1); 877 878 // Create collection and add card 879 const collection = Collection.create( 880 { 881 authorId: curatorId, 882 name: 'Multi-Card Collection', 883 accessType: CollectionAccessType.OPEN, 884 collaboratorIds: [], 885 createdAt: new Date(), 886 updatedAt: new Date(), 887 }, 888 new UniqueEntityID(), 889 ).unwrap(); 890 891 collection.addCard(urlCard1.cardId, curatorId); 892 await collectionRepository.save(collection); 893 894 // Create URL card for otherCurator with the same URL (multiple cards) 895 const urlCard2a = new CardBuilder() 896 .withCuratorId(otherCuratorId.value) 897 .withUrlCard(url) 898 .buildOrThrow(); 899 900 await cardRepository.save(urlCard2a); 901 urlCard2a.addToLibrary(otherCuratorId); 902 await cardRepository.save(urlCard2a); 903 904 // Create ANOTHER URL card for otherCurator with the same URL 905 const urlCard2b = new CardBuilder() 906 .withCuratorId(otherCuratorId.value) 907 .withUrlCard(url) 908 .buildOrThrow(); 909 910 await cardRepository.save(urlCard2b); 911 urlCard2b.addToLibrary(otherCuratorId); 912 await cardRepository.save(urlCard2b); 913 914 // Query collection cards with second user as callingUserId 915 const result = await queryRepository.getCardsInCollection( 916 collection.collectionId.getStringValue(), 917 { 918 page: 1, 919 limit: 10, 920 sortBy: CardSortField.UPDATED_AT, 921 sortOrder: SortOrder.DESC, 922 }, 923 otherCuratorId.value, // callingUserId who has multiple cards with this URL 924 ); 925 926 expect(result.items).toHaveLength(1); 927 expect(result.items[0]?.url).toBe(sharedUrl); 928 expect(result.items[0]?.urlInLibrary).toBe(true); // otherCurator has this URL (even with multiple cards) 929 }); 930 931 it('should handle multiple URLs in collection with different urlInLibrary values', async () => { 932 // URL 1: otherCurator also has it 933 const url1 = URL.create( 934 'https://example.com/shared-collection-url-1', 935 ).unwrap(); 936 const urlCard1a = new CardBuilder() 937 .withCuratorId(curatorId.value) 938 .withUrlCard(url1) 939 .withCreatedAt(new Date('2023-01-01')) 940 .withUpdatedAt(new Date('2023-01-01')) 941 .buildOrThrow(); 942 943 await cardRepository.save(urlCard1a); 944 945 const urlCard1b = new CardBuilder() 946 .withCuratorId(otherCuratorId.value) 947 .withUrlCard(url1) 948 .buildOrThrow(); 949 950 await cardRepository.save(urlCard1b); 951 urlCard1b.addToLibrary(otherCuratorId); 952 await cardRepository.save(urlCard1b); 953 954 // URL 2: otherCurator does NOT have it 955 const url2 = URL.create( 956 'https://example.com/unique-collection-url-2', 957 ).unwrap(); 958 const urlCard2 = new CardBuilder() 959 .withCuratorId(curatorId.value) 960 .withUrlCard(url2) 961 .withCreatedAt(new Date('2023-01-02')) 962 .withUpdatedAt(new Date('2023-01-02')) 963 .buildOrThrow(); 964 965 await cardRepository.save(urlCard2); 966 967 // Create collection and add both cards 968 const collection = Collection.create( 969 { 970 authorId: curatorId, 971 name: 'Mixed Collection', 972 accessType: CollectionAccessType.OPEN, 973 collaboratorIds: [], 974 createdAt: new Date(), 975 updatedAt: new Date(), 976 }, 977 new UniqueEntityID(), 978 ).unwrap(); 979 980 collection.addCard(urlCard1a.cardId, curatorId); 981 collection.addCard(urlCard2.cardId, curatorId); 982 await collectionRepository.save(collection); 983 984 // Query collection cards with otherCurator as callingUserId 985 const result = await queryRepository.getCardsInCollection( 986 collection.collectionId.getStringValue(), 987 { 988 page: 1, 989 limit: 10, 990 sortBy: CardSortField.UPDATED_AT, 991 sortOrder: SortOrder.DESC, 992 }, 993 otherCuratorId.value, 994 ); 995 996 expect(result.items).toHaveLength(2); 997 998 const card1 = result.items.find((item) => item.url === url1.value); 999 const card2 = result.items.find((item) => item.url === url2.value); 1000 1001 expect(card1?.urlInLibrary).toBe(true); // otherCurator has this URL 1002 expect(card2?.urlInLibrary).toBe(false); // otherCurator doesn't have this URL 1003 }); 1004 1005 it('should correctly handle urlInLibrary with third user viewing collection', async () => { 1006 const sharedUrl = 'https://example.com/popular-collection-url'; 1007 const url = URL.create(sharedUrl).unwrap(); 1008 1009 // First user creates card and adds to collection 1010 const urlCard1 = new CardBuilder() 1011 .withCuratorId(curatorId.value) 1012 .withUrlCard(url) 1013 .buildOrThrow(); 1014 1015 await cardRepository.save(urlCard1); 1016 urlCard1.addToLibrary(curatorId); 1017 await cardRepository.save(urlCard1); 1018 1019 // Create collection and add card 1020 const collection = Collection.create( 1021 { 1022 authorId: curatorId, 1023 name: 'Popular Collection', 1024 accessType: CollectionAccessType.OPEN, 1025 collaboratorIds: [], 1026 createdAt: new Date(), 1027 updatedAt: new Date(), 1028 }, 1029 new UniqueEntityID(), 1030 ).unwrap(); 1031 1032 collection.addCard(urlCard1.cardId, curatorId); 1033 await collectionRepository.save(collection); 1034 1035 // Second user creates card with the same URL 1036 const urlCard2 = new CardBuilder() 1037 .withCuratorId(otherCuratorId.value) 1038 .withUrlCard(url) 1039 .buildOrThrow(); 1040 1041 await cardRepository.save(urlCard2); 1042 urlCard2.addToLibrary(otherCuratorId); 1043 await cardRepository.save(urlCard2); 1044 1045 // Query collection cards with third user as callingUserId (who doesn't have this URL) 1046 const result = await queryRepository.getCardsInCollection( 1047 collection.collectionId.getStringValue(), 1048 { 1049 page: 1, 1050 limit: 10, 1051 sortBy: CardSortField.UPDATED_AT, 1052 sortOrder: SortOrder.DESC, 1053 }, 1054 thirdCuratorId.value, // third user checking 1055 ); 1056 1057 expect(result.items).toHaveLength(1); 1058 expect(result.items[0]?.url).toBe(sharedUrl); 1059 expect(result.items[0]?.urlInLibrary).toBe(false); // thirdCurator doesn't have this URL 1060 expect(result.items[0]?.urlLibraryCount).toBe(2); // But 2 users have it 1061 }); 1062 }); 1063 1064 describe('URL type filtering', () => { 1065 it('should filter collection cards by urlType', async () => { 1066 // Create URL cards with different types 1067 const articleUrl = URL.create('https://example.com/article').unwrap(); 1068 const articleMetadata = UrlMetadata.create({ 1069 url: articleUrl.value, 1070 title: 'Test Article', 1071 type: UrlType.ARTICLE, 1072 }).unwrap(); 1073 1074 const videoUrl = URL.create('https://example.com/video').unwrap(); 1075 const videoMetadata = UrlMetadata.create({ 1076 url: videoUrl.value, 1077 title: 'Test Video', 1078 type: UrlType.VIDEO, 1079 }).unwrap(); 1080 1081 const articleCard = new CardBuilder() 1082 .withCuratorId(curatorId.value) 1083 .withUrlCard(articleUrl, articleMetadata) 1084 .buildOrThrow(); 1085 1086 const videoCard = new CardBuilder() 1087 .withCuratorId(curatorId.value) 1088 .withUrlCard(videoUrl, videoMetadata) 1089 .buildOrThrow(); 1090 1091 await cardRepository.save(articleCard); 1092 await cardRepository.save(videoCard); 1093 1094 // Create collection and add both cards 1095 const collection = Collection.create( 1096 { 1097 authorId: curatorId, 1098 name: 'Mixed Collection', 1099 accessType: CollectionAccessType.OPEN, 1100 collaboratorIds: [], 1101 createdAt: new Date(), 1102 updatedAt: new Date(), 1103 }, 1104 new UniqueEntityID(), 1105 ).unwrap(); 1106 1107 collection.addCard(articleCard.cardId, curatorId); 1108 collection.addCard(videoCard.cardId, curatorId); 1109 await collectionRepository.save(collection); 1110 1111 // Query for only article type 1112 const articleResult = await queryRepository.getCardsInCollection( 1113 collection.collectionId.getStringValue(), 1114 { 1115 page: 1, 1116 limit: 10, 1117 sortBy: CardSortField.UPDATED_AT, 1118 sortOrder: SortOrder.DESC, 1119 urlType: UrlType.ARTICLE, 1120 }, 1121 ); 1122 1123 expect(articleResult.items).toHaveLength(1); 1124 expect(articleResult.items[0]?.url).toBe(articleUrl.value); 1125 1126 // Query for only video type 1127 const videoResult = await queryRepository.getCardsInCollection( 1128 collection.collectionId.getStringValue(), 1129 { 1130 page: 1, 1131 limit: 10, 1132 sortBy: CardSortField.UPDATED_AT, 1133 sortOrder: SortOrder.DESC, 1134 urlType: UrlType.VIDEO, 1135 }, 1136 ); 1137 1138 expect(videoResult.items).toHaveLength(1); 1139 expect(videoResult.items[0]?.url).toBe(videoUrl.value); 1140 }); 1141 1142 it('should return empty result when no collection cards match the urlType filter', async () => { 1143 const articleUrl = URL.create('https://example.com/article').unwrap(); 1144 const articleMetadata = UrlMetadata.create({ 1145 url: articleUrl.value, 1146 title: 'Test Article', 1147 type: UrlType.ARTICLE, 1148 }).unwrap(); 1149 1150 const articleCard = new CardBuilder() 1151 .withCuratorId(curatorId.value) 1152 .withUrlCard(articleUrl, articleMetadata) 1153 .buildOrThrow(); 1154 1155 await cardRepository.save(articleCard); 1156 1157 // Create collection and add article card 1158 const collection = Collection.create( 1159 { 1160 authorId: curatorId, 1161 name: 'Article Collection', 1162 accessType: CollectionAccessType.OPEN, 1163 collaboratorIds: [], 1164 createdAt: new Date(), 1165 updatedAt: new Date(), 1166 }, 1167 new UniqueEntityID(), 1168 ).unwrap(); 1169 1170 collection.addCard(articleCard.cardId, curatorId); 1171 await collectionRepository.save(collection); 1172 1173 // Query for video type when only article exists 1174 const result = await queryRepository.getCardsInCollection( 1175 collection.collectionId.getStringValue(), 1176 { 1177 page: 1, 1178 limit: 10, 1179 sortBy: CardSortField.UPDATED_AT, 1180 sortOrder: SortOrder.DESC, 1181 urlType: UrlType.VIDEO, 1182 }, 1183 ); 1184 1185 expect(result.items).toHaveLength(0); 1186 expect(result.totalCount).toBe(0); 1187 }); 1188 }); 1189});