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