A social knowledge tool for researchers built on ATProto
1import {
2 PostgreSqlContainer,
3 StartedPostgreSqlContainer,
4} from '@testcontainers/postgresql';
5import postgres from 'postgres';
6import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
7import { DrizzleCardQueryRepository } from '../../infrastructure/repositories/DrizzleCardQueryRepository';
8import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository';
9import { CuratorId } from '../../domain/value-objects/CuratorId';
10import { cards } from '../../infrastructure/repositories/schema/card.sql';
11import {
12 collections,
13 collectionCards,
14} from '../../infrastructure/repositories/schema/collection.sql';
15import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql';
16import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql';
17import { CardBuilder } from '../utils/builders/CardBuilder';
18import { URL } from '../../domain/value-objects/URL';
19import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository';
20import { createTestSchema } from '../test-utils/createTestSchema';
21
22describe('DrizzleCardQueryRepository - getNoteCardsForUrl', () => {
23 let container: StartedPostgreSqlContainer;
24 let db: PostgresJsDatabase;
25 let queryRepository: DrizzleCardQueryRepository;
26 let cardRepository: DrizzleCardRepository;
27
28 // Test data
29 let curator1: CuratorId;
30 let curator2: CuratorId;
31 let curator3: CuratorId;
32
33 // Setup before all tests
34 beforeAll(async () => {
35 // Start PostgreSQL container
36 container = await new PostgreSqlContainer('postgres:14').start();
37
38 // Create database connection
39 const connectionString = container.getConnectionUri();
40 process.env.DATABASE_URL = connectionString;
41 const client = postgres(connectionString);
42 db = drizzle(client);
43
44 // Create repositories
45 queryRepository = new DrizzleCardQueryRepository(db);
46 cardRepository = new DrizzleCardRepository(db);
47
48 // Create schema using helper function
49 await createTestSchema(db);
50
51 // Create test data
52 curator1 = CuratorId.create('did:plc:testcurator1').unwrap();
53 curator2 = CuratorId.create('did:plc:testcurator2').unwrap();
54 curator3 = CuratorId.create('did:plc:testcurator3').unwrap();
55 }, 60000); // Increase timeout for container startup
56
57 // Cleanup after all tests
58 afterAll(async () => {
59 // Stop container
60 await container.stop();
61 });
62
63 // Clear data between tests
64 beforeEach(async () => {
65 await db.delete(collectionCards);
66 await db.delete(collections);
67 await db.delete(libraryMemberships);
68 await db.delete(cards);
69 await db.delete(publishedRecords);
70 });
71
72 describe('getNoteCardsForUrl', () => {
73 it('should return note cards from multiple users for the same URL', async () => {
74 const testUrl = 'https://example.com/shared-article';
75 const url = URL.create(testUrl).unwrap();
76
77 // Create note cards for different users with the same URL
78 const noteCard1 = new CardBuilder()
79 .withCuratorId(curator1.value)
80 .withNoteCard('First user note about the article')
81 .withUrl(url)
82 .withCreatedAt(new Date('2023-01-01'))
83 .withUpdatedAt(new Date('2023-01-01'))
84 .buildOrThrow();
85
86 const noteCard2 = new CardBuilder()
87 .withCuratorId(curator2.value)
88 .withNoteCard('Second user note about the article')
89 .withUrl(url)
90 .withCreatedAt(new Date('2023-01-02'))
91 .withUpdatedAt(new Date('2023-01-02'))
92 .buildOrThrow();
93
94 const noteCard3 = new CardBuilder()
95 .withCuratorId(curator3.value)
96 .withNoteCard('Third user note about the article')
97 .withUrl(url)
98 .withCreatedAt(new Date('2023-01-03'))
99 .withUpdatedAt(new Date('2023-01-03'))
100 .buildOrThrow();
101
102 // Save note cards
103 await cardRepository.save(noteCard1);
104 await cardRepository.save(noteCard2);
105 await cardRepository.save(noteCard3);
106
107 // Query note cards
108 const result = await queryRepository.getNoteCardsForUrl(testUrl, {
109 page: 1,
110 limit: 10,
111 sortBy: CardSortField.UPDATED_AT,
112 sortOrder: SortOrder.DESC,
113 });
114
115 expect(result.items).toHaveLength(3);
116 expect(result.totalCount).toBe(3);
117 expect(result.hasMore).toBe(false);
118
119 // Check that all three users' notes are included
120 const authorIds = result.items.map((note) => note.authorId);
121 expect(authorIds).toContain(curator1.value);
122 expect(authorIds).toContain(curator2.value);
123 expect(authorIds).toContain(curator3.value);
124
125 // Check note content
126 const noteTexts = result.items.map((note) => note.note);
127 expect(noteTexts).toContain('First user note about the article');
128 expect(noteTexts).toContain('Second user note about the article');
129 expect(noteTexts).toContain('Third user note about the article');
130
131 // Check sorting (DESC by updatedAt)
132 expect(result.items[0]!.note).toBe('Third user note about the article');
133 expect(result.items[1]!.note).toBe('Second user note about the article');
134 expect(result.items[2]!.note).toBe('First user note about the article');
135 });
136
137 it('should return empty result when no notes exist for the URL', async () => {
138 const testUrl = 'https://example.com/nonexistent-article';
139
140 const result = await queryRepository.getNoteCardsForUrl(testUrl, {
141 page: 1,
142 limit: 10,
143 sortBy: CardSortField.UPDATED_AT,
144 sortOrder: SortOrder.DESC,
145 });
146
147 expect(result.items).toHaveLength(0);
148 expect(result.totalCount).toBe(0);
149 expect(result.hasMore).toBe(false);
150 });
151
152 it('should not return notes for different URLs', async () => {
153 const testUrl1 = 'https://example.com/article1';
154 const testUrl2 = 'https://example.com/article2';
155 const url1 = URL.create(testUrl1).unwrap();
156 const url2 = URL.create(testUrl2).unwrap();
157
158 // Create note cards with different URLs
159 const noteCard1 = new CardBuilder()
160 .withCuratorId(curator1.value)
161 .withNoteCard('Note for article 1')
162 .withUrl(url1)
163 .buildOrThrow();
164
165 const noteCard2 = new CardBuilder()
166 .withCuratorId(curator2.value)
167 .withNoteCard('Note for article 2')
168 .withUrl(url2)
169 .buildOrThrow();
170
171 await cardRepository.save(noteCard1);
172 await cardRepository.save(noteCard2);
173
174 // Query for testUrl1
175 const result = await queryRepository.getNoteCardsForUrl(testUrl1, {
176 page: 1,
177 limit: 10,
178 sortBy: CardSortField.UPDATED_AT,
179 sortOrder: SortOrder.DESC,
180 });
181
182 expect(result.items).toHaveLength(1);
183 expect(result.items[0]!.note).toBe('Note for article 1');
184 expect(result.items[0]!.authorId).toBe(curator1.value);
185 });
186
187 it('should handle pagination correctly', async () => {
188 const testUrl = 'https://example.com/popular-article';
189 const url = URL.create(testUrl).unwrap();
190
191 // Create 5 note cards with the same URL from different users
192 for (let i = 1; i <= 5; i++) {
193 const curator = CuratorId.create(`did:plc:curator${i}`).unwrap();
194
195 const noteCard = new CardBuilder()
196 .withCuratorId(curator.value)
197 .withNoteCard(`Note ${i} about the article`)
198 .withUrl(url)
199 .withCreatedAt(new Date(`2023-01-0${i}`))
200 .withUpdatedAt(new Date(`2023-01-0${i}`))
201 .buildOrThrow();
202
203 await cardRepository.save(noteCard);
204 }
205
206 // Test first page with limit 2
207 const page1 = await queryRepository.getNoteCardsForUrl(testUrl, {
208 page: 1,
209 limit: 2,
210 sortBy: CardSortField.CREATED_AT,
211 sortOrder: SortOrder.ASC,
212 });
213
214 expect(page1.items).toHaveLength(2);
215 expect(page1.totalCount).toBe(5);
216 expect(page1.hasMore).toBe(true);
217 expect(page1.items[0]!.note).toBe('Note 1 about the article');
218 expect(page1.items[1]!.note).toBe('Note 2 about the article');
219
220 // Test second page
221 const page2 = await queryRepository.getNoteCardsForUrl(testUrl, {
222 page: 2,
223 limit: 2,
224 sortBy: CardSortField.CREATED_AT,
225 sortOrder: SortOrder.ASC,
226 });
227
228 expect(page2.items).toHaveLength(2);
229 expect(page2.totalCount).toBe(5);
230 expect(page2.hasMore).toBe(true);
231 expect(page2.items[0]!.note).toBe('Note 3 about the article');
232 expect(page2.items[1]!.note).toBe('Note 4 about the article');
233
234 // Test last page
235 const page3 = await queryRepository.getNoteCardsForUrl(testUrl, {
236 page: 3,
237 limit: 2,
238 sortBy: CardSortField.CREATED_AT,
239 sortOrder: SortOrder.ASC,
240 });
241
242 expect(page3.items).toHaveLength(1);
243 expect(page3.totalCount).toBe(5);
244 expect(page3.hasMore).toBe(false);
245 expect(page3.items[0]!.note).toBe('Note 5 about the article');
246 });
247
248 it('should sort by createdAt ascending', async () => {
249 const testUrl = 'https://example.com/test-article';
250 const url = URL.create(testUrl).unwrap();
251
252 const noteCard1 = new CardBuilder()
253 .withCuratorId(curator1.value)
254 .withNoteCard('First note')
255 .withUrl(url)
256 .withCreatedAt(new Date('2023-01-03'))
257 .buildOrThrow();
258
259 const noteCard2 = new CardBuilder()
260 .withCuratorId(curator2.value)
261 .withNoteCard('Second note')
262 .withUrl(url)
263 .withCreatedAt(new Date('2023-01-01'))
264 .buildOrThrow();
265
266 const noteCard3 = new CardBuilder()
267 .withCuratorId(curator3.value)
268 .withNoteCard('Third note')
269 .withUrl(url)
270 .withCreatedAt(new Date('2023-01-02'))
271 .buildOrThrow();
272
273 await cardRepository.save(noteCard1);
274 await cardRepository.save(noteCard2);
275 await cardRepository.save(noteCard3);
276
277 const result = await queryRepository.getNoteCardsForUrl(testUrl, {
278 page: 1,
279 limit: 10,
280 sortBy: CardSortField.CREATED_AT,
281 sortOrder: SortOrder.ASC,
282 });
283
284 expect(result.items).toHaveLength(3);
285 expect(result.items[0]!.note).toBe('Second note');
286 expect(result.items[1]!.note).toBe('Third note');
287 expect(result.items[2]!.note).toBe('First note');
288 });
289 });
290});