A social knowledge tool for researchers built on ATProto
45
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement `getCollectionsContainingCardForUser` method in DrizzleCollectionQueryRepository

Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>

+811 -2
+45 -2
src/modules/cards/infrastructure/repositories/DrizzleCollectionQueryRepository.ts
··· 1 - import { eq, desc, asc, count, sql, or, ilike } from 'drizzle-orm'; 1 + import { eq, desc, asc, count, sql, or, ilike, and } from 'drizzle-orm'; 2 2 import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; 3 3 import { 4 4 ICollectionQueryRepository, ··· 7 7 CollectionQueryResultDTO, 8 8 CollectionSortField, 9 9 SortOrder, 10 + CollectionContainingCardDTO, 10 11 } from '../../domain/ICollectionQueryRepository'; 11 - import { collections } from './schema/collection.sql'; 12 + import { collections, collectionCards } from './schema/collection.sql'; 12 13 import { publishedRecords } from './schema/publishedRecord.sql'; 13 14 import { CollectionMapper } from './mappers/CollectionMapper'; 14 15 ··· 104 105 }; 105 106 } catch (error) { 106 107 console.error('Error in findByCreator:', error); 108 + throw error; 109 + } 110 + } 111 + 112 + async getCollectionsContainingCardForUser( 113 + cardId: string, 114 + curatorId: string, 115 + ): Promise<CollectionContainingCardDTO[]> { 116 + try { 117 + // Find collections authored by this curator that contain this card 118 + const collectionResults = await this.db 119 + .select({ 120 + id: collections.id, 121 + name: collections.name, 122 + description: collections.description, 123 + uri: publishedRecords.uri, 124 + }) 125 + .from(collections) 126 + .leftJoin( 127 + publishedRecords, 128 + eq(collections.publishedRecordId, publishedRecords.id), 129 + ) 130 + .innerJoin( 131 + collectionCards, 132 + eq(collections.id, collectionCards.collectionId), 133 + ) 134 + .where( 135 + and( 136 + eq(collections.authorId, curatorId), 137 + eq(collectionCards.cardId, cardId), 138 + ), 139 + ) 140 + .orderBy(asc(collections.name)); 141 + 142 + return collectionResults.map((result) => ({ 143 + id: result.id, 144 + uri: result.uri || undefined, 145 + name: result.name, 146 + description: result.description || undefined, 147 + })); 148 + } catch (error) { 149 + console.error('Error in getCollectionsContainingCardForUser:', error); 107 150 throw error; 108 151 } 109 152 }
+766
src/modules/cards/tests/infrastructure/DrizzleCollectionQueryRepository.getCollectionsContainingCardForUser.integration.test.ts
··· 1 + import { 2 + PostgreSqlContainer, 3 + StartedPostgreSqlContainer, 4 + } from '@testcontainers/postgresql'; 5 + import postgres from 'postgres'; 6 + import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; 7 + import { DrizzleCollectionQueryRepository } from '../../infrastructure/repositories/DrizzleCollectionQueryRepository'; 8 + import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository'; 9 + import { DrizzleCollectionRepository } from '../../infrastructure/repositories/DrizzleCollectionRepository'; 10 + import { CuratorId } from '../../domain/value-objects/CuratorId'; 11 + import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; 12 + import { 13 + collections, 14 + collectionCollaborators, 15 + collectionCards, 16 + } from '../../infrastructure/repositories/schema/collection.sql'; 17 + import { cards } from '../../infrastructure/repositories/schema/card.sql'; 18 + import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql'; 19 + import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql'; 20 + import { Collection, CollectionAccessType } from '../../domain/Collection'; 21 + import { CardFactory } from '../../domain/CardFactory'; 22 + import { CardTypeEnum } from '../../domain/value-objects/CardType'; 23 + import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId'; 24 + import { URL } from '../../domain/value-objects/URL'; 25 + import { createTestSchema } from '../test-utils/createTestSchema'; 26 + 27 + describe('DrizzleCollectionQueryRepository - getCollectionsContainingCardForUser', () => { 28 + let container: StartedPostgreSqlContainer; 29 + let db: PostgresJsDatabase; 30 + let queryRepository: DrizzleCollectionQueryRepository; 31 + let collectionRepository: DrizzleCollectionRepository; 32 + let cardRepository: DrizzleCardRepository; 33 + 34 + // Test data 35 + let curatorId: CuratorId; 36 + let otherCuratorId: 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 DrizzleCollectionQueryRepository(db); 51 + collectionRepository = new DrizzleCollectionRepository(db); 52 + cardRepository = new DrizzleCardRepository(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 + }, 60000); // Increase timeout for container startup 61 + 62 + // Cleanup after all tests 63 + afterAll(async () => { 64 + // Stop container 65 + await container.stop(); 66 + }); 67 + 68 + // Clear data between tests 69 + beforeEach(async () => { 70 + await db.delete(collectionCards); 71 + await db.delete(collectionCollaborators); 72 + await db.delete(collections); 73 + await db.delete(libraryMemberships); 74 + await db.delete(cards); 75 + await db.delete(publishedRecords); 76 + }); 77 + 78 + describe('URL card in collections', () => { 79 + it('should return collections when user has URL card in multiple collections', async () => { 80 + const testUrl = 'https://example.com/test-article'; 81 + 82 + // Create a URL card 83 + const url = URL.create(testUrl).unwrap(); 84 + const card = CardFactory.create({ 85 + curatorId: curatorId.value, 86 + cardInput: { 87 + type: CardTypeEnum.URL, 88 + url: url.value, 89 + }, 90 + }).unwrap(); 91 + 92 + // Add card to library 93 + const addToLibResult = card.addToLibrary(curatorId); 94 + expect(addToLibResult.isOk()).toBe(true); 95 + 96 + await cardRepository.save(card); 97 + 98 + // Create first collection 99 + const collection1 = Collection.create( 100 + { 101 + authorId: curatorId, 102 + name: 'Tech Articles', 103 + description: 'Collection of technology articles', 104 + accessType: CollectionAccessType.OPEN, 105 + collaboratorIds: [], 106 + createdAt: new Date(), 107 + updatedAt: new Date(), 108 + }, 109 + new UniqueEntityID(), 110 + ).unwrap(); 111 + 112 + // Create second collection 113 + const collection2 = Collection.create( 114 + { 115 + authorId: curatorId, 116 + name: 'Reading List', 117 + description: 'My personal reading list', 118 + accessType: CollectionAccessType.OPEN, 119 + collaboratorIds: [], 120 + createdAt: new Date(), 121 + updatedAt: new Date(), 122 + }, 123 + new UniqueEntityID(), 124 + ).unwrap(); 125 + 126 + // Add card to both collections 127 + const addToCollection1Result = collection1.addCard(card.cardId, curatorId); 128 + expect(addToCollection1Result.isOk()).toBe(true); 129 + 130 + const addToCollection2Result = collection2.addCard(card.cardId, curatorId); 131 + expect(addToCollection2Result.isOk()).toBe(true); 132 + 133 + // Mark collections as published 134 + const collection1PublishedRecordId = PublishedRecordId.create({ 135 + uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1', 136 + cid: 'bafyreicollection1cid', 137 + }); 138 + 139 + const collection2PublishedRecordId = PublishedRecordId.create({ 140 + uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2', 141 + cid: 'bafyreicollection2cid', 142 + }); 143 + 144 + collection1.markAsPublished(collection1PublishedRecordId); 145 + collection2.markAsPublished(collection2PublishedRecordId); 146 + 147 + // Save collections 148 + await collectionRepository.save(collection1); 149 + await collectionRepository.save(collection2); 150 + 151 + // Execute the query 152 + const result = await queryRepository.getCollectionsContainingCardForUser( 153 + card.cardId.getStringValue(), 154 + curatorId.value, 155 + ); 156 + 157 + // Verify the result 158 + expect(result).toHaveLength(2); 159 + 160 + // Sort by name for consistent testing 161 + result.sort((a, b) => a.name.localeCompare(b.name)); 162 + 163 + // Verify collection details 164 + expect(result[0]?.id).toBe(collection2.collectionId.getStringValue()); // Reading List comes first alphabetically 165 + expect(result[0]?.uri).toBe('at://did:plc:testcurator/network.cosmik.collection/collection2'); 166 + expect(result[0]?.name).toBe('Reading List'); 167 + expect(result[0]?.description).toBe('My personal reading list'); 168 + 169 + expect(result[1]?.id).toBe(collection1.collectionId.getStringValue()); // Tech Articles comes second 170 + expect(result[1]?.uri).toBe('at://did:plc:testcurator/network.cosmik.collection/collection1'); 171 + expect(result[1]?.name).toBe('Tech Articles'); 172 + expect(result[1]?.description).toBe('Collection of technology articles'); 173 + }); 174 + 175 + it('should return empty array when user has URL card but not in any collections', async () => { 176 + const testUrl = 'https://example.com/standalone-article'; 177 + 178 + // Create a URL card 179 + const url = URL.create(testUrl).unwrap(); 180 + const card = CardFactory.create({ 181 + curatorId: curatorId.value, 182 + cardInput: { 183 + type: CardTypeEnum.URL, 184 + url: url.value, 185 + }, 186 + }).unwrap(); 187 + 188 + // Add card to library 189 + const addToLibResult = card.addToLibrary(curatorId); 190 + expect(addToLibResult.isOk()).toBe(true); 191 + 192 + await cardRepository.save(card); 193 + 194 + // Execute the query 195 + const result = await queryRepository.getCollectionsContainingCardForUser( 196 + card.cardId.getStringValue(), 197 + curatorId.value, 198 + ); 199 + 200 + // Verify the result 201 + expect(result).toHaveLength(0); 202 + }); 203 + 204 + it('should return empty array when card does not exist', async () => { 205 + const nonExistentCardId = new UniqueEntityID().toString(); 206 + 207 + // Execute the query 208 + const result = await queryRepository.getCollectionsContainingCardForUser( 209 + nonExistentCardId, 210 + curatorId.value, 211 + ); 212 + 213 + // Verify the result 214 + expect(result).toHaveLength(0); 215 + }); 216 + 217 + it('should not return collections from other users even if they have the same card', async () => { 218 + const testUrl = 'https://example.com/shared-article'; 219 + 220 + // Create URL card for first user 221 + const url = URL.create(testUrl).unwrap(); 222 + const card1 = CardFactory.create({ 223 + curatorId: curatorId.value, 224 + cardInput: { 225 + type: CardTypeEnum.URL, 226 + url: url.value, 227 + }, 228 + }).unwrap(); 229 + 230 + const addToLibResult1 = card1.addToLibrary(curatorId); 231 + expect(addToLibResult1.isOk()).toBe(true); 232 + 233 + await cardRepository.save(card1); 234 + 235 + // Create URL card for second user (different card, same URL) 236 + const card2 = CardFactory.create({ 237 + curatorId: otherCuratorId.value, 238 + cardInput: { 239 + type: CardTypeEnum.URL, 240 + url: url.value, 241 + }, 242 + }).unwrap(); 243 + 244 + const addToLibResult2 = card2.addToLibrary(otherCuratorId); 245 + expect(addToLibResult2.isOk()).toBe(true); 246 + 247 + await cardRepository.save(card2); 248 + 249 + // Create collection for second user and add their card 250 + const otherUserCollection = Collection.create( 251 + { 252 + authorId: otherCuratorId, 253 + name: 'Other User Collection', 254 + accessType: CollectionAccessType.OPEN, 255 + collaboratorIds: [], 256 + createdAt: new Date(), 257 + updatedAt: new Date(), 258 + }, 259 + new UniqueEntityID(), 260 + ).unwrap(); 261 + 262 + const addToOtherCollectionResult = otherUserCollection.addCard( 263 + card2.cardId, 264 + otherCuratorId, 265 + ); 266 + expect(addToOtherCollectionResult.isOk()).toBe(true); 267 + 268 + await collectionRepository.save(otherUserCollection); 269 + 270 + // Execute the query for first user's card 271 + const result = await queryRepository.getCollectionsContainingCardForUser( 272 + card1.cardId.getStringValue(), 273 + curatorId.value, 274 + ); 275 + 276 + // Verify the result - should be empty since first user's card is not in any collections 277 + expect(result).toHaveLength(0); 278 + }); 279 + 280 + it('should only return collections owned by the requesting user', async () => { 281 + const testUrl = 'https://example.com/multi-user-article'; 282 + 283 + // Create URL card for the user 284 + const url = URL.create(testUrl).unwrap(); 285 + const card = CardFactory.create({ 286 + curatorId: curatorId.value, 287 + cardInput: { 288 + type: CardTypeEnum.URL, 289 + url: url.value, 290 + }, 291 + }).unwrap(); 292 + 293 + const addToLibResult = card.addToLibrary(curatorId); 294 + expect(addToLibResult.isOk()).toBe(true); 295 + 296 + await cardRepository.save(card); 297 + 298 + // Create user's own collection 299 + const userCollection = Collection.create( 300 + { 301 + authorId: curatorId, 302 + name: 'My Collection', 303 + accessType: CollectionAccessType.OPEN, 304 + collaboratorIds: [], 305 + createdAt: new Date(), 306 + updatedAt: new Date(), 307 + }, 308 + new UniqueEntityID(), 309 + ).unwrap(); 310 + 311 + const addToUserCollectionResult = userCollection.addCard( 312 + card.cardId, 313 + curatorId, 314 + ); 315 + expect(addToUserCollectionResult.isOk()).toBe(true); 316 + 317 + await collectionRepository.save(userCollection); 318 + 319 + // Create another user's collection (this should not appear in results) 320 + const otherUserCollection = Collection.create( 321 + { 322 + authorId: otherCuratorId, 323 + name: 'Other User Collection', 324 + accessType: CollectionAccessType.OPEN, 325 + collaboratorIds: [], 326 + createdAt: new Date(), 327 + updatedAt: new Date(), 328 + }, 329 + new UniqueEntityID(), 330 + ).unwrap(); 331 + 332 + // Note: We don't add the card to the other user's collection since they can't add 333 + // another user's card to their collection in this domain model 334 + 335 + await collectionRepository.save(otherUserCollection); 336 + 337 + // Execute the query 338 + const result = await queryRepository.getCollectionsContainingCardForUser( 339 + card.cardId.getStringValue(), 340 + curatorId.value, 341 + ); 342 + 343 + // Verify the result - should only see user's own collection 344 + expect(result).toHaveLength(1); 345 + expect(result[0]?.name).toBe('My Collection'); 346 + expect(result[0]?.id).toBe(userCollection.collectionId.getStringValue()); 347 + }); 348 + }); 349 + 350 + describe('Note cards in collections', () => { 351 + it('should return collections containing note cards', async () => { 352 + // Create a note card 353 + const card = CardFactory.create({ 354 + curatorId: curatorId.value, 355 + cardInput: { 356 + type: CardTypeEnum.NOTE, 357 + text: 'This is a test note', 358 + }, 359 + }).unwrap(); 360 + 361 + // Add card to library 362 + const addToLibResult = card.addToLibrary(curatorId); 363 + expect(addToLibResult.isOk()).toBe(true); 364 + 365 + await cardRepository.save(card); 366 + 367 + // Create collection 368 + const collection = Collection.create( 369 + { 370 + authorId: curatorId, 371 + name: 'My Notes', 372 + description: 'Collection of my personal notes', 373 + accessType: CollectionAccessType.OPEN, 374 + collaboratorIds: [], 375 + createdAt: new Date(), 376 + updatedAt: new Date(), 377 + }, 378 + new UniqueEntityID(), 379 + ).unwrap(); 380 + 381 + // Add card to collection 382 + const addToCollectionResult = collection.addCard(card.cardId, curatorId); 383 + expect(addToCollectionResult.isOk()).toBe(true); 384 + 385 + await collectionRepository.save(collection); 386 + 387 + // Execute the query 388 + const result = await queryRepository.getCollectionsContainingCardForUser( 389 + card.cardId.getStringValue(), 390 + curatorId.value, 391 + ); 392 + 393 + // Verify the result 394 + expect(result).toHaveLength(1); 395 + expect(result[0]?.id).toBe(collection.collectionId.getStringValue()); 396 + expect(result[0]?.name).toBe('My Notes'); 397 + expect(result[0]?.description).toBe('Collection of my personal notes'); 398 + expect(result[0]?.uri).toBeUndefined(); // Not published 399 + }); 400 + 401 + it('should handle collections with and without descriptions', async () => { 402 + // Create a note card 403 + const card = CardFactory.create({ 404 + curatorId: curatorId.value, 405 + cardInput: { 406 + type: CardTypeEnum.NOTE, 407 + text: 'Test note for collections', 408 + }, 409 + }).unwrap(); 410 + 411 + await cardRepository.save(card); 412 + 413 + // Create collection with description 414 + const collectionWithDesc = Collection.create( 415 + { 416 + authorId: curatorId, 417 + name: 'Collection With Description', 418 + description: 'This collection has a description', 419 + accessType: CollectionAccessType.OPEN, 420 + collaboratorIds: [], 421 + createdAt: new Date(), 422 + updatedAt: new Date(), 423 + }, 424 + new UniqueEntityID(), 425 + ).unwrap(); 426 + 427 + // Create collection without description 428 + const collectionWithoutDesc = Collection.create( 429 + { 430 + authorId: curatorId, 431 + name: 'Collection Without Description', 432 + // No description provided 433 + accessType: CollectionAccessType.OPEN, 434 + collaboratorIds: [], 435 + createdAt: new Date(), 436 + updatedAt: new Date(), 437 + }, 438 + new UniqueEntityID(), 439 + ).unwrap(); 440 + 441 + // Add card to both collections 442 + collectionWithDesc.addCard(card.cardId, curatorId); 443 + collectionWithoutDesc.addCard(card.cardId, curatorId); 444 + 445 + await collectionRepository.save(collectionWithDesc); 446 + await collectionRepository.save(collectionWithoutDesc); 447 + 448 + // Execute the query 449 + const result = await queryRepository.getCollectionsContainingCardForUser( 450 + card.cardId.getStringValue(), 451 + curatorId.value, 452 + ); 453 + 454 + // Verify the result 455 + expect(result).toHaveLength(2); 456 + 457 + // Sort by name for consistent testing 458 + result.sort((a, b) => a.name.localeCompare(b.name)); 459 + 460 + expect(result[0]?.name).toBe('Collection With Description'); 461 + expect(result[0]?.description).toBe('This collection has a description'); 462 + 463 + expect(result[1]?.name).toBe('Collection Without Description'); 464 + expect(result[1]?.description).toBeUndefined(); 465 + }); 466 + }); 467 + 468 + describe('Published and unpublished collections', () => { 469 + it('should return URI for published collections and undefined for unpublished', async () => { 470 + // Create a card 471 + const card = CardFactory.create({ 472 + curatorId: curatorId.value, 473 + cardInput: { 474 + type: CardTypeEnum.NOTE, 475 + text: 'Card for published/unpublished test', 476 + }, 477 + }).unwrap(); 478 + 479 + await cardRepository.save(card); 480 + 481 + // Create published collection 482 + const publishedCollection = Collection.create( 483 + { 484 + authorId: curatorId, 485 + name: 'Published Collection', 486 + description: 'This collection is published', 487 + accessType: CollectionAccessType.OPEN, 488 + collaboratorIds: [], 489 + createdAt: new Date(), 490 + updatedAt: new Date(), 491 + }, 492 + new UniqueEntityID(), 493 + ).unwrap(); 494 + 495 + // Create unpublished collection 496 + const unpublishedCollection = Collection.create( 497 + { 498 + authorId: curatorId, 499 + name: 'Unpublished Collection', 500 + description: 'This collection is not published', 501 + accessType: CollectionAccessType.OPEN, 502 + collaboratorIds: [], 503 + createdAt: new Date(), 504 + updatedAt: new Date(), 505 + }, 506 + new UniqueEntityID(), 507 + ).unwrap(); 508 + 509 + // Mark published collection as published 510 + const publishedRecordId = PublishedRecordId.create({ 511 + uri: 'at://did:plc:testcurator/network.cosmik.collection/published123', 512 + cid: 'bafyreipublishedcid', 513 + }); 514 + 515 + publishedCollection.markAsPublished(publishedRecordId); 516 + 517 + // Add card to both collections 518 + publishedCollection.addCard(card.cardId, curatorId); 519 + unpublishedCollection.addCard(card.cardId, curatorId); 520 + 521 + await collectionRepository.save(publishedCollection); 522 + await collectionRepository.save(unpublishedCollection); 523 + 524 + // Execute the query 525 + const result = await queryRepository.getCollectionsContainingCardForUser( 526 + card.cardId.getStringValue(), 527 + curatorId.value, 528 + ); 529 + 530 + // Verify the result 531 + expect(result).toHaveLength(2); 532 + 533 + // Find collections by name 534 + const publishedResult = result.find(c => c.name === 'Published Collection'); 535 + const unpublishedResult = result.find(c => c.name === 'Unpublished Collection'); 536 + 537 + expect(publishedResult?.uri).toBe('at://did:plc:testcurator/network.cosmik.collection/published123'); 538 + expect(unpublishedResult?.uri).toBeUndefined(); 539 + }); 540 + }); 541 + 542 + describe('Sorting and ordering', () => { 543 + it('should return collections sorted by name in ascending order', async () => { 544 + // Create a card 545 + const card = CardFactory.create({ 546 + curatorId: curatorId.value, 547 + cardInput: { 548 + type: CardTypeEnum.NOTE, 549 + text: 'Card for sorting test', 550 + }, 551 + }).unwrap(); 552 + 553 + await cardRepository.save(card); 554 + 555 + // Create collections with names that will test alphabetical sorting 556 + const collectionNames = ['Zebra Collection', 'Alpha Collection', 'Beta Collection']; 557 + 558 + for (const name of collectionNames) { 559 + const collection = Collection.create( 560 + { 561 + authorId: curatorId, 562 + name, 563 + accessType: CollectionAccessType.OPEN, 564 + collaboratorIds: [], 565 + createdAt: new Date(), 566 + updatedAt: new Date(), 567 + }, 568 + new UniqueEntityID(), 569 + ).unwrap(); 570 + 571 + collection.addCard(card.cardId, curatorId); 572 + await collectionRepository.save(collection); 573 + } 574 + 575 + // Execute the query 576 + const result = await queryRepository.getCollectionsContainingCardForUser( 577 + card.cardId.getStringValue(), 578 + curatorId.value, 579 + ); 580 + 581 + // Verify the result is sorted by name 582 + expect(result).toHaveLength(3); 583 + expect(result[0]?.name).toBe('Alpha Collection'); 584 + expect(result[1]?.name).toBe('Beta Collection'); 585 + expect(result[2]?.name).toBe('Zebra Collection'); 586 + }); 587 + }); 588 + 589 + describe('Edge cases', () => { 590 + it('should handle non-existent curator gracefully', async () => { 591 + const card = CardFactory.create({ 592 + curatorId: curatorId.value, 593 + cardInput: { 594 + type: CardTypeEnum.NOTE, 595 + text: 'Test card', 596 + }, 597 + }).unwrap(); 598 + 599 + await cardRepository.save(card); 600 + 601 + // Execute the query with non-existent curator 602 + const result = await queryRepository.getCollectionsContainingCardForUser( 603 + card.cardId.getStringValue(), 604 + 'did:plc:nonexistent', 605 + ); 606 + 607 + // Verify the result 608 + expect(result).toHaveLength(0); 609 + }); 610 + 611 + it('should handle invalid card ID format gracefully', async () => { 612 + // Execute the query with invalid card ID 613 + const result = await queryRepository.getCollectionsContainingCardForUser( 614 + 'invalid-card-id', 615 + curatorId.value, 616 + ); 617 + 618 + // Verify the result 619 + expect(result).toHaveLength(0); 620 + }); 621 + 622 + it('should handle empty curator ID gracefully', async () => { 623 + const card = CardFactory.create({ 624 + curatorId: curatorId.value, 625 + cardInput: { 626 + type: CardTypeEnum.NOTE, 627 + text: 'Test card', 628 + }, 629 + }).unwrap(); 630 + 631 + await cardRepository.save(card); 632 + 633 + // Execute the query with empty curator ID 634 + const result = await queryRepository.getCollectionsContainingCardForUser( 635 + card.cardId.getStringValue(), 636 + '', 637 + ); 638 + 639 + // Verify the result 640 + expect(result).toHaveLength(0); 641 + }); 642 + 643 + it('should handle collections with null published records', async () => { 644 + // Create a card 645 + const card = CardFactory.create({ 646 + curatorId: curatorId.value, 647 + cardInput: { 648 + type: CardTypeEnum.NOTE, 649 + text: 'Card for null published record test', 650 + }, 651 + }).unwrap(); 652 + 653 + await cardRepository.save(card); 654 + 655 + // Create collection without published record 656 + const collection = Collection.create( 657 + { 658 + authorId: curatorId, 659 + name: 'Collection Without Published Record', 660 + accessType: CollectionAccessType.OPEN, 661 + collaboratorIds: [], 662 + createdAt: new Date(), 663 + updatedAt: new Date(), 664 + }, 665 + new UniqueEntityID(), 666 + ).unwrap(); 667 + 668 + collection.addCard(card.cardId, curatorId); 669 + await collectionRepository.save(collection); 670 + 671 + // Execute the query 672 + const result = await queryRepository.getCollectionsContainingCardForUser( 673 + card.cardId.getStringValue(), 674 + curatorId.value, 675 + ); 676 + 677 + // Verify the result 678 + expect(result).toHaveLength(1); 679 + expect(result[0]?.uri).toBeUndefined(); 680 + expect(result[0]?.name).toBe('Collection Without Published Record'); 681 + }); 682 + }); 683 + 684 + describe('Multiple card types', () => { 685 + it('should work with different card types in the same collection', async () => { 686 + // Create different types of cards 687 + const urlCard = CardFactory.create({ 688 + curatorId: curatorId.value, 689 + cardInput: { 690 + type: CardTypeEnum.URL, 691 + url: 'https://example.com/test', 692 + }, 693 + }).unwrap(); 694 + 695 + const noteCard = CardFactory.create({ 696 + curatorId: curatorId.value, 697 + cardInput: { 698 + type: CardTypeEnum.NOTE, 699 + text: 'Test note', 700 + }, 701 + }).unwrap(); 702 + 703 + const highlightCard = CardFactory.create({ 704 + curatorId: curatorId.value, 705 + cardInput: { 706 + type: CardTypeEnum.HIGHLIGHT, 707 + text: 'Test highlight', 708 + url: 'https://example.com/source', 709 + }, 710 + }).unwrap(); 711 + 712 + await cardRepository.save(urlCard); 713 + await cardRepository.save(noteCard); 714 + await cardRepository.save(highlightCard); 715 + 716 + // Create collection and add all cards 717 + const collection = Collection.create( 718 + { 719 + authorId: curatorId, 720 + name: 'Mixed Content Collection', 721 + description: 'Collection with different card types', 722 + accessType: CollectionAccessType.OPEN, 723 + collaboratorIds: [], 724 + createdAt: new Date(), 725 + updatedAt: new Date(), 726 + }, 727 + new UniqueEntityID(), 728 + ).unwrap(); 729 + 730 + collection.addCard(urlCard.cardId, curatorId); 731 + collection.addCard(noteCard.cardId, curatorId); 732 + collection.addCard(highlightCard.cardId, curatorId); 733 + 734 + await collectionRepository.save(collection); 735 + 736 + // Test each card type 737 + const urlResult = await queryRepository.getCollectionsContainingCardForUser( 738 + urlCard.cardId.getStringValue(), 739 + curatorId.value, 740 + ); 741 + 742 + const noteResult = await queryRepository.getCollectionsContainingCardForUser( 743 + noteCard.cardId.getStringValue(), 744 + curatorId.value, 745 + ); 746 + 747 + const highlightResult = await queryRepository.getCollectionsContainingCardForUser( 748 + highlightCard.cardId.getStringValue(), 749 + curatorId.value, 750 + ); 751 + 752 + // Verify all return the same collection 753 + expect(urlResult).toHaveLength(1); 754 + expect(noteResult).toHaveLength(1); 755 + expect(highlightResult).toHaveLength(1); 756 + 757 + expect(urlResult[0]?.name).toBe('Mixed Content Collection'); 758 + expect(noteResult[0]?.name).toBe('Mixed Content Collection'); 759 + expect(highlightResult[0]?.name).toBe('Mixed Content Collection'); 760 + 761 + expect(urlResult[0]?.id).toBe(collection.collectionId.getStringValue()); 762 + expect(noteResult[0]?.id).toBe(collection.collectionId.getStringValue()); 763 + expect(highlightResult[0]?.id).toBe(collection.collectionId.getStringValue()); 764 + }); 765 + }); 766 + });