+5
-1
src/modules/cards/infrastructure/repositories/query-services/UrlCardQueryService.ts
+5
-1
src/modules/cards/infrastructure/repositories/query-services/UrlCardQueryService.ts
···
359
359
options: CardQueryOptions,
360
360
): Promise<PaginatedQueryResult<LibraryForUrlDTO>> {
361
361
try {
362
-
const { page, limit } = options;
362
+
const { page, limit, sortBy, sortOrder } = options;
363
363
const offset = (page - 1) * limit;
364
+
365
+
// Build the sort order
366
+
const orderDirection = sortOrder === SortOrder.ASC ? asc : desc;
364
367
365
368
// Get all URL cards with this URL and their library memberships
366
369
const librariesQuery = this.db
···
376
379
.from(libraryMemberships)
377
380
.innerJoin(cards, eq(libraryMemberships.cardId, cards.id))
378
381
.where(and(eq(cards.url, url), eq(cards.type, CardTypeEnum.URL)))
382
+
.orderBy(orderDirection(this.getSortColumn(sortBy)))
379
383
.limit(limit)
380
384
.offset(offset);
381
385
+276
src/modules/cards/tests/infrastructure/DrizzleCardQueryRepository.getLibrariesForUrl.integration.test.ts
+276
src/modules/cards/tests/infrastructure/DrizzleCardQueryRepository.getLibrariesForUrl.integration.test.ts
···
15
15
import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository';
16
16
import { createTestSchema } from '../test-utils/createTestSchema';
17
17
import { CardTypeEnum } from '../../domain/value-objects/CardType';
18
+
import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId';
18
19
19
20
describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => {
20
21
let container: StartedPostgreSqlContainer;
···
281
282
// Should return empty since card is not in any library
282
283
expect(result.items).toHaveLength(0);
283
284
expect(result.totalCount).toBe(0);
285
+
});
286
+
});
287
+
288
+
describe('sorting', () => {
289
+
it('should sort by createdAt in descending order by default', async () => {
290
+
const testUrl = 'https://example.com/sort-test';
291
+
const url = URL.create(testUrl).unwrap();
292
+
293
+
// Create cards with different creation times
294
+
const card1 = new CardBuilder()
295
+
.withCuratorId(curator1.value)
296
+
.withType(CardTypeEnum.URL)
297
+
.withUrl(url)
298
+
.buildOrThrow();
299
+
300
+
await new Promise((resolve) => setTimeout(resolve, 1000));
301
+
const card2 = new CardBuilder()
302
+
.withCuratorId(curator2.value)
303
+
.withType(CardTypeEnum.URL)
304
+
.withUrl(url)
305
+
.buildOrThrow();
306
+
307
+
await new Promise((resolve) => setTimeout(resolve, 1000));
308
+
const card3 = new CardBuilder()
309
+
.withCuratorId(curator3.value)
310
+
.withType(CardTypeEnum.URL)
311
+
.withUrl(url)
312
+
.buildOrThrow();
313
+
314
+
card1.addToLibrary(curator1);
315
+
card2.addToLibrary(curator2);
316
+
card3.addToLibrary(curator3);
317
+
318
+
// Save cards with slight delays to ensure different timestamps
319
+
await cardRepository.save(card1);
320
+
await new Promise((resolve) => setTimeout(resolve, 10));
321
+
await cardRepository.save(card2);
322
+
await new Promise((resolve) => setTimeout(resolve, 10));
323
+
await cardRepository.save(card3);
324
+
325
+
const result = await queryRepository.getLibrariesForUrl(testUrl, {
326
+
page: 1,
327
+
limit: 10,
328
+
sortBy: CardSortField.CREATED_AT,
329
+
sortOrder: SortOrder.DESC,
330
+
});
331
+
332
+
expect(result.items).toHaveLength(3);
333
+
334
+
// Should be sorted by creation time, newest first
335
+
const cardIds = result.items.map((lib) => lib.card.id);
336
+
expect(cardIds[0]).toBe(card3.cardId.getStringValue()); // Most recent
337
+
expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Middle
338
+
expect(cardIds[2]).toBe(card1.cardId.getStringValue()); // Oldest
339
+
});
340
+
341
+
it('should sort by createdAt in ascending order when specified', async () => {
342
+
const testUrl = 'https://example.com/sort-asc-test';
343
+
const url = URL.create(testUrl).unwrap();
344
+
345
+
// Create cards with different creation times
346
+
const card1 = new CardBuilder()
347
+
.withCuratorId(curator1.value)
348
+
.withType(CardTypeEnum.URL)
349
+
.withUrl(url)
350
+
.buildOrThrow();
351
+
352
+
const card2 = new CardBuilder()
353
+
.withCuratorId(curator2.value)
354
+
.withType(CardTypeEnum.URL)
355
+
.withUrl(url)
356
+
.buildOrThrow();
357
+
358
+
card1.addToLibrary(curator1);
359
+
card2.addToLibrary(curator2);
360
+
361
+
// Save cards with slight delay to ensure different timestamps
362
+
await cardRepository.save(card1);
363
+
await new Promise((resolve) => setTimeout(resolve, 10));
364
+
await cardRepository.save(card2);
365
+
366
+
const result = await queryRepository.getLibrariesForUrl(testUrl, {
367
+
page: 1,
368
+
limit: 10,
369
+
sortBy: CardSortField.CREATED_AT,
370
+
sortOrder: SortOrder.ASC,
371
+
});
372
+
373
+
expect(result.items).toHaveLength(2);
374
+
375
+
// Should be sorted by creation time, oldest first
376
+
const cardIds = result.items.map((lib) => lib.card.id);
377
+
expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Oldest
378
+
expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Newest
379
+
});
380
+
381
+
it('should sort by updatedAt in descending order', async () => {
382
+
const testUrl = 'https://example.com/sort-updated-test';
383
+
const url = URL.create(testUrl).unwrap();
384
+
385
+
// Create cards
386
+
const card1 = new CardBuilder()
387
+
.withCuratorId(curator1.value)
388
+
.withType(CardTypeEnum.URL)
389
+
.withUrl(url)
390
+
.buildOrThrow();
391
+
392
+
const card2 = new CardBuilder()
393
+
.withCuratorId(curator2.value)
394
+
.withType(CardTypeEnum.URL)
395
+
.withUrl(url)
396
+
.buildOrThrow();
397
+
398
+
card1.addToLibrary(curator1);
399
+
card2.addToLibrary(curator2);
400
+
401
+
// Save cards
402
+
await cardRepository.save(card1);
403
+
await cardRepository.save(card2);
404
+
405
+
// Update card1 to have a more recent updatedAt
406
+
await new Promise((resolve) => setTimeout(resolve, 1000));
407
+
card1.markAsPublished(
408
+
PublishedRecordId.create({
409
+
uri: 'at://did:plc:publishedrecord1',
410
+
cid: 'bafyreicpublishedrecord1',
411
+
}),
412
+
);
413
+
await cardRepository.save(card1); // This should update the updatedAt timestamp
414
+
415
+
const result = await queryRepository.getLibrariesForUrl(testUrl, {
416
+
page: 1,
417
+
limit: 10,
418
+
sortBy: CardSortField.UPDATED_AT,
419
+
sortOrder: SortOrder.DESC,
420
+
});
421
+
422
+
expect(result.items).toHaveLength(2);
423
+
424
+
// card1 should be first since it was updated more recently
425
+
const cardIds = result.items.map((lib) => lib.card.id);
426
+
expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Most recently updated
427
+
expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Less recently updated
428
+
});
429
+
430
+
it('should sort by libraryCount in descending order', async () => {
431
+
const testUrl = 'https://example.com/sort-library-count-test';
432
+
const url = URL.create(testUrl).unwrap();
433
+
434
+
// Create cards
435
+
const card1 = new CardBuilder()
436
+
.withCuratorId(curator1.value)
437
+
.withType(CardTypeEnum.URL)
438
+
.withUrl(url)
439
+
.buildOrThrow();
440
+
441
+
const card2 = new CardBuilder()
442
+
.withCuratorId(curator2.value)
443
+
.withType(CardTypeEnum.URL)
444
+
.withUrl(url)
445
+
.buildOrThrow();
446
+
447
+
const card3 = new CardBuilder()
448
+
.withCuratorId(curator3.value)
449
+
.withType(CardTypeEnum.URL)
450
+
.withUrl(url)
451
+
.buildOrThrow();
452
+
453
+
// Add cards to libraries with different counts
454
+
card1.addToLibrary(curator1);
455
+
456
+
card2.addToLibrary(curator2);
457
+
card2.addToLibrary(curator1); // card2 has 2 library memberships
458
+
459
+
card3.addToLibrary(curator3);
460
+
card3.addToLibrary(curator1); // card3 has 3 library memberships
461
+
card3.addToLibrary(curator2);
462
+
463
+
await cardRepository.save(card1);
464
+
await cardRepository.save(card2);
465
+
await cardRepository.save(card3);
466
+
467
+
const result = await queryRepository.getLibrariesForUrl(testUrl, {
468
+
page: 1,
469
+
limit: 10,
470
+
sortBy: CardSortField.LIBRARY_COUNT,
471
+
sortOrder: SortOrder.DESC,
472
+
});
473
+
474
+
// Should return all library memberships, but sorted by the card's library count
475
+
expect(result.items.length).toBeGreaterThan(0);
476
+
477
+
// Group by card ID to check sorting
478
+
const cardGroups = new Map<string, any[]>();
479
+
result.items.forEach((item) => {
480
+
const cardId = item.card.id;
481
+
if (!cardGroups.has(cardId)) {
482
+
cardGroups.set(cardId, []);
483
+
}
484
+
cardGroups.get(cardId)!.push(item);
485
+
});
486
+
487
+
// Get the first occurrence of each card to check library count ordering
488
+
const uniqueCards = Array.from(cardGroups.entries()).map(
489
+
([cardId, items]) => ({
490
+
cardId,
491
+
libraryCount: items[0]!.card.libraryCount,
492
+
}),
493
+
);
494
+
495
+
// Should be sorted by library count descending
496
+
for (let i = 0; i < uniqueCards.length - 1; i++) {
497
+
expect(uniqueCards[i]!.libraryCount).toBeGreaterThanOrEqual(
498
+
uniqueCards[i + 1]!.libraryCount,
499
+
);
500
+
}
501
+
});
502
+
503
+
it('should sort by libraryCount in ascending order when specified', async () => {
504
+
const testUrl = 'https://example.com/sort-library-count-asc-test';
505
+
const url = URL.create(testUrl).unwrap();
506
+
507
+
// Create cards with different library counts
508
+
const card1 = new CardBuilder()
509
+
.withCuratorId(curator1.value)
510
+
.withType(CardTypeEnum.URL)
511
+
.withUrl(url)
512
+
.buildOrThrow();
513
+
514
+
const card2 = new CardBuilder()
515
+
.withCuratorId(curator2.value)
516
+
.withType(CardTypeEnum.URL)
517
+
.withUrl(url)
518
+
.buildOrThrow();
519
+
520
+
// card1 has 1 library membership, card2 has 2
521
+
card1.addToLibrary(curator1);
522
+
card2.addToLibrary(curator2);
523
+
card2.addToLibrary(curator1);
524
+
525
+
await cardRepository.save(card1);
526
+
await cardRepository.save(card2);
527
+
528
+
const result = await queryRepository.getLibrariesForUrl(testUrl, {
529
+
page: 1,
530
+
limit: 10,
531
+
sortBy: CardSortField.LIBRARY_COUNT,
532
+
sortOrder: SortOrder.ASC,
533
+
});
534
+
535
+
expect(result.items.length).toBeGreaterThan(0);
536
+
537
+
// Group by card ID and check ascending order
538
+
const cardGroups = new Map<string, any[]>();
539
+
result.items.forEach((item) => {
540
+
const cardId = item.card.id;
541
+
if (!cardGroups.has(cardId)) {
542
+
cardGroups.set(cardId, []);
543
+
}
544
+
cardGroups.get(cardId)!.push(item);
545
+
});
546
+
547
+
const uniqueCards = Array.from(cardGroups.entries()).map(
548
+
([cardId, items]) => ({
549
+
cardId,
550
+
libraryCount: items[0]!.card.libraryCount,
551
+
}),
552
+
);
553
+
554
+
// Should be sorted by library count ascending
555
+
for (let i = 0; i < uniqueCards.length - 1; i++) {
556
+
expect(uniqueCards[i]!.libraryCount).toBeLessThanOrEqual(
557
+
uniqueCards[i + 1]!.libraryCount,
558
+
);
559
+
}
284
560
});
285
561
});
286
562