This repository has no description
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 { INoteCardInput } from '../../../domain/CardFactory';
8import { CardId } from '../../../domain/value-objects/CardId';
9import { CollectionId } from '../../../domain/value-objects/CollectionId';
10import { CuratorId } from '../../../domain/value-objects/CuratorId';
11import { CardTypeEnum } from '../../../domain/value-objects/CardType';
12import { URL } from '../../../domain/value-objects/URL';
13import { CardCollectionService } from '../../../domain/services/CardCollectionService';
14import { CardContent } from '../../../domain/value-objects/CardContent';
15import { CardFactory } from '../../../domain/CardFactory';
16import { CardLibraryService } from '../../../domain/services/CardLibraryService';
17import { AuthenticationError } from '../../../../../shared/core/AuthenticationError';
18
19export interface UpdateUrlCardAssociationsDTO {
20 cardId: string;
21 curatorId: string;
22 note?: string;
23 addToCollections?: string[];
24 removeFromCollections?: string[];
25}
26
27export interface UpdateUrlCardAssociationsResponseDTO {
28 urlCardId: string;
29 noteCardId?: string;
30 addedToCollections: string[];
31 removedFromCollections: string[];
32}
33
34export class ValidationError extends UseCaseError {
35 constructor(message: string) {
36 super(message);
37 }
38}
39
40export class UpdateUrlCardAssociationsUseCase extends BaseUseCase<
41 UpdateUrlCardAssociationsDTO,
42 Result<
43 UpdateUrlCardAssociationsResponseDTO,
44 ValidationError | AuthenticationError | AppError.UnexpectedError
45 >
46> {
47 constructor(
48 private cardRepository: ICardRepository,
49 private cardLibraryService: CardLibraryService,
50 private cardCollectionService: CardCollectionService,
51 eventPublisher: IEventPublisher,
52 ) {
53 super(eventPublisher);
54 }
55
56 async execute(
57 request: UpdateUrlCardAssociationsDTO,
58 ): Promise<
59 Result<
60 UpdateUrlCardAssociationsResponseDTO,
61 ValidationError | AuthenticationError | AppError.UnexpectedError
62 >
63 > {
64 try {
65 // Validate and create CuratorId
66 const curatorIdResult = CuratorId.create(request.curatorId);
67 if (curatorIdResult.isErr()) {
68 return err(
69 new ValidationError(
70 `Invalid curator ID: ${curatorIdResult.error.message}`,
71 ),
72 );
73 }
74 const curatorId = curatorIdResult.value;
75
76 // Validate and create CardId
77 const cardIdResult = CardId.createFromString(request.cardId);
78 if (cardIdResult.isErr()) {
79 return err(
80 new ValidationError(`Invalid card ID: ${cardIdResult.error.message}`),
81 );
82 }
83 const cardId = cardIdResult.value;
84
85 // Find the URL card - it must already exist
86 const existingUrlCardResult = await this.cardRepository.findById(cardId);
87 if (existingUrlCardResult.isErr()) {
88 return err(
89 AppError.UnexpectedError.create(existingUrlCardResult.error),
90 );
91 }
92
93 const urlCard = existingUrlCardResult.value;
94 if (!urlCard) {
95 return err(
96 new ValidationError(
97 'URL card not found. Please add the URL to your library first.',
98 ),
99 );
100 }
101
102 // Verify it's a URL card
103 if (!urlCard.isUrlCard) {
104 return err(
105 new ValidationError(
106 'Card must be a URL card to update associations.',
107 ),
108 );
109 }
110
111 // Verify ownership
112 if (!urlCard.curatorId.equals(curatorId)) {
113 return err(
114 new ValidationError(
115 'You do not have permission to update this card.',
116 ),
117 );
118 }
119
120 // Get the URL from the card for note operations
121 if (!urlCard.url) {
122 return err(new ValidationError('URL card must have a URL property.'));
123 }
124 const url = urlCard.url;
125
126 let noteCard;
127
128 // Handle note updates/creation
129 if (request.note !== undefined) {
130 // Check if note card already exists
131 const existingNoteCardResult =
132 await this.cardRepository.findUsersNoteCardByUrl(url, curatorId);
133 if (existingNoteCardResult.isErr()) {
134 return err(
135 AppError.UnexpectedError.create(existingNoteCardResult.error),
136 );
137 }
138
139 noteCard = existingNoteCardResult.value;
140
141 if (noteCard) {
142 // Update existing note card
143 const newContentResult = CardContent.createNoteContent(request.note);
144 if (newContentResult.isErr()) {
145 return err(new ValidationError(newContentResult.error.message));
146 }
147
148 const updateContentResult = noteCard.updateContent(
149 newContentResult.value,
150 );
151 if (updateContentResult.isErr()) {
152 return err(new ValidationError(updateContentResult.error.message));
153 }
154
155 // Save updated note card
156 const saveNoteCardResult = await this.cardRepository.save(noteCard);
157 if (saveNoteCardResult.isErr()) {
158 return err(
159 AppError.UnexpectedError.create(saveNoteCardResult.error),
160 );
161 }
162 } else {
163 // Create new note card
164 const noteCardInput: INoteCardInput = {
165 type: CardTypeEnum.NOTE,
166 text: request.note,
167 parentCardId: urlCard.cardId.getStringValue(),
168 url: url.value,
169 };
170
171 const noteCardResult = CardFactory.create({
172 curatorId: request.curatorId,
173 cardInput: noteCardInput,
174 });
175
176 if (noteCardResult.isErr()) {
177 return err(new ValidationError(noteCardResult.error.message));
178 }
179
180 noteCard = noteCardResult.value;
181
182 // Save 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
190 // Add note card to library using domain service
191 const addNoteCardToLibraryResult =
192 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId);
193 if (addNoteCardToLibraryResult.isErr()) {
194 // Propagate authentication errors
195 if (
196 addNoteCardToLibraryResult.error instanceof AuthenticationError
197 ) {
198 return err(addNoteCardToLibraryResult.error);
199 }
200 if (
201 addNoteCardToLibraryResult.error instanceof
202 AppError.UnexpectedError
203 ) {
204 return err(addNoteCardToLibraryResult.error);
205 }
206 return err(
207 new ValidationError(addNoteCardToLibraryResult.error.message),
208 );
209 }
210
211 // Update noteCard reference to the one returned by the service
212 noteCard = addNoteCardToLibraryResult.value;
213 }
214 }
215
216 const addedToCollections: string[] = [];
217 const removedFromCollections: string[] = [];
218
219 // Handle adding to collections
220 if (request.addToCollections && request.addToCollections.length > 0) {
221 const collectionIds: CollectionId[] = [];
222 for (const collectionIdStr of request.addToCollections) {
223 const collectionIdResult =
224 CollectionId.createFromString(collectionIdStr);
225 if (collectionIdResult.isErr()) {
226 return err(
227 new ValidationError(
228 `Invalid collection ID: ${collectionIdResult.error.message}`,
229 ),
230 );
231 }
232 collectionIds.push(collectionIdResult.value);
233 }
234
235 const addToCollectionsResult =
236 await this.cardCollectionService.addCardToCollections(
237 urlCard,
238 collectionIds,
239 curatorId,
240 );
241 if (addToCollectionsResult.isErr()) {
242 // Propagate authentication errors
243 if (addToCollectionsResult.error instanceof AuthenticationError) {
244 return err(addToCollectionsResult.error);
245 }
246 if (
247 addToCollectionsResult.error instanceof AppError.UnexpectedError
248 ) {
249 return err(addToCollectionsResult.error);
250 }
251 return err(new ValidationError(addToCollectionsResult.error.message));
252 }
253
254 // Publish events for all affected collections
255 const updatedCollections = addToCollectionsResult.value;
256 for (const collection of updatedCollections) {
257 const publishResult =
258 await this.publishEventsForAggregate(collection);
259 if (publishResult.isErr()) {
260 console.error(
261 'Failed to publish events for collection:',
262 publishResult.error,
263 );
264 // Don't fail the operation if event publishing fails
265 }
266 addedToCollections.push(collection.collectionId.getStringValue());
267 }
268 }
269
270 // Handle removing from collections
271 if (
272 request.removeFromCollections &&
273 request.removeFromCollections.length > 0
274 ) {
275 const collectionIds: CollectionId[] = [];
276 for (const collectionIdStr of request.removeFromCollections) {
277 const collectionIdResult =
278 CollectionId.createFromString(collectionIdStr);
279 if (collectionIdResult.isErr()) {
280 return err(
281 new ValidationError(
282 `Invalid collection ID: ${collectionIdResult.error.message}`,
283 ),
284 );
285 }
286 collectionIds.push(collectionIdResult.value);
287 }
288
289 const removeFromCollectionsResult =
290 await this.cardCollectionService.removeCardFromCollections(
291 urlCard,
292 collectionIds,
293 curatorId,
294 );
295 if (removeFromCollectionsResult.isErr()) {
296 // Propagate authentication errors
297 if (
298 removeFromCollectionsResult.error instanceof AuthenticationError
299 ) {
300 return err(removeFromCollectionsResult.error);
301 }
302 if (
303 removeFromCollectionsResult.error instanceof
304 AppError.UnexpectedError
305 ) {
306 return err(removeFromCollectionsResult.error);
307 }
308 return err(
309 new ValidationError(removeFromCollectionsResult.error.message),
310 );
311 }
312
313 // Publish events for all affected collections
314 const updatedCollections = removeFromCollectionsResult.value;
315 for (const collection of updatedCollections) {
316 const publishResult =
317 await this.publishEventsForAggregate(collection);
318 if (publishResult.isErr()) {
319 console.error(
320 'Failed to publish events for collection:',
321 publishResult.error,
322 );
323 // Don't fail the operation if event publishing fails
324 }
325 removedFromCollections.push(collection.collectionId.getStringValue());
326 }
327 }
328
329 return ok({
330 urlCardId: urlCard.cardId.getStringValue(),
331 noteCardId: noteCard?.cardId.getStringValue(),
332 addedToCollections,
333 removedFromCollections,
334 });
335 } catch (error) {
336 return err(AppError.UnexpectedError.create(error));
337 }
338 }
339}