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 { DrizzleCollectionRepository } from '../../infrastructure/repositories/DrizzleCollectionRepository';
10import { CuratorId } from '../../domain/value-objects/CuratorId';
11import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID';
12import { cards } from '../../infrastructure/repositories/schema/card.sql';
13import {
14 collections,
15 collectionCards,
16} from '../../infrastructure/repositories/schema/collection.sql';
17import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql';
18import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql';
19import { Collection, CollectionAccessType } from '../../domain/Collection';
20import { CardBuilder } from '../utils/builders/CardBuilder';
21import { URL } from '../../domain/value-objects/URL';
22import { UrlMetadata } from '../../domain/value-objects/UrlMetadata';
23import { createTestSchema } from '../test-utils/createTestSchema';
24import { CardTypeEnum } from '../../domain/value-objects/CardType';
25
26describe('DrizzleCardQueryRepository - getUrlCardView', () => {
27 let container: StartedPostgreSqlContainer;
28 let db: PostgresJsDatabase;
29 let queryRepository: DrizzleCardQueryRepository;
30 let cardRepository: DrizzleCardRepository;
31 let collectionRepository: DrizzleCollectionRepository;
32
33 // Test data
34 let curatorId: CuratorId;
35 let otherCuratorId: CuratorId;
36 let thirdCuratorId: CuratorId;
37
38 // Setup before all tests
39 beforeAll(async () => {
40 // Start PostgreSQL container
41 container = await new PostgreSqlContainer('postgres:14').start();
42
43 // Create database connection
44 const connectionString = container.getConnectionUri();
45 process.env.DATABASE_URL = connectionString;
46 const client = postgres(connectionString);
47 db = drizzle(client);
48
49 // Create repositories
50 queryRepository = new DrizzleCardQueryRepository(db);
51 cardRepository = new DrizzleCardRepository(db);
52 collectionRepository = new DrizzleCollectionRepository(db);
53
54 // Create schema using helper function
55 await createTestSchema(db);
56
57 // Create test data
58 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
59 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
60 thirdCuratorId = CuratorId.create('did:plc:thirdcurator').unwrap();
61 }, 60000); // Increase timeout for container startup
62
63 // Cleanup after all tests
64 afterAll(async () => {
65 // Stop container
66 await container.stop();
67 });
68
69 // Clear data between tests
70 beforeEach(async () => {
71 await db.delete(collectionCards);
72 await db.delete(collections);
73 await db.delete(libraryMemberships);
74 await db.delete(cards);
75 await db.delete(publishedRecords);
76 });
77
78 describe('getUrlCardView', () => {
79 it('should return null when card does not exist', async () => {
80 const nonExistentCardId = new UniqueEntityID().toString();
81
82 const result = await queryRepository.getUrlCardView(nonExistentCardId);
83
84 expect(result).toBeNull();
85 });
86
87 it('should return null when card exists but is not a URL card', async () => {
88 // Create a note card
89 const noteCard = new CardBuilder()
90 .withCuratorId(curatorId.value)
91 .withNoteCard('This is a note')
92 .buildOrThrow();
93
94 await cardRepository.save(noteCard);
95
96 const result = await queryRepository.getUrlCardView(
97 noteCard.cardId.getStringValue(),
98 );
99
100 expect(result).toBeNull();
101 });
102
103 it('should return URL card view with basic metadata', async () => {
104 // Create URL card with metadata
105 const url = URL.create('https://example.com/article').unwrap();
106 const urlMetadata = UrlMetadata.create({
107 url: url.value,
108 title: 'Test Article',
109 description: 'A test article description',
110 author: 'John Doe',
111 imageUrl: 'https://example.com/image.jpg',
112 }).unwrap();
113
114 const urlCard = new CardBuilder()
115 .withCuratorId(curatorId.value)
116 .withUrlCard(url, urlMetadata)
117 .buildOrThrow();
118
119 await cardRepository.save(urlCard);
120
121 const result = await queryRepository.getUrlCardView(
122 urlCard.cardId.getStringValue(),
123 );
124
125 expect(result).toBeDefined();
126 expect(result?.id).toBe(urlCard.cardId.getStringValue());
127 expect(result?.type).toBe(CardTypeEnum.URL);
128 expect(result?.url).toBe(url.value);
129 expect(result?.cardContent.title).toBe('Test Article');
130 expect(result?.cardContent.description).toBe(
131 'A test article description',
132 );
133 expect(result?.cardContent.author).toBe('John Doe');
134 expect(result?.cardContent.thumbnailUrl).toBe(
135 'https://example.com/image.jpg',
136 );
137 expect(result?.libraries).toEqual([]);
138 expect(result?.collections).toEqual([]);
139 });
140
141 it('should return URL card view with minimal metadata', async () => {
142 // Create URL card without metadata
143 const url = URL.create('https://example.com/minimal').unwrap();
144 const urlCard = new CardBuilder()
145 .withCuratorId(curatorId.value)
146 .withUrlCard(url)
147 .buildOrThrow();
148
149 await cardRepository.save(urlCard);
150
151 const result = await queryRepository.getUrlCardView(
152 urlCard.cardId.getStringValue(),
153 );
154
155 expect(result).toBeDefined();
156 expect(result?.id).toBe(urlCard.cardId.getStringValue());
157 expect(result?.type).toBe(CardTypeEnum.URL);
158 expect(result?.url).toBe(url.value);
159 expect(result?.cardContent.title).toBeUndefined();
160 expect(result?.cardContent.description).toBeUndefined();
161 expect(result?.cardContent.author).toBeUndefined();
162 expect(result?.cardContent.thumbnailUrl).toBeUndefined();
163 });
164
165 it('should include creator in library when URL card is in their library', async () => {
166 // Create URL card
167 const url = URL.create('https://example.com/creator-library').unwrap();
168 const urlCard = new CardBuilder()
169 .withCuratorId(curatorId.value)
170 .withUrlCard(url)
171 .buildOrThrow();
172
173 await cardRepository.save(urlCard);
174
175 // Add card to creator's library (URL cards can only be in creator's library)
176 urlCard.addToLibrary(curatorId);
177 await cardRepository.save(urlCard);
178
179 const result = await queryRepository.getUrlCardView(
180 urlCard.cardId.getStringValue(),
181 );
182
183 expect(result).toBeDefined();
184 expect(result?.libraries).toHaveLength(1);
185 expect(result?.libraries[0]?.userId).toBe(curatorId.value);
186 });
187
188 it('should include collections that contain the card', async () => {
189 // Create URL card
190 const url = URL.create('https://example.com/collection-article').unwrap();
191 const urlCard = new CardBuilder()
192 .withCuratorId(curatorId.value)
193 .withUrlCard(url)
194 .buildOrThrow();
195
196 await cardRepository.save(urlCard);
197
198 // Create collections
199 const collection1 = Collection.create(
200 {
201 authorId: curatorId,
202 name: 'Reading List',
203 description: 'Articles to read',
204 accessType: CollectionAccessType.OPEN,
205 collaboratorIds: [],
206 createdAt: new Date(),
207 updatedAt: new Date(),
208 },
209 new UniqueEntityID(),
210 ).unwrap();
211
212 const collection2 = Collection.create(
213 {
214 authorId: otherCuratorId,
215 name: 'Favorites',
216 accessType: CollectionAccessType.OPEN,
217 collaboratorIds: [],
218 createdAt: new Date(),
219 updatedAt: new Date(),
220 },
221 new UniqueEntityID(),
222 ).unwrap();
223
224 // Add card to collections
225 collection1.addCard(urlCard.cardId, curatorId);
226 collection2.addCard(urlCard.cardId, otherCuratorId);
227
228 await collectionRepository.save(collection1);
229 await collectionRepository.save(collection2);
230
231 const result = await queryRepository.getUrlCardView(
232 urlCard.cardId.getStringValue(),
233 );
234
235 expect(result).toBeDefined();
236 expect(result?.collections).toHaveLength(2);
237
238 // Check collection details
239 const collectionNames = result?.collections.map((c) => c.name).sort();
240 expect(collectionNames).toEqual(['Favorites', 'Reading List']);
241
242 const readingListCollection = result?.collections.find(
243 (c) => c.name === 'Reading List',
244 );
245 expect(readingListCollection?.id).toBe(
246 collection1.collectionId.getStringValue(),
247 );
248 expect(readingListCollection?.authorId).toBe(curatorId.value);
249
250 const favoritesCollection = result?.collections.find(
251 (c) => c.name === 'Favorites',
252 );
253 expect(favoritesCollection?.id).toBe(
254 collection2.collectionId.getStringValue(),
255 );
256 expect(favoritesCollection?.authorId).toBe(otherCuratorId.value);
257 });
258
259 it('should handle card with both library and collections', async () => {
260 // Create URL card with full metadata
261 const url = URL.create('https://example.com/comprehensive').unwrap();
262 const urlMetadata = UrlMetadata.create({
263 url: url.value,
264 title: 'Comprehensive Article',
265 description: 'An article with everything',
266 author: 'Jane Smith',
267 imageUrl: 'https://example.com/comprehensive.jpg',
268 siteName: 'Example Site',
269 }).unwrap();
270
271 const urlCard = new CardBuilder()
272 .withCuratorId(curatorId.value)
273 .withUrlCard(url, urlMetadata)
274 .buildOrThrow();
275
276 await cardRepository.save(urlCard);
277
278 // Add to creator's library (URL cards can only be in creator's library)
279 urlCard.addToLibrary(curatorId);
280 await cardRepository.save(urlCard);
281
282 // Create multiple collections
283 const workCollection = Collection.create(
284 {
285 authorId: curatorId,
286 name: 'Work Research',
287 accessType: CollectionAccessType.CLOSED,
288 collaboratorIds: [],
289 createdAt: new Date(),
290 updatedAt: new Date(),
291 },
292 new UniqueEntityID(),
293 ).unwrap();
294
295 const personalCollection = Collection.create(
296 {
297 authorId: curatorId,
298 name: 'Personal Reading',
299 accessType: CollectionAccessType.OPEN,
300 collaboratorIds: [],
301 createdAt: new Date(),
302 updatedAt: new Date(),
303 },
304 new UniqueEntityID(),
305 ).unwrap();
306
307 const sharedCollection = Collection.create(
308 {
309 authorId: otherCuratorId,
310 name: 'Shared Articles',
311 accessType: CollectionAccessType.OPEN,
312 collaboratorIds: [],
313 createdAt: new Date(),
314 updatedAt: new Date(),
315 },
316 new UniqueEntityID(),
317 ).unwrap();
318
319 // Add card to collections
320 workCollection.addCard(urlCard.cardId, curatorId);
321 personalCollection.addCard(urlCard.cardId, curatorId);
322 sharedCollection.addCard(urlCard.cardId, otherCuratorId);
323
324 await collectionRepository.save(workCollection);
325 await collectionRepository.save(personalCollection);
326 await collectionRepository.save(sharedCollection);
327
328 const result = await queryRepository.getUrlCardView(
329 urlCard.cardId.getStringValue(),
330 );
331
332 expect(result).toBeDefined();
333
334 // Check URL metadata
335 expect(result?.cardContent.title).toBe('Comprehensive Article');
336 expect(result?.cardContent.description).toBe(
337 'An article with everything',
338 );
339 expect(result?.cardContent.author).toBe('Jane Smith');
340 expect(result?.cardContent.thumbnailUrl).toBe(
341 'https://example.com/comprehensive.jpg',
342 );
343
344 // Check libraries - URL cards can only be in creator's library
345 expect(result?.libraries).toHaveLength(1);
346 expect(result?.libraries[0]?.userId).toBe(curatorId.value);
347
348 // Check collections
349 expect(result?.collections).toHaveLength(3);
350 const collectionNames = result?.collections.map((c) => c.name).sort();
351 expect(collectionNames).toEqual([
352 'Personal Reading',
353 'Shared Articles',
354 'Work Research',
355 ]);
356
357 // Verify collection authors
358 const workColl = result?.collections.find(
359 (c) => c.name === 'Work Research',
360 );
361 expect(workColl?.authorId).toBe(curatorId.value);
362
363 const sharedColl = result?.collections.find(
364 (c) => c.name === 'Shared Articles',
365 );
366 expect(sharedColl?.authorId).toBe(otherCuratorId.value);
367 });
368
369 it('should handle card with no libraries or collections', async () => {
370 // Create URL card that's not in any libraries or collections
371 const url = URL.create('https://example.com/orphaned').unwrap();
372 const urlCard = new CardBuilder()
373 .withCuratorId(curatorId.value)
374 .withUrlCard(url)
375 .buildOrThrow();
376
377 await cardRepository.save(urlCard);
378
379 const result = await queryRepository.getUrlCardView(
380 urlCard.cardId.getStringValue(),
381 );
382
383 expect(result).toBeDefined();
384 expect(result?.id).toBe(urlCard.cardId.getStringValue());
385 expect(result?.type).toBe(CardTypeEnum.URL);
386 expect(result?.url).toBe(url.value);
387 expect(result?.libraries).toEqual([]);
388 expect(result?.collections).toEqual([]);
389 });
390
391 it('should handle card in library but not in any collections', async () => {
392 // Create URL card
393 const url = URL.create('https://example.com/library-only').unwrap();
394 const urlCard = new CardBuilder()
395 .withCuratorId(curatorId.value)
396 .withUrlCard(url)
397 .buildOrThrow();
398
399 await cardRepository.save(urlCard);
400
401 // Add to library only
402 urlCard.addToLibrary(curatorId);
403 await cardRepository.save(urlCard);
404
405 const result = await queryRepository.getUrlCardView(
406 urlCard.cardId.getStringValue(),
407 );
408
409 expect(result).toBeDefined();
410 expect(result?.libraries).toHaveLength(1);
411 expect(result?.libraries[0]?.userId).toBe(curatorId.value);
412 expect(result?.collections).toEqual([]);
413 });
414
415 it('should handle card in collections but not in any libraries', async () => {
416 // Create URL card
417 const url = URL.create('https://example.com/collection-only').unwrap();
418 const urlCard = new CardBuilder()
419 .withCuratorId(curatorId.value)
420 .withUrlCard(url)
421 .buildOrThrow();
422
423 await cardRepository.save(urlCard);
424
425 // Create collection and add card
426 const collection = Collection.create(
427 {
428 authorId: curatorId,
429 name: 'Collection Only',
430 accessType: CollectionAccessType.OPEN,
431 collaboratorIds: [],
432 createdAt: new Date(),
433 updatedAt: new Date(),
434 },
435 new UniqueEntityID(),
436 ).unwrap();
437
438 collection.addCard(urlCard.cardId, curatorId);
439 await collectionRepository.save(collection);
440
441 const result = await queryRepository.getUrlCardView(
442 urlCard.cardId.getStringValue(),
443 );
444
445 expect(result).toBeDefined();
446 expect(result?.libraries).toEqual([]);
447 expect(result?.collections).toHaveLength(1);
448 expect(result?.collections[0]?.name).toBe('Collection Only');
449 expect(result?.collections[0]?.authorId).toBe(curatorId.value);
450 });
451
452 it('should handle duplicate library memberships gracefully', async () => {
453 // Create URL card
454 const url = URL.create('https://example.com/duplicate-test').unwrap();
455 const urlCard = new CardBuilder()
456 .withCuratorId(curatorId.value)
457 .withUrlCard(url)
458 .buildOrThrow();
459
460 await cardRepository.save(urlCard);
461
462 // Add to library multiple times (should be handled by domain logic)
463 urlCard.addToLibrary(curatorId);
464 await cardRepository.save(urlCard);
465
466 // Try to add again (should not create duplicate)
467 urlCard.addToLibrary(curatorId);
468 await cardRepository.save(urlCard);
469
470 const result = await queryRepository.getUrlCardView(
471 urlCard.cardId.getStringValue(),
472 );
473
474 expect(result).toBeDefined();
475 expect(result?.libraries).toHaveLength(1);
476 expect(result?.libraries[0]?.userId).toBe(curatorId.value);
477 });
478
479 it('should handle multiple collections from different users', async () => {
480 // Create URL card
481 const url = URL.create('https://example.com/popular-article').unwrap();
482 const urlCard = new CardBuilder()
483 .withCuratorId(curatorId.value)
484 .withUrlCard(url)
485 .buildOrThrow();
486
487 await cardRepository.save(urlCard);
488
489 // Add to creator's library
490 urlCard.addToLibrary(curatorId);
491 await cardRepository.save(urlCard);
492
493 // Create multiple collections from different users
494 const collections = [];
495 for (let i = 1; i <= 5; i++) {
496 const collection = Collection.create(
497 {
498 authorId: curatorId,
499 name: `Collection ${i}`,
500 accessType: CollectionAccessType.OPEN,
501 collaboratorIds: [],
502 createdAt: new Date(),
503 updatedAt: new Date(),
504 },
505 new UniqueEntityID(),
506 ).unwrap();
507
508 collection.addCard(urlCard.cardId, curatorId);
509 await collectionRepository.save(collection);
510 collections.push(collection);
511 }
512
513 const result = await queryRepository.getUrlCardView(
514 urlCard.cardId.getStringValue(),
515 );
516
517 expect(result).toBeDefined();
518 // URL cards can only be in creator's library
519 expect(result?.libraries).toHaveLength(1);
520 expect(result?.collections).toHaveLength(5);
521
522 // Verify all collections are present
523 const collectionNames = result?.collections.map((c) => c.name).sort();
524 expect(collectionNames).toEqual([
525 'Collection 1',
526 'Collection 2',
527 'Collection 3',
528 'Collection 4',
529 'Collection 5',
530 ]);
531 });
532
533 it('should include connected note card in URL card view', async () => {
534 // Create URL card with metadata
535 const url = URL.create('https://example.com/article-with-note').unwrap();
536 const urlMetadata = UrlMetadata.create({
537 url: url.value,
538 title: 'Article with Note',
539 description: 'An article that has a connected note',
540 author: 'Jane Doe',
541 imageUrl: 'https://example.com/note-article.jpg',
542 }).unwrap();
543
544 const urlCard = new CardBuilder()
545 .withCuratorId(curatorId.value)
546 .withUrlCard(url, urlMetadata)
547 .buildOrThrow();
548
549 await cardRepository.save(urlCard);
550
551 // Create connected note card
552 const noteCard = new CardBuilder()
553 .withCuratorId(curatorId.value)
554 .withNoteCard(
555 'This is my detailed analysis of the article. It covers several key points and provides additional insights.',
556 )
557 .withParentCard(urlCard.cardId)
558 .buildOrThrow();
559
560 await cardRepository.save(noteCard);
561
562 // Add both cards to user's library
563 urlCard.addToLibrary(curatorId);
564 await cardRepository.save(urlCard);
565
566 noteCard.addToLibrary(curatorId);
567 await cardRepository.save(noteCard);
568
569 const result = await queryRepository.getUrlCardView(
570 urlCard.cardId.getStringValue(),
571 );
572
573 expect(result).toBeDefined();
574 expect(result?.id).toBe(urlCard.cardId.getStringValue());
575 expect(result?.type).toBe(CardTypeEnum.URL);
576 expect(result?.url).toBe(url.value);
577
578 // Check URL metadata
579 expect(result?.cardContent.title).toBe('Article with Note');
580 expect(result?.cardContent.description).toBe(
581 'An article that has a connected note',
582 );
583 expect(result?.cardContent.author).toBe('Jane Doe');
584 expect(result?.cardContent.thumbnailUrl).toBe(
585 'https://example.com/note-article.jpg',
586 );
587
588 // Check that the connected note is included
589 expect(result?.note).toBeDefined();
590 expect(result?.note?.id).toBe(noteCard.cardId.getStringValue());
591 expect(result?.note?.text).toBe(
592 'This is my detailed analysis of the article. It covers several key points and provides additional insights.',
593 );
594
595 // Check libraries
596 expect(result?.libraries).toHaveLength(1);
597 expect(result?.libraries[0]?.userId).toBe(curatorId.value);
598 });
599
600 it('should not include note cards that belong to different URL cards', async () => {
601 // Create first URL card
602 const url1 = URL.create('https://example.com/article1').unwrap();
603 const urlCard1 = new CardBuilder()
604 .withCuratorId(curatorId.value)
605 .withUrlCard(url1)
606 .buildOrThrow();
607
608 await cardRepository.save(urlCard1);
609
610 // Create second URL card
611 const url2 = URL.create('https://example.com/article2').unwrap();
612 const urlCard2 = new CardBuilder()
613 .withCuratorId(curatorId.value)
614 .withUrlCard(url2)
615 .buildOrThrow();
616
617 await cardRepository.save(urlCard2);
618
619 // Create note card connected to the SECOND URL card
620 const noteCard = new CardBuilder()
621 .withCuratorId(curatorId.value)
622 .withNoteCard('This note is for article 2')
623 .withParentCard(urlCard2.cardId)
624 .buildOrThrow();
625
626 await cardRepository.save(noteCard);
627
628 // Add cards to libraries
629 urlCard1.addToLibrary(curatorId);
630 await cardRepository.save(urlCard1);
631
632 urlCard2.addToLibrary(curatorId);
633 await cardRepository.save(urlCard2);
634
635 noteCard.addToLibrary(curatorId);
636 await cardRepository.save(noteCard);
637
638 // Query the FIRST URL card
639 const result = await queryRepository.getUrlCardView(
640 urlCard1.cardId.getStringValue(),
641 );
642
643 expect(result).toBeDefined();
644 expect(result?.id).toBe(urlCard1.cardId.getStringValue());
645
646 // Should NOT have a note since the note belongs to urlCard2
647 expect(result?.note).toBeUndefined();
648 });
649 });
650
651 describe('getLibrariesForCard', () => {
652 it('should return empty array when card has no library memberships', async () => {
653 // Create URL card but don't add to any libraries
654 const url = URL.create('https://example.com/no-libraries').unwrap();
655 const urlCard = new CardBuilder()
656 .withCuratorId(curatorId.value)
657 .withUrlCard(url)
658 .buildOrThrow();
659
660 await cardRepository.save(urlCard);
661
662 const result = await queryRepository.getLibrariesForCard(
663 urlCard.cardId.getStringValue(),
664 );
665
666 expect(result).toEqual([]);
667 });
668
669 it('should return single user ID when card is in one library', async () => {
670 // Create URL card
671 const url = URL.create('https://example.com/single-library').unwrap();
672 const urlCard = new CardBuilder()
673 .withCuratorId(curatorId.value)
674 .withUrlCard(url)
675 .buildOrThrow();
676
677 await cardRepository.save(urlCard);
678
679 // Add to one user's library
680 urlCard.addToLibrary(curatorId);
681 await cardRepository.save(urlCard);
682
683 const result = await queryRepository.getLibrariesForCard(
684 urlCard.cardId.getStringValue(),
685 );
686
687 expect(result).toHaveLength(1);
688 expect(result).toContain(curatorId.value);
689 });
690
691 it('should return creator ID for URL card in creator library', async () => {
692 // Create URL card
693 const url = URL.create('https://example.com/creator-library').unwrap();
694 const urlCard = new CardBuilder()
695 .withCuratorId(curatorId.value)
696 .withUrlCard(url)
697 .buildOrThrow();
698
699 await cardRepository.save(urlCard);
700
701 // Add to creator's library (URL cards can only be in creator's library)
702 urlCard.addToLibrary(curatorId);
703 await cardRepository.save(urlCard);
704
705 const result = await queryRepository.getLibrariesForCard(
706 urlCard.cardId.getStringValue(),
707 );
708
709 expect(result).toHaveLength(1);
710 expect(result).toContain(curatorId.value);
711 });
712
713 it('should return empty array for non-existent card', async () => {
714 const nonExistentCardId = new UniqueEntityID().toString();
715
716 const result =
717 await queryRepository.getLibrariesForCard(nonExistentCardId);
718
719 expect(result).toEqual([]);
720 });
721
722 it('should handle duplicate library memberships gracefully', async () => {
723 // Create URL card
724 const url = URL.create('https://example.com/duplicate-test').unwrap();
725 const urlCard = new CardBuilder()
726 .withCuratorId(curatorId.value)
727 .withUrlCard(url)
728 .buildOrThrow();
729
730 await cardRepository.save(urlCard);
731
732 // Add to library multiple times (domain logic should prevent duplicates)
733 urlCard.addToLibrary(curatorId);
734 await cardRepository.save(urlCard);
735
736 urlCard.addToLibrary(curatorId);
737 await cardRepository.save(urlCard);
738
739 const result = await queryRepository.getLibrariesForCard(
740 urlCard.cardId.getStringValue(),
741 );
742
743 expect(result).toHaveLength(1);
744 expect(result).toContain(curatorId.value);
745 });
746
747 it('should work with both URL and NOTE cards', async () => {
748 // Create URL card
749 const url = URL.create('https://example.com/with-note').unwrap();
750 const urlCard = new CardBuilder()
751 .withCuratorId(curatorId.value)
752 .withUrlCard(url)
753 .buildOrThrow();
754
755 await cardRepository.save(urlCard);
756
757 // Create note card
758 const noteCard = new CardBuilder()
759 .withCuratorId(curatorId.value)
760 .withNoteCard('Test note')
761 .buildOrThrow();
762
763 await cardRepository.save(noteCard);
764
765 // Add both cards to libraries
766 urlCard.addToLibrary(curatorId);
767 await cardRepository.save(urlCard);
768
769 noteCard.addToLibrary(otherCuratorId);
770 await cardRepository.save(noteCard);
771
772 // Test URL card libraries
773 const urlResult = await queryRepository.getLibrariesForCard(
774 urlCard.cardId.getStringValue(),
775 );
776 expect(urlResult).toEqual([curatorId.value]);
777
778 // Test note card libraries
779 const noteResult = await queryRepository.getLibrariesForCard(
780 noteCard.cardId.getStringValue(),
781 );
782 expect(noteResult).toEqual([otherCuratorId.value]);
783 });
784
785 it('should return libraries in consistent order', async () => {
786 // Create URL card
787 const url = URL.create('https://example.com/order-test').unwrap();
788 const urlCard = new CardBuilder()
789 .withCuratorId(thirdCuratorId.value)
790 .withUrlCard(url)
791 .buildOrThrow();
792
793 await cardRepository.save(urlCard);
794
795 // Add to creator's library (URL cards can only be in creator's library)
796 urlCard.addToLibrary(thirdCuratorId);
797 await cardRepository.save(urlCard);
798
799 const result = await queryRepository.getLibrariesForCard(
800 urlCard.cardId.getStringValue(),
801 );
802
803 expect(result).toHaveLength(1);
804 expect(result).toContain(thirdCuratorId.value);
805 });
806 });
807});