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