A social knowledge tool for researchers built on ATProto
1import { Result, ok, err } from '../../../../shared/core/Result';
2import { Card } from '../Card';
3import { Collection } from '../Collection';
4import { CuratorId } from '../value-objects/CuratorId';
5import { CollectionId } from '../value-objects/CollectionId';
6import { ICollectionRepository } from '../ICollectionRepository';
7import { ICollectionPublisher } from '../../application/ports/ICollectionPublisher';
8import { AppError } from '../../../../shared/core/AppError';
9import { DomainService } from '../../../../shared/domain/DomainService';
10
11export class CardCollectionValidationError extends Error {
12 constructor(message: string) {
13 super(message);
14 this.name = 'CardCollectionValidationError';
15 }
16}
17
18export class CardCollectionService implements DomainService {
19 constructor(
20 private collectionRepository: ICollectionRepository,
21 private collectionPublisher: ICollectionPublisher,
22 ) {}
23
24 async addCardToCollection(
25 card: Card,
26 collectionId: CollectionId,
27 curatorId: CuratorId,
28 ): Promise<
29 Result<Collection, CardCollectionValidationError | AppError.UnexpectedError>
30 > {
31 try {
32 // Find the collection
33 const collectionResult =
34 await this.collectionRepository.findById(collectionId);
35 if (collectionResult.isErr()) {
36 return err(AppError.UnexpectedError.create(collectionResult.error));
37 }
38
39 const collection = collectionResult.value;
40 if (!collection) {
41 return err(
42 new CardCollectionValidationError(
43 `Collection not found: ${collectionId.getStringValue()}`,
44 ),
45 );
46 }
47
48 // Add card to collection
49 const addCardResult = collection.addCard(card.cardId, curatorId);
50 if (addCardResult.isErr()) {
51 return err(
52 new CardCollectionValidationError(
53 `Failed to add card to collection: ${addCardResult.error.message}`,
54 ),
55 );
56 }
57
58 // Publish the collection link
59 const publishLinkResult =
60 await this.collectionPublisher.publishCardAddedToCollection(
61 card,
62 collection,
63 curatorId,
64 );
65 if (publishLinkResult.isErr()) {
66 return err(
67 new CardCollectionValidationError(
68 `Failed to publish collection link: ${publishLinkResult.error.message}`,
69 ),
70 );
71 }
72
73 // Mark the card link as published in the collection
74 collection.markCardLinkAsPublished(card.cardId, publishLinkResult.value);
75
76 // Save the updated collection
77 const saveCollectionResult =
78 await this.collectionRepository.save(collection);
79 if (saveCollectionResult.isErr()) {
80 return err(AppError.UnexpectedError.create(saveCollectionResult.error));
81 }
82
83 return ok(collection);
84 } catch (error) {
85 return err(AppError.UnexpectedError.create(error));
86 }
87 }
88
89 async addCardToCollections(
90 card: Card,
91 collectionIds: CollectionId[],
92 curatorId: CuratorId,
93 ): Promise<
94 Result<
95 Collection[],
96 CardCollectionValidationError | AppError.UnexpectedError
97 >
98 > {
99 const updatedCollections: Collection[] = [];
100
101 for (const collectionId of collectionIds) {
102 const result = await this.addCardToCollection(
103 card,
104 collectionId,
105 curatorId,
106 );
107 if (result.isErr()) {
108 return err(result.error);
109 }
110 updatedCollections.push(result.value);
111 }
112 return ok(updatedCollections);
113 }
114
115 async removeCardFromCollection(
116 card: Card,
117 collectionId: CollectionId,
118 curatorId: CuratorId,
119 ): Promise<
120 Result<
121 Collection | null,
122 CardCollectionValidationError | AppError.UnexpectedError
123 >
124 > {
125 try {
126 // Find the collection
127 const collectionResult =
128 await this.collectionRepository.findById(collectionId);
129 if (collectionResult.isErr()) {
130 return err(AppError.UnexpectedError.create(collectionResult.error));
131 }
132
133 const collection = collectionResult.value;
134 if (!collection) {
135 return err(
136 new CardCollectionValidationError(
137 `Collection not found: ${collectionId.getStringValue()}`,
138 ),
139 );
140 }
141
142 // Check if card is in collection
143 const cardLink = collection.cardLinks.find((link) =>
144 link.cardId.equals(card.cardId),
145 );
146 if (!cardLink) {
147 // Card is not in collection, nothing to do
148 return ok(null);
149 }
150
151 // If the card link was published, unpublish it
152 if (cardLink.publishedRecordId) {
153 const unpublishLinkResult =
154 await this.collectionPublisher.unpublishCardAddedToCollection(
155 cardLink.publishedRecordId,
156 );
157 if (unpublishLinkResult.isErr()) {
158 return err(
159 new CardCollectionValidationError(
160 `Failed to unpublish collection link: ${unpublishLinkResult.error.message}`,
161 ),
162 );
163 }
164 }
165
166 // Remove card from collection
167 const removeCardResult = collection.removeCard(card.cardId, curatorId);
168 if (removeCardResult.isErr()) {
169 return err(
170 new CardCollectionValidationError(
171 `Failed to remove card from collection: ${removeCardResult.error.message}`,
172 ),
173 );
174 }
175
176 // Save the updated collection
177 const saveCollectionResult =
178 await this.collectionRepository.save(collection);
179 if (saveCollectionResult.isErr()) {
180 return err(AppError.UnexpectedError.create(saveCollectionResult.error));
181 }
182
183 return ok(collection);
184 } catch (error) {
185 return err(AppError.UnexpectedError.create(error));
186 }
187 }
188
189 async removeCardFromCollections(
190 card: Card,
191 collectionIds: CollectionId[],
192 curatorId: CuratorId,
193 ): Promise<
194 Result<
195 Collection[],
196 CardCollectionValidationError | AppError.UnexpectedError
197 >
198 > {
199 const updatedCollections: Collection[] = [];
200
201 for (const collectionId of collectionIds) {
202 const result = await this.removeCardFromCollection(
203 card,
204 collectionId,
205 curatorId,
206 );
207 if (result.isErr()) {
208 return err(result.error);
209 }
210 if (result.value !== null) {
211 updatedCollections.push(result.value);
212 }
213 }
214 return ok(updatedCollections);
215 }
216}