This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / cards / domain / services / CardLibraryService.ts
15 kB 462 lines
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}