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 = request.context === OperationContext.FIREHOSE_EVENT;
168 const noteCardOptions = isFirehoseEvent && request.publishedRecordIds?.noteCard ? {
169 skipPublishing: true,
170 publishedRecordId: request.publishedRecordIds.noteCard,
171 } : undefined;
172
173 // Update note card in library (handles save and republish)
174 const updateNoteResult =
175 await this.cardLibraryService.updateCardInLibrary(noteCard, curatorId, noteCardOptions);
176 if (updateNoteResult.isErr()) {
177 // Propagate authentication errors
178 if (
179 updateNoteResult.error instanceof AuthenticationError
180 ) {
181 return err(updateNoteResult.error);
182 }
183 if (
184 updateNoteResult.error instanceof AppError.UnexpectedError
185 ) {
186 return err(updateNoteResult.error);
187 }
188 return err(
189 new ValidationError(updateNoteResult.error.message),
190 );
191 }
192
193 // Update noteCard reference to the one returned by the service
194 noteCard = updateNoteResult.value;
195 } else {
196 // Create new note card
197 const noteCardInput: INoteCardInput = {
198 type: CardTypeEnum.NOTE,
199 text: request.note,
200 parentCardId: urlCard.cardId.getStringValue(),
201 url: url.value,
202 };
203
204 const noteCardResult = CardFactory.create({
205 curatorId: request.curatorId,
206 cardInput: noteCardInput,
207 });
208
209 if (noteCardResult.isErr()) {
210 return err(new ValidationError(noteCardResult.error.message));
211 }
212
213 noteCard = noteCardResult.value;
214
215 // Save note card
216 const saveNoteCardResult = await this.cardRepository.save(noteCard);
217 if (saveNoteCardResult.isErr()) {
218 return err(
219 AppError.UnexpectedError.create(saveNoteCardResult.error),
220 );
221 }
222
223 // Determine service options based on context
224 const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT;
225 const noteCardOptions = isFirehoseEvent && request.publishedRecordIds?.noteCard ? {
226 skipPublishing: true,
227 publishedRecordId: request.publishedRecordIds.noteCard,
228 } : undefined;
229
230 // Add note card to library using domain service
231 const addNoteCardToLibraryResult =
232 await this.cardLibraryService.addCardToLibrary(noteCard, curatorId, noteCardOptions);
233 if (addNoteCardToLibraryResult.isErr()) {
234 // Propagate authentication errors
235 if (
236 addNoteCardToLibraryResult.error instanceof AuthenticationError
237 ) {
238 return err(addNoteCardToLibraryResult.error);
239 }
240 if (
241 addNoteCardToLibraryResult.error instanceof
242 AppError.UnexpectedError
243 ) {
244 return err(addNoteCardToLibraryResult.error);
245 }
246 return err(
247 new ValidationError(addNoteCardToLibraryResult.error.message),
248 );
249 }
250
251 // Update noteCard reference to the one returned by the service
252 noteCard = addNoteCardToLibraryResult.value;
253 }
254 }
255
256 const addedToCollections: string[] = [];
257 const removedFromCollections: string[] = [];
258
259 // Handle adding to collections
260 if (request.addToCollections && request.addToCollections.length > 0) {
261 const collectionIds: CollectionId[] = [];
262 for (const collectionIdStr of request.addToCollections) {
263 const collectionIdResult =
264 CollectionId.createFromString(collectionIdStr);
265 if (collectionIdResult.isErr()) {
266 return err(
267 new ValidationError(
268 `Invalid collection ID: ${collectionIdResult.error.message}`,
269 ),
270 );
271 }
272 collectionIds.push(collectionIdResult.value);
273 }
274
275 // Determine service options based on context
276 const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT;
277 const collectionOptions = isFirehoseEvent && request.publishedRecordIds?.collectionLinks ? {
278 skipPublishing: true,
279 publishedRecordIds: request.publishedRecordIds.collectionLinks,
280 } : undefined;
281
282 const addToCollectionsResult =
283 await this.cardCollectionService.addCardToCollections(
284 urlCard,
285 collectionIds,
286 curatorId,
287 collectionOptions,
288 );
289 if (addToCollectionsResult.isErr()) {
290 // Propagate authentication errors
291 if (addToCollectionsResult.error instanceof AuthenticationError) {
292 return err(addToCollectionsResult.error);
293 }
294 if (
295 addToCollectionsResult.error instanceof AppError.UnexpectedError
296 ) {
297 return err(addToCollectionsResult.error);
298 }
299 return err(new ValidationError(addToCollectionsResult.error.message));
300 }
301
302 // Publish events for all affected collections
303 const updatedCollections = addToCollectionsResult.value;
304 for (const collection of updatedCollections) {
305 const publishResult =
306 await this.publishEventsForAggregate(collection);
307 if (publishResult.isErr()) {
308 console.error(
309 'Failed to publish events for collection:',
310 publishResult.error,
311 );
312 // Don't fail the operation if event publishing fails
313 }
314 addedToCollections.push(collection.collectionId.getStringValue());
315 }
316 }
317
318 // Handle removing from collections
319 if (
320 request.removeFromCollections &&
321 request.removeFromCollections.length > 0
322 ) {
323 const collectionIds: CollectionId[] = [];
324 for (const collectionIdStr of request.removeFromCollections) {
325 const collectionIdResult =
326 CollectionId.createFromString(collectionIdStr);
327 if (collectionIdResult.isErr()) {
328 return err(
329 new ValidationError(
330 `Invalid collection ID: ${collectionIdResult.error.message}`,
331 ),
332 );
333 }
334 collectionIds.push(collectionIdResult.value);
335 }
336
337 // Determine service options based on context
338 const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT;
339 const collectionOptions = isFirehoseEvent ? {
340 skipPublishing: true,
341 } : undefined;
342
343 const removeFromCollectionsResult =
344 await this.cardCollectionService.removeCardFromCollections(
345 urlCard,
346 collectionIds,
347 curatorId,
348 collectionOptions,
349 );
350 if (removeFromCollectionsResult.isErr()) {
351 // Propagate authentication errors
352 if (
353 removeFromCollectionsResult.error instanceof AuthenticationError
354 ) {
355 return err(removeFromCollectionsResult.error);
356 }
357 if (
358 removeFromCollectionsResult.error instanceof
359 AppError.UnexpectedError
360 ) {
361 return err(removeFromCollectionsResult.error);
362 }
363 return err(
364 new ValidationError(removeFromCollectionsResult.error.message),
365 );
366 }
367
368 // Publish events for all affected collections
369 const updatedCollections = removeFromCollectionsResult.value;
370 for (const collection of updatedCollections) {
371 const publishResult =
372 await this.publishEventsForAggregate(collection);
373 if (publishResult.isErr()) {
374 console.error(
375 'Failed to publish events for collection:',
376 publishResult.error,
377 );
378 // Don't fail the operation if event publishing fails
379 }
380 removedFromCollections.push(collection.collectionId.getStringValue());
381 }
382 }
383
384 return ok({
385 urlCardId: urlCard.cardId.getStringValue(),
386 noteCardId: noteCard?.cardId.getStringValue(),
387 addedToCollections,
388 removedFromCollections,
389 });
390 } catch (error) {
391 return err(AppError.UnexpectedError.create(error));
392 }
393 }
394}