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