A social knowledge tool for researchers built on ATProto
1import {
2 ICollectionQueryRepository,
3 CollectionQueryOptions,
4 CollectionQueryResultDTO,
5 CollectionContainingCardDTO,
6 CollectionForUrlDTO,
7 PaginatedQueryResult,
8 CollectionSortField,
9 SortOrder,
10 CollectionForUrlQueryOptions,
11} from '../../domain/ICollectionQueryRepository';
12import { Collection } from '../../domain/Collection';
13import { InMemoryCollectionRepository } from './InMemoryCollectionRepository';
14import { InMemoryCardRepository } from './InMemoryCardRepository';
15
16export class InMemoryCollectionQueryRepository
17 implements ICollectionQueryRepository
18{
19 constructor(
20 private collectionRepository: InMemoryCollectionRepository,
21 private cardRepository?: InMemoryCardRepository,
22 ) {}
23
24 async findByCreator(
25 curatorId: string,
26 options: CollectionQueryOptions,
27 ): Promise<PaginatedQueryResult<CollectionQueryResultDTO>> {
28 try {
29 const allCollections = this.collectionRepository.getAllCollections();
30 let creatorCollections = allCollections.filter(
31 (collection) => collection.authorId.value === curatorId,
32 );
33
34 if (options.searchText && options.searchText.trim()) {
35 const searchTerm = options.searchText.trim().toLowerCase();
36 creatorCollections = creatorCollections.filter((collection) => {
37 const nameMatch = collection.name.value
38 .toLowerCase()
39 .includes(searchTerm);
40 const descriptionMatch =
41 collection.description?.value.toLowerCase().includes(searchTerm) ||
42 false;
43 return nameMatch || descriptionMatch;
44 });
45 }
46
47 const sortedCollections = this.sortCollections(
48 creatorCollections,
49 options.sortBy,
50 options.sortOrder,
51 );
52
53 const startIndex = (options.page - 1) * options.limit;
54 const endIndex = startIndex + options.limit;
55 const paginatedCollections = sortedCollections.slice(
56 startIndex,
57 endIndex,
58 );
59
60 const items: CollectionQueryResultDTO[] = paginatedCollections.map(
61 (collection) => {
62 const collectionPublishedRecordId = collection.publishedRecordId;
63 return {
64 id: collection.collectionId.getStringValue(),
65 uri: collectionPublishedRecordId?.uri,
66 authorId: collection.authorId.value,
67 name: collection.name.value,
68 description: collection.description?.value,
69 accessType: collection.accessType,
70 cardCount: collection.cardCount,
71 createdAt: collection.createdAt,
72 updatedAt: collection.updatedAt,
73 };
74 },
75 );
76
77 return {
78 items,
79 totalCount: creatorCollections.length,
80 hasMore: endIndex < creatorCollections.length,
81 };
82 } catch (error) {
83 throw new Error(
84 `Failed to query collections: ${error instanceof Error ? error.message : String(error)}`,
85 );
86 }
87 }
88
89 private sortCollections(
90 collections: Collection[],
91 sortBy: CollectionSortField,
92 sortOrder: SortOrder,
93 ): Collection[] {
94 const sorted = [...collections].sort((a, b) => {
95 let comparison = 0;
96
97 switch (sortBy) {
98 case CollectionSortField.NAME:
99 comparison = a.name.value.localeCompare(b.name.value);
100 break;
101 case CollectionSortField.CREATED_AT:
102 comparison = a.createdAt.getTime() - b.createdAt.getTime();
103 break;
104 case CollectionSortField.UPDATED_AT:
105 comparison = a.updatedAt.getTime() - b.updatedAt.getTime();
106 break;
107 case CollectionSortField.CARD_COUNT:
108 comparison = a.cardCount - b.cardCount;
109 break;
110 default:
111 comparison = 0;
112 }
113
114 return sortOrder === SortOrder.DESC ? -comparison : comparison;
115 });
116
117 return sorted;
118 }
119
120 async getCollectionsContainingCardForUser(
121 cardId: string,
122 curatorId: string,
123 ): Promise<CollectionContainingCardDTO[]> {
124 try {
125 const allCollections = this.collectionRepository.getAllCollections();
126 const creatorCollections = allCollections.filter(
127 (collection) => collection.authorId.value === curatorId,
128 );
129
130 const collectionsWithCard = creatorCollections.filter((collection) =>
131 collection.cardLinks.some(
132 (link) => link.cardId.getStringValue() === cardId,
133 ),
134 );
135
136 const result: CollectionContainingCardDTO[] = collectionsWithCard.map(
137 (collection) => {
138 const collectionPublishedRecordId = collection.publishedRecordId;
139 return {
140 id: collection.collectionId.getStringValue(),
141 uri: collectionPublishedRecordId?.uri,
142 name: collection.name.value,
143 description: collection.description?.value,
144 };
145 },
146 );
147
148 return result;
149 } catch (error) {
150 throw new Error(
151 `Failed to get collections containing card: ${error instanceof Error ? error.message : String(error)}`,
152 );
153 }
154 }
155
156 async getCollectionsWithUrl(
157 url: string,
158 options: CollectionForUrlQueryOptions,
159 ): Promise<PaginatedQueryResult<CollectionForUrlDTO>> {
160 try {
161 if (!this.cardRepository) {
162 throw new Error(
163 'Card repository is required for getCollectionsWithUrl',
164 );
165 }
166
167 const allCards = this.cardRepository.getAllCards();
168 const cardsWithUrl = allCards.filter(
169 (card) => card.isUrlCard && card.url?.value === url,
170 );
171
172 const cardIds = new Set(
173 cardsWithUrl.map((card) => card.cardId.getStringValue()),
174 );
175
176 const allCollections = this.collectionRepository.getAllCollections();
177 const collectionsWithUrl = allCollections.filter((collection) =>
178 collection.cardLinks.some((link) =>
179 cardIds.has(link.cardId.getStringValue()),
180 ),
181 );
182
183 // Sort collections
184 const sortedCollections = this.sortCollections(
185 collectionsWithUrl,
186 options.sortBy,
187 options.sortOrder,
188 );
189
190 // Apply pagination
191 const { page, limit } = options;
192 const startIndex = (page - 1) * limit;
193 const endIndex = startIndex + limit;
194 const paginatedCollections = sortedCollections.slice(
195 startIndex,
196 endIndex,
197 );
198
199 const items: CollectionForUrlDTO[] = paginatedCollections.map(
200 (collection) => {
201 const collectionPublishedRecordId = collection.publishedRecordId;
202 return {
203 id: collection.collectionId.getStringValue(),
204 uri: collectionPublishedRecordId?.uri,
205 name: collection.name.value,
206 description: collection.description?.value,
207 authorId: collection.authorId.value,
208 };
209 },
210 );
211
212 return {
213 items,
214 totalCount: sortedCollections.length,
215 hasMore: endIndex < sortedCollections.length,
216 };
217 } catch (error) {
218 throw new Error(
219 `Failed to get collections with URL: ${error instanceof Error ? error.message : String(error)}`,
220 );
221 }
222 }
223
224 clear(): void {
225 // No separate state to clear
226 }
227}