A social knowledge tool for researchers built on ATProto
1import { Result, ok, err } from '../../../../shared/core/Result';
2import { ICardRepository } from '../../domain/ICardRepository';
3import { Card } from '../../domain/Card';
4import { CardId } from '../../domain/value-objects/CardId';
5import { CuratorId } from '../../domain/value-objects/CuratorId';
6import { URL } from '../../domain/value-objects/URL';
7
8export class InMemoryCardRepository implements ICardRepository {
9 private static instance: InMemoryCardRepository;
10 private cards: Map<string, Card> = new Map();
11 private shouldFail: boolean = false;
12 private shouldFailSave: boolean = false;
13
14 private constructor() {}
15
16 public static getInstance(): InMemoryCardRepository {
17 if (!InMemoryCardRepository.instance) {
18 InMemoryCardRepository.instance = new InMemoryCardRepository();
19 }
20 return InMemoryCardRepository.instance;
21 }
22
23 private clone(card: Card): Card {
24 // Simple clone - in a real implementation you'd want proper deep cloning
25 const cardResult = Card.create(
26 {
27 curatorId: card.props.curatorId,
28 type: card.type,
29 content: card.content,
30 parentCardId: card.parentCardId,
31 url: card.url,
32 publishedRecordId: card.publishedRecordId,
33 libraryMemberships: card.libraryMemberships,
34 libraryCount: card.libraryCount,
35 createdAt: card.createdAt,
36 updatedAt: card.updatedAt,
37 },
38 card.id,
39 );
40
41 if (cardResult.isErr()) {
42 throw new Error(`Failed to clone card: ${cardResult.error.message}`);
43 }
44
45 return cardResult.value;
46 }
47
48 async findById(id: CardId): Promise<Result<Card | null>> {
49 if (this.shouldFail) {
50 return err(new Error('Simulated find failure'));
51 }
52 try {
53 const card = this.cards.get(id.getStringValue());
54 return ok(card ? this.clone(card) : null);
55 } catch (error) {
56 return err(error as Error);
57 }
58 }
59
60 async findUsersUrlCardByUrl(
61 url: URL,
62 curatorId: CuratorId,
63 ): Promise<Result<Card | null>> {
64 try {
65 const card = Array.from(this.cards.values()).find(
66 (card) =>
67 card.type.value === 'URL' &&
68 card.url?.value === url.value &&
69 card.props.curatorId.equals(curatorId),
70 );
71 return ok(card ? this.clone(card) : null);
72 } catch (error) {
73 return err(error as Error);
74 }
75 }
76
77 async findUsersNoteCardByUrl(
78 url: URL,
79 curatorId: CuratorId,
80 ): Promise<Result<Card | null>> {
81 try {
82 const card = Array.from(this.cards.values()).find(
83 (card) =>
84 card.type.value === 'NOTE' &&
85 card.url?.value === url.value &&
86 card.props.curatorId.equals(curatorId),
87 );
88 return ok(card ? this.clone(card) : null);
89 } catch (error) {
90 return err(error as Error);
91 }
92 }
93
94 async save(card: Card): Promise<Result<void>> {
95 if (this.shouldFailSave || this.shouldFail) {
96 return err(new Error('Simulated save failure'));
97 }
98 try {
99 this.cards.set(card.cardId.getStringValue(), this.clone(card));
100 return ok(undefined);
101 } catch (error) {
102 return err(error as Error);
103 }
104 }
105
106 async delete(cardId: CardId): Promise<Result<void>> {
107 try {
108 this.cards.delete(cardId.getStringValue());
109 return ok(undefined);
110 } catch (error) {
111 return err(error as Error);
112 }
113 }
114
115 setShouldFail(shouldFail: boolean): void {
116 this.shouldFail = shouldFail;
117 }
118
119 setShouldFailSave(shouldFailSave: boolean): void {
120 this.shouldFailSave = shouldFailSave;
121 }
122
123 // Helper methods for testing
124 public clear(): void {
125 this.cards.clear();
126 this.shouldFail = false;
127 this.shouldFailSave = false;
128 }
129
130 public getStoredCard(id: CardId): Card | undefined {
131 return this.cards.get(id.getStringValue());
132 }
133
134 public getAllCards(): Card[] {
135 return Array.from(this.cards.values()).map((card) => this.clone(card));
136 }
137}