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