A social knowledge tool for researchers built on ATProto
1import { ICollectionPublisher } from '../../application/ports/ICollectionPublisher';
2import { Collection } from '../../domain/Collection';
3import { Card } from '../../domain/Card';
4import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId';
5import { ok, err, Result } from '../../../../shared/core/Result';
6import { UseCaseError } from '../../../../shared/core/UseCaseError';
7import { AppError } from '../../../../shared/core/AppError';
8import { CuratorId } from '../../domain/value-objects/CuratorId';
9import { EnvironmentConfigService } from 'src/shared/infrastructure/config/EnvironmentConfigService';
10
11export class FakeCollectionPublisher implements ICollectionPublisher {
12 private publishedCollections: Map<string, Collection> = new Map();
13 private publishedLinks: Map<
14 string,
15 { cardId: string; linkRecord: PublishedRecordId }[]
16 > = new Map();
17 private unpublishedCollections: Array<{ uri: string; cid: string }> = [];
18 private removedLinks: Array<{ cardId: string; collectionId: string }> = [];
19 private shouldFail: boolean = false;
20 private shouldFailUnpublish: boolean = false;
21 private collectionType =
22 new EnvironmentConfigService().getAtProtoCollections().collection;
23
24 private cardCollectionLinkType =
25 new EnvironmentConfigService().getAtProtoCollections().collectionLink;
26
27 async publish(
28 collection: Collection,
29 ): Promise<Result<PublishedRecordId, UseCaseError>> {
30 if (this.shouldFail) {
31 return err(
32 AppError.UnexpectedError.create(
33 new Error('Simulated collection publish failure'),
34 ),
35 );
36 }
37
38 const collectionId = collection.collectionId.getStringValue();
39 const fakeDid = process.env.BSKY_DID || 'did:plc:rlknsba2qldjkicxsmni3vyn';
40
41 // Simulate publishing the collection record itself
42 const fakeCollectionUri = `at://${fakeDid}/${this.collectionType}/${collectionId}`;
43 const fakeCollectionCid = `fake-collection-cid-${collectionId}`;
44
45 const collectionRecord = PublishedRecordId.create({
46 uri: fakeCollectionUri,
47 cid: fakeCollectionCid,
48 });
49
50 // Store the published collection for inspection
51 this.publishedCollections.set(collectionId, collection);
52
53 console.log(
54 `[FakeCollectionPublisher] Published collection ${collectionId}`,
55 );
56
57 return ok(collectionRecord);
58 }
59
60 async publishCardAddedToCollection(
61 card: Card,
62 collection: Collection,
63 curatorId: CuratorId,
64 ): Promise<Result<PublishedRecordId, UseCaseError>> {
65 if (this.shouldFail) {
66 return err(
67 AppError.UnexpectedError.create(
68 new Error('Simulated card-collection link publish failure'),
69 ),
70 );
71 }
72
73 const collectionId = collection.collectionId.getStringValue();
74 const cardId = card.cardId.getStringValue();
75
76 // Simulate publishing a card-collection link
77 const fakeLinkUri = `at://${curatorId.value}/${this.cardCollectionLinkType}/${collectionId}-${cardId}`;
78 const fakeLinkCid = `fake-link-cid-${collectionId}-${cardId}`;
79
80 const linkRecord = PublishedRecordId.create({
81 uri: fakeLinkUri,
82 cid: fakeLinkCid,
83 });
84
85 // Store published link for inspection
86 const existingLinks = this.publishedLinks.get(collectionId) || [];
87 existingLinks.push({
88 cardId,
89 linkRecord,
90 });
91 this.publishedLinks.set(collectionId, existingLinks);
92
93 console.log(
94 `[FakeCollectionPublisher] Published card ${cardId} added to collection ${collectionId} link at ${fakeLinkUri}`,
95 );
96
97 return ok(linkRecord);
98 }
99
100 async unpublishCardAddedToCollection(
101 recordId: PublishedRecordId,
102 ): Promise<Result<void, UseCaseError>> {
103 if (this.shouldFailUnpublish) {
104 return err(
105 AppError.UnexpectedError.create(
106 new Error('Simulated card-collection link unpublish failure'),
107 ),
108 );
109 }
110
111 // Find and remove the link by its published record ID
112 for (const [collectionId, links] of this.publishedLinks.entries()) {
113 const linkIndex = links.findIndex(
114 (link) => link.linkRecord.uri === recordId.uri,
115 );
116 if (linkIndex !== -1) {
117 const removedLink = links.splice(linkIndex, 1)[0];
118 this.removedLinks.push({
119 cardId: removedLink!.cardId,
120 collectionId,
121 });
122 console.log(
123 `[FakeCollectionPublisher] Unpublished card-collection link ${recordId.uri}`,
124 );
125 return ok(undefined);
126 }
127 }
128
129 console.warn(
130 `[FakeCollectionPublisher] Card-collection link not found for unpublishing: ${recordId.uri}`,
131 );
132 return ok(undefined);
133 }
134
135 async unpublish(
136 recordId: PublishedRecordId,
137 ): Promise<Result<void, UseCaseError>> {
138 if (this.shouldFailUnpublish) {
139 return err(
140 AppError.UnexpectedError.create(
141 new Error('Simulated collection unpublish failure'),
142 ),
143 );
144 }
145
146 // Find and remove the collection by its published record ID
147 for (const [
148 collectionId,
149 collection,
150 ] of this.publishedCollections.entries()) {
151 if (collection.publishedRecordId?.uri === recordId.uri) {
152 this.publishedCollections.delete(collectionId);
153 this.publishedLinks.delete(collectionId);
154 this.unpublishedCollections.push({
155 uri: recordId.uri,
156 cid: recordId.cid,
157 });
158 console.log(
159 `[FakeCollectionPublisher] Unpublished collection ${recordId.uri}`,
160 );
161 return ok(undefined);
162 }
163 }
164 if (this.publishedCollections.size === 0) {
165 this.unpublishedCollections.push({
166 uri: recordId.uri,
167 cid: recordId.cid,
168 });
169 console.log(
170 `[FakeCollectionPublisher] Unpublished collection ${recordId.uri} (not found in published collections)`,
171 );
172 return ok(undefined);
173 }
174
175 console.warn(
176 `[FakeCollectionPublisher] Collection not found for unpublishing: ${recordId.uri}`,
177 );
178 return ok(undefined);
179 }
180
181 setShouldFail(shouldFail: boolean): void {
182 this.shouldFail = shouldFail;
183 }
184
185 setShouldFailUnpublish(shouldFailUnpublish: boolean): void {
186 this.shouldFailUnpublish = shouldFailUnpublish;
187 }
188
189 clear(): void {
190 this.publishedCollections.clear();
191 this.publishedLinks.clear();
192 this.unpublishedCollections = [];
193 this.removedLinks = [];
194 this.shouldFail = false;
195 this.shouldFailUnpublish = false;
196 }
197
198 getPublishedCollections(): Collection[] {
199 return Array.from(this.publishedCollections.values());
200 }
201
202 getPublishedLinksForCollection(
203 collectionId: string,
204 ): Array<{ cardId: string; linkRecord: PublishedRecordId }> {
205 return this.publishedLinks.get(collectionId) || [];
206 }
207
208 getUnpublishedCollections(): Array<{ uri: string; cid: string }> {
209 return this.unpublishedCollections;
210 }
211
212 getRemovedLinksForCollection(
213 collectionId: string,
214 ): Array<{ cardId: string; collectionId: string }> {
215 return this.removedLinks.filter(
216 (link) => link.collectionId === collectionId,
217 );
218 }
219
220 getAllRemovedLinks(): Array<{ cardId: string; collectionId: string }> {
221 return this.removedLinks;
222 }
223 getAllPublishedLinks() {
224 const allLinks: Array<{ cardId: string; linkRecord: PublishedRecordId }> =
225 [];
226 for (const links of this.publishedLinks.values()) {
227 allLinks.push(...links);
228 }
229 return allLinks;
230 }
231}