A social knowledge tool for researchers built on ATProto
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';
11
12export class CardLibraryValidationError extends Error {
13 constructor(message: string) {
14 super(message);
15 this.name = 'CardLibraryValidationError';
16 }
17}
18
19export class CardLibraryService implements DomainService {
20 constructor(
21 private cardRepository: ICardRepository,
22 private cardPublisher: ICardPublisher,
23 private collectionRepository: ICollectionRepository,
24 private cardCollectionService: CardCollectionService,
25 ) {}
26
27 async addCardToLibrary(
28 card: Card,
29 curatorId: CuratorId,
30 ): Promise<
31 Result<Card, CardLibraryValidationError | AppError.UnexpectedError>
32 > {
33 try {
34 // Check if card is already in curator's library
35 const isInLibrary = card.isInLibrary(curatorId);
36
37 if (isInLibrary) {
38 // Card is already in library, nothing to do
39 return ok(card);
40 }
41 const addToLibResult = card.addToLibrary(curatorId);
42 if (addToLibResult.isErr()) {
43 return err(
44 new CardLibraryValidationError(
45 `Failed to add card to library: ${addToLibResult.error.message}`,
46 ),
47 );
48 }
49 let parentCardPublishedRecordId: PublishedRecordId | undefined =
50 undefined;
51
52 if (card.parentCardId) {
53 // Ensure parent card is in the curator's library
54 const parentCardResult = await this.cardRepository.findById(
55 card.parentCardId,
56 );
57 if (parentCardResult.isErr()) {
58 return err(
59 new CardLibraryValidationError(
60 `Failed to fetch parent card: ${parentCardResult.error.message}`,
61 ),
62 );
63 }
64 const parentCardValue = parentCardResult.value;
65
66 if (!parentCardValue) {
67 return err(new CardLibraryValidationError(`Parent card not found`));
68 }
69 parentCardPublishedRecordId = parentCardValue.publishedRecordId;
70 }
71
72 // Publish card to library
73 const publishResult = await this.cardPublisher.publishCardToLibrary(
74 card,
75 curatorId,
76 parentCardPublishedRecordId,
77 );
78 if (publishResult.isErr()) {
79 return err(
80 new CardLibraryValidationError(
81 `Failed to publish card to library: ${publishResult.error.message}`,
82 ),
83 );
84 }
85
86 // Mark card as published in library
87 const markCardAsPublishedResult = card.markCardInLibraryAsPublished(
88 curatorId,
89 publishResult.value,
90 );
91 if (markCardAsPublishedResult.isErr()) {
92 return err(
93 new CardLibraryValidationError(
94 `Failed to mark card as published in library: ${markCardAsPublishedResult.error.message}`,
95 ),
96 );
97 }
98
99 // Save updated card
100 const saveResult = await this.cardRepository.save(card);
101 if (saveResult.isErr()) {
102 return err(AppError.UnexpectedError.create(saveResult.error));
103 }
104
105 return ok(card);
106 } catch (error) {
107 return err(AppError.UnexpectedError.create(error));
108 }
109 }
110
111 async removeCardFromLibrary(
112 card: Card,
113 curatorId: CuratorId,
114 ): Promise<
115 Result<Card, CardLibraryValidationError | AppError.UnexpectedError>
116 > {
117 try {
118 // Check if card is in curator's library
119 const isInLibrary = card.isInLibrary(curatorId);
120
121 if (!isInLibrary) {
122 // Card is not in library, nothing to do
123 return ok(card);
124 }
125
126 // Get all collections owned by the curator that contain this card
127 const collectionsResult =
128 await this.collectionRepository.findByCuratorIdContainingCard(
129 curatorId,
130 card.cardId,
131 );
132 if (collectionsResult.isErr()) {
133 return err(AppError.UnexpectedError.create(collectionsResult.error));
134 }
135
136 // Remove card from all curator's collections
137 const collections = collectionsResult.value;
138 const collectionIds = collections.map(
139 (collection) => collection.collectionId,
140 );
141
142 if (collectionIds.length > 0) {
143 const removeFromCollectionsResult =
144 await this.cardCollectionService.removeCardFromCollections(
145 card,
146 collectionIds,
147 curatorId,
148 );
149 if (removeFromCollectionsResult.isErr()) {
150 return err(
151 new CardLibraryValidationError(
152 `Failed to remove card from collections: ${removeFromCollectionsResult.error.message}`,
153 ),
154 );
155 }
156 }
157
158 // Get library info to check if it was published
159 const libraryInfo = card.getLibraryInfo(curatorId);
160 if (libraryInfo?.publishedRecordId) {
161 // Unpublish card from library
162 const unpublishResult =
163 await this.cardPublisher.unpublishCardFromLibrary(
164 libraryInfo.publishedRecordId,
165 libraryInfo.curatorId,
166 );
167 if (unpublishResult.isErr()) {
168 return err(
169 new CardLibraryValidationError(
170 `Failed to unpublish card from library: ${unpublishResult.error.message}`,
171 ),
172 );
173 }
174 }
175
176 // Remove card from library
177 const removeResult = card.removeFromLibrary(curatorId);
178 if (removeResult.isErr()) {
179 return err(
180 new CardLibraryValidationError(
181 `Failed to remove card from library: ${removeResult.error.message}`,
182 ),
183 );
184 }
185
186 // Save updated card
187 const saveResult = await this.cardRepository.save(card);
188 if (saveResult.isErr()) {
189 return err(AppError.UnexpectedError.create(saveResult.error));
190 }
191
192 return ok(card);
193 } catch (error) {
194 return err(AppError.UnexpectedError.create(error));
195 }
196 }
197}