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});