A social knowledge tool for researchers built on ATProto
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 { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle';
10import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService';
11
12export interface GetCollectionsQuery {
13 curatorId: string;
14 page?: number;
15 limit?: number;
16 sortBy?: CollectionSortField;
17 sortOrder?: SortOrder;
18 searchText?: string;
19}
20
21// Enriched data for the final use case result
22export interface CollectionListItemDTO {
23 id: string;
24 uri?: string;
25 name: string;
26 description?: string;
27 updatedAt: Date;
28 createdAt: Date;
29 cardCount: number;
30 createdBy: {
31 id: string;
32 name: string;
33 handle: string;
34 avatarUrl?: string;
35 };
36}
37export interface GetCollectionsResult {
38 collections: CollectionListItemDTO[];
39 pagination: {
40 currentPage: number;
41 totalPages: number;
42 totalCount: number;
43 hasMore: boolean;
44 limit: number;
45 };
46 sorting: {
47 sortBy: CollectionSortField;
48 sortOrder: SortOrder;
49 };
50}
51
52export class ValidationError extends Error {
53 constructor(message: string) {
54 super(message);
55 this.name = 'ValidationError';
56 }
57}
58
59export class GetCollectionsUseCase
60 implements UseCase<GetCollectionsQuery, Result<GetCollectionsResult>>
61{
62 constructor(
63 private collectionQueryRepo: ICollectionQueryRepository,
64 private profileService: IProfileService,
65 private identityResolver: IIdentityResolutionService,
66 ) {}
67
68 async execute(
69 query: GetCollectionsQuery,
70 ): Promise<Result<GetCollectionsResult>> {
71 // Set defaults
72 const page = query.page || 1;
73 const limit = Math.min(query.limit || 20, 100); // Cap at 100
74 const sortBy = query.sortBy || CollectionSortField.UPDATED_AT;
75 const sortOrder = query.sortOrder || SortOrder.DESC;
76
77 // Parse and validate curator identifier
78 const identifierResult = DIDOrHandle.create(query.curatorId);
79 if (identifierResult.isErr()) {
80 return err(new ValidationError('Invalid curator identifier'));
81 }
82
83 // Resolve to DID
84 const didResult = await this.identityResolver.resolveToDID(
85 identifierResult.value,
86 );
87 if (didResult.isErr()) {
88 return err(
89 new ValidationError(
90 `Could not resolve curator identifier: ${didResult.error.message}`,
91 ),
92 );
93 }
94
95 const resolvedDid = didResult.value.value;
96
97 try {
98 // Execute query to get raw collection data using the resolved DID
99 const result = await this.collectionQueryRepo.findByCreator(resolvedDid, {
100 page,
101 limit,
102 sortBy,
103 sortOrder,
104 searchText: query.searchText,
105 });
106
107 // Get user profile for the curator using the resolved DID
108 const profileResult = await this.profileService.getProfile(resolvedDid);
109
110 if (profileResult.isErr()) {
111 return err(
112 new Error(
113 `Failed to fetch user profile: ${profileResult.error instanceof Error ? profileResult.error.message : 'Unknown error'}`,
114 ),
115 );
116 }
117 const profile = profileResult.value;
118
119 // Transform raw data to enriched DTOs
120 const enrichedCollections: CollectionListItemDTO[] = result.items.map(
121 (item) => {
122 return {
123 id: item.id,
124 uri: item.uri,
125 name: item.name,
126 description: item.description,
127 updatedAt: item.updatedAt,
128 createdAt: item.createdAt,
129 cardCount: item.cardCount,
130 createdBy: {
131 id: profile.id,
132 name: profile.name,
133 handle: profile.handle,
134 avatarUrl: profile.avatarUrl,
135 },
136 };
137 },
138 );
139
140 return ok({
141 collections: enrichedCollections,
142 pagination: {
143 currentPage: page,
144 totalPages: Math.ceil(result.totalCount / limit),
145 totalCount: result.totalCount,
146 hasMore: page * limit < result.totalCount,
147 limit,
148 },
149 sorting: {
150 sortBy,
151 sortOrder,
152 },
153 });
154 } catch (error) {
155 return err(
156 new Error(
157 `Failed to retrieve collections: ${error instanceof Error ? error.message : 'Unknown error'}`,
158 ),
159 );
160 }
161 }
162}