A social knowledge tool for researchers built on ATProto
1import { CollectionId } from 'src/modules/cards/domain/value-objects/CollectionId';
2import { err, ok, Result } from 'src/shared/core/Result';
3import { UseCase } from 'src/shared/core/UseCase';
4import {
5 ICardQueryRepository,
6 CardSortField,
7 SortOrder,
8 UrlCardView,
9} from '../../../domain/ICardQueryRepository';
10import { ICollectionRepository } from '../../../domain/ICollectionRepository';
11import { IProfileService } from '../../../domain/services/IProfileService';
12
13export interface GetCollectionPageQuery {
14 collectionId: string;
15 callerDid?: string;
16 page?: number;
17 limit?: number;
18 sortBy?: CardSortField;
19 sortOrder?: SortOrder;
20}
21
22export type CollectionPageUrlCardDTO = UrlCardView;
23export interface GetCollectionPageResult {
24 id: string;
25 uri?: string;
26 name: string;
27 description?: string;
28 author: {
29 id: string;
30 name: string;
31 handle: string;
32 avatarUrl?: string;
33 };
34 urlCards: CollectionPageUrlCardDTO[];
35 pagination: {
36 currentPage: number;
37 totalPages: number;
38 totalCount: number;
39 hasMore: boolean;
40 limit: number;
41 };
42 sorting: {
43 sortBy: CardSortField;
44 sortOrder: SortOrder;
45 };
46}
47
48export class ValidationError extends Error {
49 constructor(message: string) {
50 super(message);
51 this.name = 'ValidationError';
52 }
53}
54
55export class CollectionNotFoundError extends Error {
56 constructor(message: string) {
57 super(message);
58 this.name = 'CollectionNotFoundError';
59 }
60}
61
62export class GetCollectionPageUseCase
63 implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>>
64{
65 constructor(
66 private collectionRepo: ICollectionRepository,
67 private cardQueryRepo: ICardQueryRepository,
68 private profileService: IProfileService,
69 ) {}
70
71 async execute(
72 query: GetCollectionPageQuery,
73 ): Promise<Result<GetCollectionPageResult>> {
74 // Set defaults
75 const page = query.page || 1;
76 const limit = Math.min(query.limit || 20, 100); // Cap at 100
77 const sortBy = query.sortBy || CardSortField.UPDATED_AT;
78 const sortOrder = query.sortOrder || SortOrder.DESC;
79
80 // Validate collection ID
81 const collectionIdResult = CollectionId.createFromString(
82 query.collectionId,
83 );
84 if (collectionIdResult.isErr()) {
85 return err(new ValidationError('Invalid collection ID'));
86 }
87
88 try {
89 // Get the collection
90 const collectionResult = await this.collectionRepo.findById(
91 collectionIdResult.value,
92 );
93
94 if (collectionResult.isErr()) {
95 return err(
96 new Error(
97 `Failed to fetch collection: ${collectionResult.error instanceof Error ? collectionResult.error.message : 'Unknown error'}`,
98 ),
99 );
100 }
101
102 const collection = collectionResult.value;
103 if (!collection) {
104 return err(new CollectionNotFoundError('Collection not found'));
105 }
106
107 const collectionPublishedRecordId = collection.publishedRecordId;
108
109 const collectionUri = collectionPublishedRecordId?.uri;
110
111 // Get author profile
112 const profileResult = await this.profileService.getProfile(
113 collection.authorId.value,
114 query.callerDid,
115 );
116
117 if (profileResult.isErr()) {
118 return err(
119 new Error(
120 `Failed to fetch author profile: ${profileResult.error instanceof Error ? profileResult.error.message : 'Unknown error'}`,
121 ),
122 );
123 }
124
125 const authorProfile = profileResult.value;
126
127 // Get cards in the collection
128 const cardsResult = await this.cardQueryRepo.getCardsInCollection(
129 query.collectionId,
130 {
131 page,
132 limit,
133 sortBy,
134 sortOrder,
135 },
136 );
137
138 // Transform raw card data to enriched DTOs
139 const enrichedCards: CollectionPageUrlCardDTO[] = cardsResult.items;
140
141 return ok({
142 id: collection.collectionId.getStringValue(),
143 uri: collectionUri,
144 name: collection.name.value,
145 description: collection.description?.value,
146 author: {
147 id: authorProfile.id,
148 name: authorProfile.name,
149 handle: authorProfile.handle,
150 avatarUrl: authorProfile.avatarUrl,
151 },
152 urlCards: enrichedCards,
153 pagination: {
154 currentPage: page,
155 totalPages: Math.ceil(cardsResult.totalCount / limit),
156 totalCount: cardsResult.totalCount,
157 hasMore: page * limit < cardsResult.totalCount,
158 limit,
159 },
160 sorting: {
161 sortBy,
162 sortOrder,
163 },
164 });
165 } catch (error) {
166 return err(
167 new Error(
168 `Failed to retrieve collection page: ${error instanceof Error ? error.message : 'Unknown error'}`,
169 ),
170 );
171 }
172 }
173}