This repository has no description
0

Configure Feed

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

semble / src / modules / cards / application / useCases / queries / SearchCollectionsUseCase.ts
5.2 kB 164 lines
1import { err, ok, Result } from 'src/shared/core/Result'; 2import { UseCase } from 'src/shared/core/UseCase'; 3import { 4 ICollectionQueryRepository, 5 CollectionSortField, 6 SortOrder, 7} from '../../../domain/ICollectionQueryRepository'; 8import { IProfileService } from 'src/modules/cards/domain/services/IProfileService'; 9import { 10 CollectionDTO, 11 PaginationDTO, 12 CollectionSortingDTO, 13} from '@semble/types'; 14import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService'; 15import { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle'; 16import { CollectionAccessType } from '../../../domain/Collection'; 17 18export interface SearchCollectionsQuery { 19 page?: number; 20 limit?: number; 21 sortBy?: CollectionSortField; 22 sortOrder?: SortOrder; 23 searchText?: string; 24 identifier?: string; // Can be DID or handle 25 accessType?: CollectionAccessType; 26} 27 28export interface SearchCollectionsResult { 29 collections: CollectionDTO[]; 30 pagination: PaginationDTO; 31 sorting: CollectionSortingDTO; 32} 33 34export class SearchCollectionsUseCase 35 implements UseCase<SearchCollectionsQuery, Result<SearchCollectionsResult>> 36{ 37 constructor( 38 private collectionQueryRepo: ICollectionQueryRepository, 39 private profileService: IProfileService, 40 private identityResolutionService: IIdentityResolutionService, 41 ) {} 42 43 async execute( 44 query: SearchCollectionsQuery, 45 ): Promise<Result<SearchCollectionsResult>> { 46 // Set defaults 47 const page = query.page || 1; 48 const limit = Math.min(query.limit || 20, 100); // Cap at 100 49 const sortBy = query.sortBy || CollectionSortField.UPDATED_AT; 50 const sortOrder = query.sortOrder || SortOrder.DESC; 51 52 try { 53 // Resolve identifier to DID if provided 54 let authorId: string | undefined; 55 if (query.identifier) { 56 const identifierResult = DIDOrHandle.create(query.identifier); 57 if (identifierResult.isErr()) { 58 return err( 59 new Error(`Invalid identifier: ${identifierResult.error.message}`), 60 ); 61 } 62 63 const didResult = await this.identityResolutionService.resolveToDID( 64 identifierResult.value, 65 ); 66 if (didResult.isErr()) { 67 return err( 68 new Error( 69 `Failed to resolve identifier to DID: ${didResult.error.message}`, 70 ), 71 ); 72 } 73 74 authorId = didResult.value.value; 75 } 76 77 // Validate accessType if provided 78 if ( 79 query.accessType && 80 !Object.values(CollectionAccessType).includes(query.accessType) 81 ) { 82 return err(new Error(`Invalid access type: ${query.accessType}`)); 83 } 84 85 // Execute query to get raw collection data 86 const result = await this.collectionQueryRepo.searchCollections({ 87 page, 88 limit, 89 sortBy, 90 sortOrder, 91 searchText: query.searchText, 92 authorId, 93 accessType: query.accessType, 94 }); 95 96 // Get unique author IDs from the results 97 const authorIds = [...new Set(result.items.map((item) => item.authorId))]; 98 99 // Fetch profiles for all authors 100 const profilePromises = authorIds.map((authorId) => 101 this.profileService.getProfile(authorId), 102 ); 103 const profileResults = await Promise.all(profilePromises); 104 105 // Create a map of authorId to profile for quick lookup 106 const profileMap = new Map(); 107 profileResults.forEach((profileResult, index) => { 108 if (profileResult.isOk()) { 109 profileMap.set(authorIds[index], profileResult.value); 110 } 111 }); 112 113 // Transform raw data to enriched DTOs 114 const enrichedCollections: CollectionDTO[] = result.items 115 .map((item) => { 116 const profile = profileMap.get(item.authorId); 117 if (!profile) { 118 // Skip collections where we couldn't fetch the profile 119 return null; 120 } 121 122 return { 123 id: item.id, 124 uri: item.uri, 125 name: item.name, 126 description: item.description, 127 accessType: item.accessType as CollectionAccessType, 128 updatedAt: item.updatedAt.toISOString(), 129 createdAt: item.createdAt.toISOString(), 130 cardCount: item.cardCount, 131 author: { 132 id: profile.id, 133 name: profile.name, 134 handle: profile.handle, 135 avatarUrl: profile.avatarUrl, 136 description: profile.bio, 137 }, 138 }; 139 }) 140 .filter((item): item is NonNullable<typeof item> => item !== null); 141 142 return ok({ 143 collections: enrichedCollections, 144 pagination: { 145 currentPage: page, 146 totalPages: Math.ceil(result.totalCount / limit), 147 totalCount: result.totalCount, 148 hasMore: page * limit < result.totalCount, 149 limit, 150 }, 151 sorting: { 152 sortBy, 153 sortOrder, 154 }, 155 }); 156 } catch (error) { 157 return err( 158 new Error( 159 `Failed to search collections: ${error instanceof Error ? error.message : 'Unknown error'}`, 160 ), 161 ); 162 } 163 } 164}