This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

wip more changes

+177 -40
+177 -40
docs/plan/api_client_type_unification.md
··· 82 82 - Sometimes `author`, sometimes `createdBy`, sometimes `authorId`, sometimes `authorHandle` 83 83 - Inconsistent inclusion of `cardCount`, `createdAt`, `updatedAt` 84 84 85 + ### GetLibrariesForUrlResponse - Missing Card Data 86 + 87 + **Current Structure** (responses.ts:331-340): 88 + ```tsx 89 + { 90 + libraries: { 91 + userId: string; 92 + name: string; 93 + handle: string; 94 + avatarUrl?: string; 95 + }[]; 96 + // ... 97 + } 98 + ``` 99 + 100 + **Issue**: When showing "who has this URL in their library", we only show user info but not their specific card. This means we can't display: 101 + - The user's note on the URL 102 + - When they saved it 103 + - Their specific card metadata 104 + 105 + **Proposed Enhancement**: Return both user and card data to enable richer UI display. 106 + 85 107 --- 86 108 87 109 ## Proposed Changes to Response Types ··· 249 271 #### GetLibrariesForUrlResponse 250 272 ```tsx 251 273 export interface GetLibrariesForUrlResponse { 252 - libraries: User[]; // Changed from inline type, enriched with full user data 274 + libraries: { 275 + user: User; // The user who has this URL in their library 276 + card: UrlCard; // Their specific card (may include a note) 277 + }[]; 253 278 pagination: Pagination; 254 279 sorting: CardSorting; 255 280 } ··· 330 355 331 356 #### Update LibraryForUrlDTO 332 357 ```tsx 333 - // Keep minimal - will be enriched in use case 358 + // Repository returns card data - will be enriched with user profile in use case 334 359 export interface LibraryForUrlDTO { 335 360 userId: string; 336 361 cardId: string; 337 - name: string; // NEW - include from repository 338 - handle: string; // NEW - include from repository 339 - avatarUrl?: string; // NEW - include from repository 362 + // Card data 363 + url: string; 364 + cardContent: { 365 + url: string; 366 + title?: string; 367 + description?: string; 368 + author?: string; 369 + thumbnailUrl?: string; 370 + }; 371 + libraryCount: number; 372 + urlLibraryCount: number; 373 + urlInLibrary?: boolean; 374 + createdAt: Date; 375 + updatedAt: Date; 376 + note?: { 377 + id: string; 378 + text: string; 379 + }; 380 + // Note: userId is the card author in this context (it's their card in their library) 340 381 } 341 382 ``` 342 383 343 384 **Implementation Impact**: 344 - - Update SQL queries in `DrizzleCardQueryRepository` to include `cards.curatorId as authorId` 345 - - Update SQL queries for `getLibrariesForUrl` to join with profiles table 385 + - Update SQL queries in `DrizzleCardQueryRepository` to include `cards.curatorId as authorId` in all `UrlCardView` queries 386 + - Update `getLibrariesForUrl` query to return full card data (similar to how `getUrlCardsOfUser` works) 387 + - No need to join with profiles table - enrichment happens in use case (following the pattern from `GetCollectionsForUrlUseCase`) 346 388 347 389 ### 2. ICollectionQueryRepository 348 390 ··· 571 613 **File**: `src/modules/cards/application/useCases/queries/GetLibrariesForUrlUseCase.ts` 572 614 573 615 **Changes**: 574 - - Enrich library data with full user profiles (name, handle, avatarUrl) 575 - - OR: Update repository to join with profiles table and return full data 616 + - Repository now returns full card data in `LibraryForUrlDTO` 617 + - Enrich with user profiles for each library owner 618 + - Transform to return both `user` and `card` objects 576 619 577 - **Option 1: Enrich in Use Case** 620 + **Updated Code** (following pattern from `GetCollectionsForUrlUseCase`): 578 621 ```tsx 579 - // After fetching from repository 580 - const uniqueUserIds = Array.from( 581 - new Set(result.items.map(lib => lib.userId)) 582 - ); 622 + async execute( 623 + query: GetLibrariesForUrlQuery, 624 + ): Promise<Result<GetLibrariesForUrlResult>> { 625 + // Validate URL 626 + const urlResult = URL.create(query.url); 627 + if (urlResult.isErr()) { 628 + return err( 629 + new ValidationError(`Invalid URL: ${urlResult.error.message}`), 630 + ); 631 + } 632 + 633 + // Set defaults 634 + const page = query.page || 1; 635 + const limit = Math.min(query.limit || 20, 100); 636 + const sortBy = query.sortBy || CardSortField.UPDATED_AT; 637 + const sortOrder = query.sortOrder || SortOrder.DESC; 638 + 639 + try { 640 + // Execute query to get libraries with full card data 641 + const result = await this.cardQueryRepo.getLibrariesForUrl(query.url, { 642 + page, 643 + limit, 644 + sortBy, 645 + sortOrder, 646 + }); 647 + 648 + // Enrich with user profiles 649 + const uniqueUserIds = Array.from( 650 + new Set(result.items.map((item) => item.userId)), 651 + ); 652 + 653 + const profilePromises = uniqueUserIds.map((userId) => 654 + this.profileService.getProfile(userId, query.callingUserId), 655 + ); 656 + 657 + const profileResults = await Promise.all(profilePromises); 658 + 659 + // Create a map of profiles 660 + const profileMap = new Map<string, User>(); 661 + 662 + for (let i = 0; i < uniqueUserIds.length; i++) { 663 + const profileResult = profileResults[i]; 664 + const userId = uniqueUserIds[i]; 665 + if (!profileResult || !userId) { 666 + return err(new Error('Missing profile result or user ID')); 667 + } 668 + if (profileResult.isErr()) { 669 + return err( 670 + new Error( 671 + `Failed to fetch user profile: ${profileResult.error instanceof Error ? profileResult.error.message : 'Unknown error'}`, 672 + ), 673 + ); 674 + } 675 + const profile = profileResult.value; 676 + profileMap.set(userId, { 677 + id: profile.id, 678 + name: profile.name, 679 + handle: profile.handle, 680 + avatarUrl: profile.avatarUrl, 681 + description: profile.bio, 682 + }); 683 + } 583 684 584 - const userProfiles = new Map<string, User>(); 585 - const profileResults = await Promise.all( 586 - uniqueUserIds.map(id => this.profileService.getProfile(id)) 587 - ); 685 + // Map items with enriched user data and card data 686 + const enrichedLibraries = result.items.map((item) => { 687 + const user = profileMap.get(item.userId); 688 + if (!user) { 689 + throw new Error(`Profile not found for user ${item.userId}`); 690 + } 588 691 589 - // Build user map and transform to User[] 590 - const enrichedLibraries = result.items.map(lib => { 591 - const profile = userProfiles.get(lib.userId); 592 - return { 593 - id: profile.id, 594 - name: profile.name, 595 - handle: profile.handle, 596 - avatarUrl: profile.avatarUrl, 597 - }; 598 - }); 599 - ``` 692 + // Build card object 693 + // Note: userId is the card author (it's their card in their library) 694 + const card: UrlCard = { 695 + id: item.cardId, 696 + type: 'URL', 697 + url: item.url, 698 + cardContent: item.cardContent, 699 + libraryCount: item.libraryCount, 700 + urlLibraryCount: item.urlLibraryCount, 701 + urlInLibrary: item.urlInLibrary, 702 + createdAt: item.createdAt.toISOString(), 703 + updatedAt: item.updatedAt.toISOString(), 704 + author: user, // Card author is same as library user 705 + note: item.note, 706 + }; 600 707 601 - **Option 2: Update Repository** (Preferred - less overhead) 602 - - Update `getLibrariesForUrl` query to join with profiles 603 - - Return full user data in `LibraryForUrlDTO` 708 + return { 709 + user, 710 + card, 711 + }; 712 + }); 713 + 714 + return ok({ 715 + libraries: enrichedLibraries, 716 + pagination: { 717 + currentPage: page, 718 + totalPages: Math.ceil(result.totalCount / limit), 719 + totalCount: result.totalCount, 720 + hasMore: page * limit < result.totalCount, 721 + limit, 722 + }, 723 + sorting: { 724 + sortBy, 725 + sortOrder, 726 + }, 727 + }); 728 + } catch (error) { 729 + return err( 730 + new Error( 731 + `Failed to retrieve libraries for URL: ${error instanceof Error ? error.message : 'Unknown error'}`, 732 + ), 733 + ); 734 + } 735 + } 736 + ``` 604 737 605 738 **Dependencies**: 606 - - Option 1: Needs `IProfileService` injected 607 - - Option 2: Update repository implementation 739 + - Needs `IProfileService` injected (add if not already present) 740 + - Repository must return full card data in `LibraryForUrlDTO` 608 741 609 742 ### 7. GetNoteCardsForUrlUseCase 610 743 ··· 758 891 759 892 ### Phase 1: Repository Updates 760 893 1. Update `UrlCardView` in `ICardQueryRepository.ts` to include `authorId` 761 - 2. Update `DrizzleCardQueryRepository` to include `cards.curatorId as authorId` in all queries returning `UrlCardView` 762 - 3. (Optional) Update `getLibrariesForUrl` to join with profiles and return enriched data 894 + 2. Update `LibraryForUrlDTO` in `ICardQueryRepository.ts` to include full card data (url, cardContent, libraryCount, etc.) 895 + 3. Update `DrizzleCardQueryRepository` to: 896 + - Include `cards.curatorId as authorId` in all queries returning `UrlCardView` 897 + - Update `getLibrariesForUrl` query to return full card data (similar to `getUrlCardsOfUser`) 763 898 764 899 ### Phase 2: Response Type Updates 765 900 1. Add unified `User` interface in `responses.ts` ··· 773 908 1. **GetProfileUseCase** - Add `description` field (minimal change) 774 909 2. **GetLibrariesForCardUseCase** - Add `description` to user DTOs 775 910 3. **GetNoteCardsForUrlUseCase** - Add `description` to author 776 - 4. **GetLibrariesForUrlUseCase** - Enrich with full user data OR wait for repository update 911 + 4. **GetLibrariesForUrlUseCase** - Enrich with user profiles, return `{ user, card }[]` instead of just users 777 912 5. **GetCollectionsUseCase** - Change `createdBy` to `author`, add `description` 778 913 6. **GetCollectionsForUrlUseCase** - Add collection dates/cardCount, author description 779 914 7. **GetUrlStatusForMyLibraryUseCase** - Enrich collections with full data ··· 836 971 2. **Performance**: Enriching authors for all cards in list views will add N+1 queries. 837 972 - **Mitigation**: Batch profile fetches with `Promise.all`, cache frequently accessed profiles, or update repository to join with profiles table. 838 973 839 - 3. **LibrariesForUrl**: Should we enrich in use case or update repository? 840 - - **Recommendation**: Update repository to join with profiles - more efficient, cleaner code. 974 + 3. **LibrariesForUrl**: Return both user and card data in response 975 + - **Decision**: Response now returns `{ user: User, card: UrlCard }[]` to show both who saved the URL and their specific card (with potential note) 976 + - **Implementation**: Repository returns full card data, use case enriches with user profiles (following pattern from `GetCollectionsForUrlUseCase`) 841 977 842 978 4. **Collection enrichment**: When displaying collections in UrlCard, do we need full Collection objects? 843 979 - **Current approach**: Keep minimal `{id, name, authorId}[]` to avoid over-fetching ··· 852 988 1. **User**: Single interface with optional `description` 853 989 2. **UrlCard**: Add `author` field (currently missing!) 854 990 3. **Collection**: Standardize to `author` (not `createdBy`), ensure complete data 991 + 4. **GetLibrariesForUrl**: Enhanced to return both `user` and `card` objects, enabling display of who saved a URL and their specific card (with potential note) 855 992 856 - Most changes are in the API client response types and use cases. Repository changes are minimal (add `authorId` to card queries). Use cases will enrich data by fetching author profiles using the existing `IProfileService`. 993 + Most changes are in the API client response types and use cases. Repository changes are minimal (add `authorId` to card queries, expand `getLibrariesForUrl` to return full card data). Use cases will enrich data by fetching author profiles using the existing `IProfileService`.