This repository has no description
1import { Result, ok, err } from '../../../../shared/core/Result';
2import { Card } from '../Card';
3import { CuratorId } from '../value-objects/CuratorId';
4import { ICardPublisher } from '../../application/ports/ICardPublisher';
5import { ICardRepository } from '../ICardRepository';
6import { ICollectionRepository } from '../ICollectionRepository';
7import { AppError } from '../../../../shared/core/AppError';
8import { DomainService } from '../../../../shared/domain/DomainService';
9import { CardCollectionService } from './CardCollectionService';
10import { PublishedRecordId } from '../value-objects/PublishedRecordId';
11import { AuthenticationError } from '../../../../shared/core/AuthenticationError';
12
13export interface CardLibraryServiceOptions {
14 skipPublishing?: boolean;
15 publishedRecordId?: PublishedRecordId;
16 skipUnpublishing?: boolean;
17 skipCollectionUnpublishing?: boolean;
18 timestamp?: Date;
19}
20
21export class CardLibraryValidationError extends Error {
22 constructor(message: string) {
23 super(message);
24 this.name = 'CardLibraryValidationError';
25 }
26}
27
28export class CardLibraryService implements DomainService {
29 constructor(
30 private cardRepository: ICardRepository,
31 private cardPublisher: ICardPublisher,
32 private collectionRepository: ICollectionRepository,
33 private cardCollectionService: CardCollectionService,
34 ) {}
35
36 async updateCardInLibrary(
37 card: Card,
38 curatorId: CuratorId,
39 options?: CardLibraryServiceOptions,
40 ): Promise<
41 Result<
42 Card,
43 | CardLibraryValidationError
44 | AuthenticationError
45 | AppError.UnexpectedError
46 >
47 > {
48 try {
49 // Check if card is in curator's library
50 const isInLibrary = card.isInLibrary(curatorId);
51 const libraryInfo = card.getLibraryInfo(curatorId);
52
53 if (!isInLibrary) {
54 return err(
55 new CardLibraryValidationError(
56 'Card is not in library and cannot be updated',
57 ),
58 );
59 }
60
61 let parentCardPublishedRecordId: PublishedRecordId | undefined =
62 undefined;
63 let viaCardPublishedRecordId: PublishedRecordId | undefined = undefined;
64
65 if (card.parentCardId) {
66 // Ensure parent card is in the curator's library
67 const parentCardResult = await this.cardRepository.findById(
68 card.parentCardId,
69 );
70 if (parentCardResult.isErr()) {
71 return err(
72 new CardLibraryValidationError(
73 `Failed to fetch parent card: ${parentCardResult.error.message}`,
74 ),
75 );
76 }
77 const parentCardValue = parentCardResult.value;
78
79 if (!parentCardValue) {
80 return err(new CardLibraryValidationError(`Parent card not found`));
81 }
82 parentCardPublishedRecordId = parentCardValue.publishedRecordId;
83 }
84
85 if (card.viaCardId) {
86 // Get via card published record ID
87 const viaCardResult = await this.cardRepository.findById(
88 card.viaCardId,
89 );
90 if (viaCardResult.isErr()) {
91 return err(
92 new CardLibraryValidationError(
93 `Failed to fetch via card: ${viaCardResult.error.message}`,
94 ),
95 );
96 }
97 const viaCardValue = viaCardResult.value;
98
99 if (viaCardValue) {
100 viaCardPublishedRecordId = viaCardValue.publishedRecordId;
101 }
102 }
103
104 if (libraryInfo?.publishedRecordId) {
105 // Handle republishing based on options
106 if (options?.skipPublishing && options?.publishedRecordId) {
107 // Skip republishing and use provided record ID
108 const updatePublishedResult = card.markCardInLibraryAsPublished(
109 curatorId,
110 options.publishedRecordId,
111 );
112 if (updatePublishedResult.isErr()) {
113 return err(
114 new CardLibraryValidationError(
115 `Failed to update published record: ${updatePublishedResult.error.message}`,
116 ),
117 );
118 }
119 } else {
120 // Card is published - republish to update it
121 const republishResult = await this.cardPublisher.publishCardToLibrary(
122 card,
123 curatorId,
124 parentCardPublishedRecordId,
125 viaCardPublishedRecordId,
126 );
127 if (republishResult.isErr()) {
128 if (republishResult.error instanceof AuthenticationError) {
129 return err(republishResult.error);
130 }
131 return err(
132 new CardLibraryValidationError(
133 `Failed to republish updated card: ${republishResult.error.message}`,
134 ),
135 );
136 }
137
138 // Update the published record ID if it changed
139 const updatePublishedResult = card.markCardInLibraryAsPublished(
140 curatorId,
141 republishResult.value,
142 );
143 if (updatePublishedResult.isErr()) {
144 return err(
145 new CardLibraryValidationError(
146 `Failed to update published record: ${updatePublishedResult.error.message}`,
147 ),
148 );
149 }
150 }
151 }
152
153 // Save updated card
154 const saveResult = await this.cardRepository.save(card);
155 if (saveResult.isErr()) {
156 return err(AppError.UnexpectedError.create(saveResult.error));
157 }
158
159 return ok(card);
160 } catch (error) {
161 return err(AppError.UnexpectedError.create(error));
162 }
163 }
164
165 async addCardToLibrary(
166 card: Card,
167 curatorId: CuratorId,
168 options?: CardLibraryServiceOptions,
169 ): Promise<
170 Result<
171 Card,
172 | CardLibraryValidationError
173 | AuthenticationError
174 | AppError.UnexpectedError
175 >
176 > {
177 try {
178 // Check if card is already in curator's library
179 const isInLibrary = card.isInLibrary(curatorId);
180 const libraryInfo = card.getLibraryInfo(curatorId);
181
182 let parentCardPublishedRecordId: PublishedRecordId | undefined =
183 undefined;
184 let viaCardPublishedRecordId: PublishedRecordId | undefined = undefined;
185
186 if (card.parentCardId) {
187 // Ensure parent card is in the curator's library
188 const parentCardResult = await this.cardRepository.findById(
189 card.parentCardId,
190 );
191 if (parentCardResult.isErr()) {
192 return err(
193 new CardLibraryValidationError(
194 `Failed to fetch parent card: ${parentCardResult.error.message}`,
195 ),
196 );
197 }
198 const parentCardValue = parentCardResult.value;
199
200 if (!parentCardValue) {
201 return err(new CardLibraryValidationError(`Parent card not found`));
202 }
203 parentCardPublishedRecordId = parentCardValue.publishedRecordId;
204 }
205
206 if (card.viaCardId) {
207 // Get via card published record ID
208 const viaCardResult = await this.cardRepository.findById(
209 card.viaCardId,
210 );
211 if (viaCardResult.isErr()) {
212 return err(
213 new CardLibraryValidationError(
214 `Failed to fetch via card: ${viaCardResult.error.message}`,
215 ),
216 );
217 }
218 const viaCardValue = viaCardResult.value;
219
220 if (viaCardValue) {
221 viaCardPublishedRecordId = viaCardValue.publishedRecordId;
222 }
223 }
224
225 if (isInLibrary && libraryInfo?.publishedRecordId) {
226 // Card is already in library and published - republish to update it
227 const republishResult = await this.cardPublisher.publishCardToLibrary(
228 card,
229 curatorId,
230 parentCardPublishedRecordId,
231 viaCardPublishedRecordId,
232 );
233 if (republishResult.isErr()) {
234 if (republishResult.error instanceof AuthenticationError) {
235 return err(republishResult.error);
236 }
237 return err(
238 new CardLibraryValidationError(
239 `Failed to republish updated card: ${republishResult.error.message}`,
240 ),
241 );
242 }
243
244 // Update the published record ID if it changed
245 const updatePublishedResult = card.markCardInLibraryAsPublished(
246 curatorId,
247 republishResult.value,
248 );
249 if (updatePublishedResult.isErr()) {
250 return err(
251 new CardLibraryValidationError(
252 `Failed to update published record: ${updatePublishedResult.error.message}`,
253 ),
254 );
255 }
256
257 // Save updated card
258 const saveResult = await this.cardRepository.save(card);
259 if (saveResult.isErr()) {
260 return err(AppError.UnexpectedError.create(saveResult.error));
261 }
262
263 return ok(card);
264 }
265
266 if (isInLibrary) {
267 // Card is already in library but not published, nothing to do
268 return ok(card);
269 }
270 const addToLibResult = card.addToLibrary(curatorId, options?.timestamp);
271 if (addToLibResult.isErr()) {
272 return err(
273 new CardLibraryValidationError(
274 `Failed to add card to library: ${addToLibResult.error.message}`,
275 ),
276 );
277 }
278
279 // Handle publishing based on options
280 if (options?.skipPublishing && options?.publishedRecordId) {
281 // Skip publishing and use provided record ID
282 const markCardAsPublishedResult = card.markCardInLibraryAsPublished(
283 curatorId,
284 options.publishedRecordId,
285 );
286 if (markCardAsPublishedResult.isErr()) {
287 return err(
288 new CardLibraryValidationError(
289 `Failed to mark card as published in library: ${markCardAsPublishedResult.error.message}`,
290 ),
291 );
292 }
293 } else {
294 // Publish card to library normally
295 const publishResult = await this.cardPublisher.publishCardToLibrary(
296 card,
297 curatorId,
298 parentCardPublishedRecordId,
299 viaCardPublishedRecordId,
300 );
301 if (publishResult.isErr()) {
302 // Propagate authentication errors
303 if (publishResult.error instanceof AuthenticationError) {
304 return err(publishResult.error);
305 }
306 return err(
307 new CardLibraryValidationError(
308 `Failed to publish card to library: ${publishResult.error.message}`,
309 ),
310 );
311 }
312
313 // Mark card as published in library
314 const markCardAsPublishedResult = card.markCardInLibraryAsPublished(
315 curatorId,
316 publishResult.value,
317 );
318 if (markCardAsPublishedResult.isErr()) {
319 return err(
320 new CardLibraryValidationError(
321 `Failed to mark card as published in library: ${markCardAsPublishedResult.error.message}`,
322 ),
323 );
324 }
325 }
326
327 // Save updated card
328 const saveResult = await this.cardRepository.save(card);
329 if (saveResult.isErr()) {
330 return err(AppError.UnexpectedError.create(saveResult.error));
331 }
332
333 return ok(card);
334 } catch (error) {
335 return err(AppError.UnexpectedError.create(error));
336 }
337 }
338
339 async removeCardFromLibrary(
340 card: Card,
341 curatorId: CuratorId,
342 options?: CardLibraryServiceOptions,
343 ): Promise<
344 Result<
345 Card,
346 | CardLibraryValidationError
347 | AuthenticationError
348 | AppError.UnexpectedError
349 >
350 > {
351 try {
352 // Check if card is in curator's library
353 const isInLibrary = card.isInLibrary(curatorId);
354
355 if (!isInLibrary) {
356 // Card is not in library, nothing to do
357 return ok(card);
358 }
359
360 // Get all collections where this curator added this card (regardless of collection ownership)
361 const collectionsResult =
362 await this.collectionRepository.findContainingCardAddedBy(
363 card.cardId,
364 curatorId,
365 );
366 if (collectionsResult.isErr()) {
367 return err(AppError.UnexpectedError.create(collectionsResult.error));
368 }
369
370 // Remove card from all curator's collections
371 const collections = collectionsResult.value;
372 const collectionIds = collections.map(
373 (collection) => collection.collectionId,
374 );
375
376 if (collectionIds.length > 0) {
377 const removeFromCollectionsResult =
378 await this.cardCollectionService.removeCardFromCollections(
379 card,
380 collectionIds,
381 curatorId,
382 { skipPublishing: options?.skipCollectionUnpublishing },
383 );
384 if (removeFromCollectionsResult.isErr()) {
385 return err(
386 new CardLibraryValidationError(
387 `Failed to remove card from collections: ${removeFromCollectionsResult.error.message}`,
388 ),
389 );
390 }
391 }
392
393 // Handle cascading removal for URL cards
394 if (card.isUrlCard && card.url) {
395 const noteCardResult = await this.cardRepository.findUsersNoteCardByUrl(
396 card.url,
397 curatorId,
398 );
399
400 if (noteCardResult.isOk() && noteCardResult.value) {
401 const noteCard = noteCardResult.value;
402
403 // Recursively remove note card from library (this will handle its unpublishing)
404 const removeNoteResult = await this.removeCardFromLibrary(
405 noteCard,
406 curatorId,
407 options, // Pass the options down to maintain skipUnpublishing behavior
408 );
409 if (removeNoteResult.isErr()) {
410 return err(
411 new CardLibraryValidationError(
412 `Failed to remove associated note card: ${removeNoteResult.error.message}`,
413 ),
414 );
415 }
416 }
417 }
418
419 // Handle unpublishing based on options
420 const libraryInfo = card.getLibraryInfo(curatorId);
421 if (libraryInfo?.publishedRecordId && !options?.skipUnpublishing) {
422 // Unpublish card from library
423 const unpublishResult =
424 await this.cardPublisher.unpublishCardFromLibrary(
425 libraryInfo.publishedRecordId,
426 libraryInfo.curatorId,
427 );
428 if (unpublishResult.isErr()) {
429 // Propagate authentication errors
430 if (unpublishResult.error instanceof AuthenticationError) {
431 return err(unpublishResult.error);
432 }
433 return err(
434 new CardLibraryValidationError(
435 `Failed to unpublish card from library: ${unpublishResult.error.message}`,
436 ),
437 );
438 }
439 }
440
441 // Remove card from library
442 const removeResult = card.removeFromLibrary(curatorId);
443 if (removeResult.isErr()) {
444 return err(
445 new CardLibraryValidationError(
446 `Failed to remove card from library: ${removeResult.error.message}`,
447 ),
448 );
449 }
450
451 // Save updated card
452 const saveResult = await this.cardRepository.save(card);
453 if (saveResult.isErr()) {
454 return err(AppError.UnexpectedError.create(saveResult.error));
455 }
456
457 return ok(card);
458 } catch (error) {
459 return err(AppError.UnexpectedError.create(error));
460 }
461 }
462}