···1515import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository';
1616import { createTestSchema } from '../test-utils/createTestSchema';
1717import { CardTypeEnum } from '../../domain/value-objects/CardType';
1818+import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId';
18191920describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => {
2021 let container: StartedPostgreSqlContainer;
···281282 // Should return empty since card is not in any library
282283 expect(result.items).toHaveLength(0);
283284 expect(result.totalCount).toBe(0);
285285+ });
286286+ });
287287+288288+ describe('sorting', () => {
289289+ it('should sort by createdAt in descending order by default', async () => {
290290+ const testUrl = 'https://example.com/sort-test';
291291+ const url = URL.create(testUrl).unwrap();
292292+293293+ // Create cards with different creation times
294294+ const card1 = new CardBuilder()
295295+ .withCuratorId(curator1.value)
296296+ .withType(CardTypeEnum.URL)
297297+ .withUrl(url)
298298+ .buildOrThrow();
299299+300300+ await new Promise((resolve) => setTimeout(resolve, 1000));
301301+ const card2 = new CardBuilder()
302302+ .withCuratorId(curator2.value)
303303+ .withType(CardTypeEnum.URL)
304304+ .withUrl(url)
305305+ .buildOrThrow();
306306+307307+ await new Promise((resolve) => setTimeout(resolve, 1000));
308308+ const card3 = new CardBuilder()
309309+ .withCuratorId(curator3.value)
310310+ .withType(CardTypeEnum.URL)
311311+ .withUrl(url)
312312+ .buildOrThrow();
313313+314314+ card1.addToLibrary(curator1);
315315+ card2.addToLibrary(curator2);
316316+ card3.addToLibrary(curator3);
317317+318318+ // Save cards with slight delays to ensure different timestamps
319319+ await cardRepository.save(card1);
320320+ await new Promise((resolve) => setTimeout(resolve, 10));
321321+ await cardRepository.save(card2);
322322+ await new Promise((resolve) => setTimeout(resolve, 10));
323323+ await cardRepository.save(card3);
324324+325325+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
326326+ page: 1,
327327+ limit: 10,
328328+ sortBy: CardSortField.CREATED_AT,
329329+ sortOrder: SortOrder.DESC,
330330+ });
331331+332332+ expect(result.items).toHaveLength(3);
333333+334334+ // Should be sorted by creation time, newest first
335335+ const cardIds = result.items.map((lib) => lib.card.id);
336336+ expect(cardIds[0]).toBe(card3.cardId.getStringValue()); // Most recent
337337+ expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Middle
338338+ expect(cardIds[2]).toBe(card1.cardId.getStringValue()); // Oldest
339339+ });
340340+341341+ it('should sort by createdAt in ascending order when specified', async () => {
342342+ const testUrl = 'https://example.com/sort-asc-test';
343343+ const url = URL.create(testUrl).unwrap();
344344+345345+ // Create cards with different creation times
346346+ const card1 = new CardBuilder()
347347+ .withCuratorId(curator1.value)
348348+ .withType(CardTypeEnum.URL)
349349+ .withUrl(url)
350350+ .buildOrThrow();
351351+352352+ const card2 = new CardBuilder()
353353+ .withCuratorId(curator2.value)
354354+ .withType(CardTypeEnum.URL)
355355+ .withUrl(url)
356356+ .buildOrThrow();
357357+358358+ card1.addToLibrary(curator1);
359359+ card2.addToLibrary(curator2);
360360+361361+ // Save cards with slight delay to ensure different timestamps
362362+ await cardRepository.save(card1);
363363+ await new Promise((resolve) => setTimeout(resolve, 10));
364364+ await cardRepository.save(card2);
365365+366366+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
367367+ page: 1,
368368+ limit: 10,
369369+ sortBy: CardSortField.CREATED_AT,
370370+ sortOrder: SortOrder.ASC,
371371+ });
372372+373373+ expect(result.items).toHaveLength(2);
374374+375375+ // Should be sorted by creation time, oldest first
376376+ const cardIds = result.items.map((lib) => lib.card.id);
377377+ expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Oldest
378378+ expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Newest
379379+ });
380380+381381+ it('should sort by updatedAt in descending order', async () => {
382382+ const testUrl = 'https://example.com/sort-updated-test';
383383+ const url = URL.create(testUrl).unwrap();
384384+385385+ // Create cards
386386+ const card1 = new CardBuilder()
387387+ .withCuratorId(curator1.value)
388388+ .withType(CardTypeEnum.URL)
389389+ .withUrl(url)
390390+ .buildOrThrow();
391391+392392+ const card2 = new CardBuilder()
393393+ .withCuratorId(curator2.value)
394394+ .withType(CardTypeEnum.URL)
395395+ .withUrl(url)
396396+ .buildOrThrow();
397397+398398+ card1.addToLibrary(curator1);
399399+ card2.addToLibrary(curator2);
400400+401401+ // Save cards
402402+ await cardRepository.save(card1);
403403+ await cardRepository.save(card2);
404404+405405+ // Update card1 to have a more recent updatedAt
406406+ await new Promise((resolve) => setTimeout(resolve, 1000));
407407+ card1.markAsPublished(
408408+ PublishedRecordId.create({
409409+ uri: 'at://did:plc:publishedrecord1',
410410+ cid: 'bafyreicpublishedrecord1',
411411+ }),
412412+ );
413413+ await cardRepository.save(card1); // This should update the updatedAt timestamp
414414+415415+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
416416+ page: 1,
417417+ limit: 10,
418418+ sortBy: CardSortField.UPDATED_AT,
419419+ sortOrder: SortOrder.DESC,
420420+ });
421421+422422+ expect(result.items).toHaveLength(2);
423423+424424+ // card1 should be first since it was updated more recently
425425+ const cardIds = result.items.map((lib) => lib.card.id);
426426+ expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Most recently updated
427427+ expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Less recently updated
428428+ });
429429+430430+ it('should sort by libraryCount in descending order', async () => {
431431+ const testUrl = 'https://example.com/sort-library-count-test';
432432+ const url = URL.create(testUrl).unwrap();
433433+434434+ // Create cards
435435+ const card1 = new CardBuilder()
436436+ .withCuratorId(curator1.value)
437437+ .withType(CardTypeEnum.URL)
438438+ .withUrl(url)
439439+ .buildOrThrow();
440440+441441+ const card2 = new CardBuilder()
442442+ .withCuratorId(curator2.value)
443443+ .withType(CardTypeEnum.URL)
444444+ .withUrl(url)
445445+ .buildOrThrow();
446446+447447+ const card3 = new CardBuilder()
448448+ .withCuratorId(curator3.value)
449449+ .withType(CardTypeEnum.URL)
450450+ .withUrl(url)
451451+ .buildOrThrow();
452452+453453+ // Add cards to libraries with different counts
454454+ card1.addToLibrary(curator1);
455455+456456+ card2.addToLibrary(curator2);
457457+ card2.addToLibrary(curator1); // card2 has 2 library memberships
458458+459459+ card3.addToLibrary(curator3);
460460+ card3.addToLibrary(curator1); // card3 has 3 library memberships
461461+ card3.addToLibrary(curator2);
462462+463463+ await cardRepository.save(card1);
464464+ await cardRepository.save(card2);
465465+ await cardRepository.save(card3);
466466+467467+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
468468+ page: 1,
469469+ limit: 10,
470470+ sortBy: CardSortField.LIBRARY_COUNT,
471471+ sortOrder: SortOrder.DESC,
472472+ });
473473+474474+ // Should return all library memberships, but sorted by the card's library count
475475+ expect(result.items.length).toBeGreaterThan(0);
476476+477477+ // Group by card ID to check sorting
478478+ const cardGroups = new Map<string, any[]>();
479479+ result.items.forEach((item) => {
480480+ const cardId = item.card.id;
481481+ if (!cardGroups.has(cardId)) {
482482+ cardGroups.set(cardId, []);
483483+ }
484484+ cardGroups.get(cardId)!.push(item);
485485+ });
486486+487487+ // Get the first occurrence of each card to check library count ordering
488488+ const uniqueCards = Array.from(cardGroups.entries()).map(
489489+ ([cardId, items]) => ({
490490+ cardId,
491491+ libraryCount: items[0]!.card.libraryCount,
492492+ }),
493493+ );
494494+495495+ // Should be sorted by library count descending
496496+ for (let i = 0; i < uniqueCards.length - 1; i++) {
497497+ expect(uniqueCards[i]!.libraryCount).toBeGreaterThanOrEqual(
498498+ uniqueCards[i + 1]!.libraryCount,
499499+ );
500500+ }
501501+ });
502502+503503+ it('should sort by libraryCount in ascending order when specified', async () => {
504504+ const testUrl = 'https://example.com/sort-library-count-asc-test';
505505+ const url = URL.create(testUrl).unwrap();
506506+507507+ // Create cards with different library counts
508508+ const card1 = new CardBuilder()
509509+ .withCuratorId(curator1.value)
510510+ .withType(CardTypeEnum.URL)
511511+ .withUrl(url)
512512+ .buildOrThrow();
513513+514514+ const card2 = new CardBuilder()
515515+ .withCuratorId(curator2.value)
516516+ .withType(CardTypeEnum.URL)
517517+ .withUrl(url)
518518+ .buildOrThrow();
519519+520520+ // card1 has 1 library membership, card2 has 2
521521+ card1.addToLibrary(curator1);
522522+ card2.addToLibrary(curator2);
523523+ card2.addToLibrary(curator1);
524524+525525+ await cardRepository.save(card1);
526526+ await cardRepository.save(card2);
527527+528528+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
529529+ page: 1,
530530+ limit: 10,
531531+ sortBy: CardSortField.LIBRARY_COUNT,
532532+ sortOrder: SortOrder.ASC,
533533+ });
534534+535535+ expect(result.items.length).toBeGreaterThan(0);
536536+537537+ // Group by card ID and check ascending order
538538+ const cardGroups = new Map<string, any[]>();
539539+ result.items.forEach((item) => {
540540+ const cardId = item.card.id;
541541+ if (!cardGroups.has(cardId)) {
542542+ cardGroups.set(cardId, []);
543543+ }
544544+ cardGroups.get(cardId)!.push(item);
545545+ });
546546+547547+ const uniqueCards = Array.from(cardGroups.entries()).map(
548548+ ([cardId, items]) => ({
549549+ cardId,
550550+ libraryCount: items[0]!.card.libraryCount,
551551+ }),
552552+ );
553553+554554+ // Should be sorted by library count ascending
555555+ for (let i = 0; i < uniqueCards.length - 1; i++) {
556556+ expect(uniqueCards[i]!.libraryCount).toBeLessThanOrEqual(
557557+ uniqueCards[i + 1]!.libraryCount,
558558+ );
559559+ }
284560 });
285561 });
286562
-2
src/webapp/app/bookmarklet/page.tsx
···1515 Anchor,
1616 CopyButton,
1717} from '@mantine/core';
1818-import { useState } from 'react';
1919-import { BiInfoCircle } from 'react-icons/bi';
2018import SembleLogo from '@/assets/semble-logo.svg';
2119import Link from 'next/link';
2220