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';
19
20export interface AddUrlToLibraryDTO {
21 url: string;
22 note?: string;
23 collectionIds?: string[];
24 curatorId: string;
25}
26
27export interface AddUrlToLibraryResponseDTO {
28 urlCardId: string;
29 noteCardId?: string;
30}
31
32export class ValidationError extends UseCaseError {
33 constructor(message: string) {
34 super(message);
35 }
36}
37
38export class AddUrlToLibraryUseCase extends BaseUseCase<
39 AddUrlToLibraryDTO,
40 Result<AddUrlToLibraryResponseDTO, ValidationError | AppError.UnexpectedError>
41> {
42 constructor(
43 private cardRepository: ICardRepository,
44 private metadataService: IMetadataService,
45 private cardLibraryService: CardLibraryService,
46 private cardCollectionService: CardCollectionService,
47 eventPublisher: IEventPublisher,
48 ) {
49 super(eventPublisher);
50 }
51
52 async execute(
53 request: AddUrlToLibraryDTO,
54 ): Promise<
55 Result<
56 AddUrlToLibraryResponseDTO,
57 ValidationError | AppError.UnexpectedError
58 >
59 > {
60 try {
61 // Validate and create CuratorId
62 const curatorIdResult = CuratorId.create(request.curatorId);
63 if (curatorIdResult.isErr()) {
64 return err(
65 new ValidationError(
66 `Invalid curator ID: ${curatorIdResult.error.message}`,
67 ),
68 );
69 }
70 const curatorId = curatorIdResult.value;
71
72 // Validate URL
73 const urlResult = URL.create(request.url);
74 if (urlResult.isErr()) {
75 return err(
76 new ValidationError(`Invalid URL: ${urlResult.error.message}`),
77 );
78 }
79 const url = urlResult.value;
80
81 // Check if URL card already exists
82 const existingUrlCardResult =
83 await this.cardRepository.findUsersUrlCardByUrl(url, curatorId);
84 if (existingUrlCardResult.isErr()) {
85 return err(
86 AppError.UnexpectedError.create(existingUrlCardResult.error),
87 );
88 }
89
90 let urlCard = existingUrlCardResult.value;
91 if (!urlCard) {
92 // Fetch metadata for URL
93 const metadataResult = await this.metadataService.fetchMetadata(url);
94 if (metadataResult.isErr()) {
95 return err(
96 new ValidationError(
97 `Failed to fetch metadata: ${metadataResult.error.message}`,
98 ),
99 );
100 }
101
102 // Create URL card
103 const urlCardInput: IUrlCardInput = {
104 type: CardTypeEnum.URL,
105 url: request.url,
106 metadata: metadataResult.value,
107 };
108
109 const urlCardResult = CardFactory.create({
110 curatorId: request.curatorId,
111 cardInput: urlCardInput,
112 });
113
114 if (urlCardResult.isErr()) {
115 return err(new ValidationError(urlCardResult.error.message));
116 }
117
118 urlCard = urlCardResult.value;
119
120 // Save URL card
121 const saveUrlCardResult = await this.cardRepository.save(urlCard);
122 if (saveUrlCardResult.isErr()) {
123 return err(AppError.UnexpectedError.create(saveUrlCardResult.error));
124 }
125 }
126
127 // Add URL card to library using domain service
128 const addUrlCardToLibraryResult =
129 await this.cardLibraryService.addCardToLibrary(urlCard, curatorId);
130 if (addUrlCardToLibraryResult.isErr()) {
131 if (
132 addUrlCardToLibraryResult.error instanceof AppError.UnexpectedError
133 ) {
134 return err(addUrlCardToLibraryResult.error);
135 }
136 return err(
137 new ValidationError(addUrlCardToLibraryResult.error.message),
138 );
139 }
140
141 // Update urlCard reference to the one returned by the service
142 urlCard = addUrlCardToLibraryResult.value;
143
144 let noteCard;
145
146 // Create note card if note is provided
147 if (request.note) {
148 const noteCardInput: INoteCardInput = {
149 type: CardTypeEnum.NOTE,
150 text: request.note,
151 parentCardId: urlCard.cardId.getStringValue(),
152 url: request.url,
153 };
154
155 const noteCardResult = CardFactory.create({
156 curatorId: request.curatorId,
157 cardInput: noteCardInput,
158 });
159
160 if (noteCardResult.isErr()) {
161 return err(new ValidationError(noteCardResult.error.message));
162 }
163
164 noteCard = noteCardResult.value;
165
166 // Save note card
167 const saveNoteCardResult = await this.cardRepository.save(noteCard);
168 if (saveNoteCardResult.isErr()) {
169 return err(AppError.UnexpectedError.create(saveNoteCardResult.error));
170 }
171
172 // Add note card to library using domain service
173 const addNoteCardToLibraryResult =
174 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId);
175 if (addNoteCardToLibraryResult.isErr()) {
176 if (
177 addNoteCardToLibraryResult.error instanceof AppError.UnexpectedError
178 ) {
179 return err(addNoteCardToLibraryResult.error);
180 }
181 return err(
182 new ValidationError(addNoteCardToLibraryResult.error.message),
183 );
184 }
185
186 // Update noteCard reference to the one returned by the service
187 noteCard = addNoteCardToLibraryResult.value;
188 }
189
190 // Handle collection additions if specified
191 if (request.collectionIds && request.collectionIds.length > 0) {
192 // Always add the URL card to collections
193 const cardToAdd = urlCard;
194
195 // Validate and create CollectionIds
196 const collectionIds: CollectionId[] = [];
197 for (const collectionIdStr of request.collectionIds) {
198 const collectionIdResult =
199 CollectionId.createFromString(collectionIdStr);
200 if (collectionIdResult.isErr()) {
201 return err(
202 new ValidationError(
203 `Invalid collection ID: ${collectionIdResult.error.message}`,
204 ),
205 );
206 }
207 collectionIds.push(collectionIdResult.value);
208 }
209
210 // Add card to collections using domain service
211 const addToCollectionsResult =
212 await this.cardCollectionService.addCardToCollections(
213 cardToAdd,
214 collectionIds,
215 curatorId,
216 );
217 if (addToCollectionsResult.isErr()) {
218 if (
219 addToCollectionsResult.error instanceof AppError.UnexpectedError
220 ) {
221 return err(addToCollectionsResult.error);
222 }
223 return err(new ValidationError(addToCollectionsResult.error.message));
224 }
225
226 // Publish events for all affected collections
227 const updatedCollections = addToCollectionsResult.value;
228 for (const collection of updatedCollections) {
229 const publishResult =
230 await this.publishEventsForAggregate(collection);
231 if (publishResult.isErr()) {
232 console.error(
233 'Failed to publish events for collection:',
234 publishResult.error,
235 );
236 // Don't fail the operation if event publishing fails
237 }
238 }
239 }
240
241 // Publish events for URL card (events are raised in addToLibrary method)
242 const publishUrlCardResult =
243 await this.publishEventsForAggregate(urlCard);
244 if (publishUrlCardResult.isErr()) {
245 console.error(
246 'Failed to publish events for URL card:',
247 publishUrlCardResult.error,
248 );
249 // Don't fail the operation if event publishing fails
250 }
251
252 return ok({
253 urlCardId: urlCard.cardId.getStringValue(),
254 noteCardId: noteCard?.cardId.getStringValue(),
255 });
256 } catch (error) {
257 return err(AppError.UnexpectedError.create(error));
258 }
259 }
260}