This repository has no description
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}