A social knowledge tool for researchers built on ATProto
1import { Result, ok, err } from '../../../../../shared/core/Result';
2import { BaseUseCase } from '../../../../../shared/core/UseCase';
3import { UseCaseError } from '../../../../../shared/core/UseCaseError';
4import { AppError } from '../../../../../shared/core/AppError';
5import { IEventPublisher } from '../../../../../shared/application/events/IEventPublisher';
6import { ICardRepository } from '../../../domain/ICardRepository';
7import {
8 CardFactory,
9 IUrlCardInput,
10 INoteCardInput,
11} from '../../../domain/CardFactory';
12import { CollectionId } from '../../../domain/value-objects/CollectionId';
13import { CuratorId } from '../../../domain/value-objects/CuratorId';
14import { IMetadataService } from '../../../domain/services/IMetadataService';
15import { CardTypeEnum } from '../../../domain/value-objects/CardType';
16import { URL } from '../../../domain/value-objects/URL';
17import { CardLibraryService } from '../../../domain/services/CardLibraryService';
18import { CardCollectionService } from '../../../domain/services/CardCollectionService';
19import { CardContent } from '../../../domain/value-objects/CardContent';
20import { AuthenticationError } from '../../../../../shared/core/AuthenticationError';
21
22export interface AddUrlToLibraryDTO {
23 url: string;
24 note?: string;
25 collectionIds?: string[];
26 curatorId: string;
27}
28
29export interface AddUrlToLibraryResponseDTO {
30 urlCardId: string;
31 noteCardId?: string;
32}
33
34export class ValidationError extends UseCaseError {
35 constructor(message: string) {
36 super(message);
37 }
38}
39
40export class AddUrlToLibraryUseCase extends BaseUseCase<
41 AddUrlToLibraryDTO,
42 Result<
43 AddUrlToLibraryResponseDTO,
44 ValidationError | AuthenticationError | AppError.UnexpectedError
45 >
46> {
47 constructor(
48 private cardRepository: ICardRepository,
49 private metadataService: IMetadataService,
50 private cardLibraryService: CardLibraryService,
51 private cardCollectionService: CardCollectionService,
52 eventPublisher: IEventPublisher,
53 ) {
54 super(eventPublisher);
55 }
56
57 async execute(
58 request: AddUrlToLibraryDTO,
59 ): Promise<
60 Result<
61 AddUrlToLibraryResponseDTO,
62 ValidationError | AuthenticationError | AppError.UnexpectedError
63 >
64 > {
65 try {
66 // Validate and create CuratorId
67 const curatorIdResult = CuratorId.create(request.curatorId);
68 if (curatorIdResult.isErr()) {
69 return err(
70 new ValidationError(
71 `Invalid curator ID: ${curatorIdResult.error.message}`,
72 ),
73 );
74 }
75 const curatorId = curatorIdResult.value;
76
77 // Validate URL
78 const urlResult = URL.create(request.url);
79 if (urlResult.isErr()) {
80 return err(
81 new ValidationError(`Invalid URL: ${urlResult.error.message}`),
82 );
83 }
84 const url = urlResult.value;
85
86 // Check if URL card already exists
87 const existingUrlCardResult =
88 await this.cardRepository.findUsersUrlCardByUrl(url, curatorId);
89 if (existingUrlCardResult.isErr()) {
90 return err(
91 AppError.UnexpectedError.create(existingUrlCardResult.error),
92 );
93 }
94
95 let urlCard = existingUrlCardResult.value;
96 if (!urlCard) {
97 // Fetch metadata for URL
98 const metadataResult = await this.metadataService.fetchMetadata(url);
99 if (metadataResult.isErr()) {
100 return err(
101 new ValidationError(
102 `Failed to fetch metadata: ${metadataResult.error.message}`,
103 ),
104 );
105 }
106
107 // Create URL card
108 const urlCardInput: IUrlCardInput = {
109 type: CardTypeEnum.URL,
110 url: url.value,
111 metadata: metadataResult.value,
112 };
113
114 const urlCardResult = CardFactory.create({
115 curatorId: request.curatorId,
116 cardInput: urlCardInput,
117 });
118
119 if (urlCardResult.isErr()) {
120 return err(new ValidationError(urlCardResult.error.message));
121 }
122
123 urlCard = urlCardResult.value;
124
125 // Save URL card
126 const saveUrlCardResult = await this.cardRepository.save(urlCard);
127 if (saveUrlCardResult.isErr()) {
128 return err(AppError.UnexpectedError.create(saveUrlCardResult.error));
129 }
130 }
131
132 // Add URL card to library using domain service
133 const addUrlCardToLibraryResult =
134 await this.cardLibraryService.addCardToLibrary(urlCard, curatorId);
135 if (addUrlCardToLibraryResult.isErr()) {
136 // Propagate authentication errors
137 if (addUrlCardToLibraryResult.error instanceof AuthenticationError) {
138 return err(addUrlCardToLibraryResult.error);
139 }
140 if (
141 addUrlCardToLibraryResult.error instanceof AppError.UnexpectedError
142 ) {
143 return err(addUrlCardToLibraryResult.error);
144 }
145 return err(
146 new ValidationError(addUrlCardToLibraryResult.error.message),
147 );
148 }
149
150 // Update urlCard reference to the one returned by the service
151 urlCard = addUrlCardToLibraryResult.value;
152
153 let noteCard;
154
155 // Handle note card creation or update if note is provided
156 if (request.note) {
157 // Check if note card already exists for this URL and curator
158 const existingNoteCardResult =
159 await this.cardRepository.findUsersNoteCardByUrl(url, curatorId);
160 if (existingNoteCardResult.isErr()) {
161 return err(
162 AppError.UnexpectedError.create(existingNoteCardResult.error),
163 );
164 }
165
166 noteCard = existingNoteCardResult.value;
167
168 if (noteCard) {
169 // Update existing note card
170 const newContentResult = CardContent.createNoteContent(request.note);
171 if (newContentResult.isErr()) {
172 return err(new ValidationError(newContentResult.error.message));
173 }
174
175 const updateContentResult = noteCard.updateContent(
176 newContentResult.value,
177 );
178 if (updateContentResult.isErr()) {
179 return err(new ValidationError(updateContentResult.error.message));
180 }
181
182 // Save updated note card
183 const saveNoteCardResult = await this.cardRepository.save(noteCard);
184 if (saveNoteCardResult.isErr()) {
185 return err(
186 AppError.UnexpectedError.create(saveNoteCardResult.error),
187 );
188 }
189 } else {
190 // Create new note card
191 const noteCardInput: INoteCardInput = {
192 type: CardTypeEnum.NOTE,
193 text: request.note,
194 parentCardId: urlCard.cardId.getStringValue(),
195 url: url.value,
196 };
197
198 const noteCardResult = CardFactory.create({
199 curatorId: request.curatorId,
200 cardInput: noteCardInput,
201 });
202
203 if (noteCardResult.isErr()) {
204 return err(new ValidationError(noteCardResult.error.message));
205 }
206
207 noteCard = noteCardResult.value;
208
209 // Save note card
210 const saveNoteCardResult = await this.cardRepository.save(noteCard);
211 if (saveNoteCardResult.isErr()) {
212 return err(
213 AppError.UnexpectedError.create(saveNoteCardResult.error),
214 );
215 }
216
217 // Add note card to library using domain service
218 const addNoteCardToLibraryResult =
219 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId);
220 if (addNoteCardToLibraryResult.isErr()) {
221 // Propagate authentication errors
222 if (
223 addNoteCardToLibraryResult.error instanceof AuthenticationError
224 ) {
225 return err(addNoteCardToLibraryResult.error);
226 }
227 if (
228 addNoteCardToLibraryResult.error instanceof
229 AppError.UnexpectedError
230 ) {
231 return err(addNoteCardToLibraryResult.error);
232 }
233 return err(
234 new ValidationError(addNoteCardToLibraryResult.error.message),
235 );
236 }
237
238 // Update noteCard reference to the one returned by the service
239 noteCard = addNoteCardToLibraryResult.value;
240 }
241 }
242
243 // Handle collection additions if specified
244 if (request.collectionIds && request.collectionIds.length > 0) {
245 // Always add the URL card to collections
246 const cardToAdd = urlCard;
247
248 // Validate and create CollectionIds
249 const collectionIds: CollectionId[] = [];
250 for (const collectionIdStr of request.collectionIds) {
251 const collectionIdResult =
252 CollectionId.createFromString(collectionIdStr);
253 if (collectionIdResult.isErr()) {
254 return err(
255 new ValidationError(
256 `Invalid collection ID: ${collectionIdResult.error.message}`,
257 ),
258 );
259 }
260 collectionIds.push(collectionIdResult.value);
261 }
262
263 // Add card to collections using domain service
264 const addToCollectionsResult =
265 await this.cardCollectionService.addCardToCollections(
266 cardToAdd,
267 collectionIds,
268 curatorId,
269 );
270 if (addToCollectionsResult.isErr()) {
271 // Propagate authentication errors
272 if (addToCollectionsResult.error instanceof AuthenticationError) {
273 return err(addToCollectionsResult.error);
274 }
275 if (
276 addToCollectionsResult.error instanceof AppError.UnexpectedError
277 ) {
278 return err(addToCollectionsResult.error);
279 }
280 return err(new ValidationError(addToCollectionsResult.error.message));
281 }
282
283 // Publish events for all affected collections
284 const updatedCollections = addToCollectionsResult.value;
285 for (const collection of updatedCollections) {
286 const publishResult =
287 await this.publishEventsForAggregate(collection);
288 if (publishResult.isErr()) {
289 console.error(
290 'Failed to publish events for collection:',
291 publishResult.error,
292 );
293 // Don't fail the operation if event publishing fails
294 }
295 }
296 }
297
298 // Publish events for URL card (events are raised in addToLibrary method)
299 const publishUrlCardResult =
300 await this.publishEventsForAggregate(urlCard);
301 if (publishUrlCardResult.isErr()) {
302 console.error(
303 'Failed to publish events for URL card:',
304 publishUrlCardResult.error,
305 );
306 // Don't fail the operation if event publishing fails
307 }
308
309 return ok({
310 urlCardId: urlCard.cardId.getStringValue(),
311 noteCardId: noteCard?.cardId.getStringValue(),
312 });
313 } catch (error) {
314 return err(AppError.UnexpectedError.create(error));
315 }
316 }
317}