Something went wrong. Try again.
A social knowledge tool for research trails, built on ATProto semble.so
research-tool social-network tools-for-thought atproto
Something went wrong. Try again.
TypeScript
at development
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455import { ICardQueryRepository, CardQueryOptions, UrlCardQueryResultDTO, CollectionCardQueryResultDTO, UrlCardViewDTO, PaginatedQueryResult, CardSortField, SortOrder, LibraryForUrlDTO, NoteCardForUrlDTO,} from '../../domain/ICardQueryRepository';import { CardTypeEnum } from '../../domain/value-objects/CardType';import { InMemoryCardRepository } from './InMemoryCardRepository';import { InMemoryCollectionRepository } from './InMemoryCollectionRepository';import { Card } from '../../domain/Card';import { CollectionId } from '../../domain/value-objects/CollectionId';import { CuratorId } from '../../domain/value-objects/CuratorId';
export class InMemoryCardQueryRepository implements ICardQueryRepository { constructor( private cardRepository: InMemoryCardRepository, private collectionRepository: InMemoryCollectionRepository, ) {}
async getUrlCardsOfUser( userId: string, options: CardQueryOptions, ): Promise<PaginatedQueryResult<UrlCardQueryResultDTO>> { try { // Get all cards and filter by user's library membership const allCards = this.cardRepository.getAllCards(); const userCards = allCards .filter( (card) => card.isUrlCard && card.isInLibrary(CuratorId.create(userId).unwrap()), ) .map((card) => this.cardToUrlCardQueryResult(card));
// Sort cards const sortedCards = this.sortCards( userCards, options.sortBy, options.sortOrder, );
// Apply pagination const startIndex = (options.page - 1) * options.limit; const endIndex = startIndex + options.limit; const paginatedCards = sortedCards.slice(startIndex, endIndex);
return { items: paginatedCards, totalCount: userCards.length, hasMore: endIndex < userCards.length, }; } catch (error) { throw new Error( `Failed to query URL cards: ${error instanceof Error ? error.message : String(error)}`, ); } }
private sortCards( cards: UrlCardQueryResultDTO[], sortBy: CardSortField, sortOrder: SortOrder, ): UrlCardQueryResultDTO[] { const sorted = [...cards].sort((a, b) => { let comparison = 0;
switch (sortBy) { case CardSortField.CREATED_AT: comparison = a.createdAt.getTime() - b.createdAt.getTime(); break; case CardSortField.UPDATED_AT: comparison = a.updatedAt.getTime() - b.updatedAt.getTime(); break; case CardSortField.LIBRARY_COUNT: comparison = a.libraryCount - b.libraryCount; break; default: comparison = 0; }
return sortOrder === SortOrder.DESC ? -comparison : comparison; });
return sorted; }
private cardToUrlCardQueryResult(card: Card): UrlCardQueryResultDTO { if (!card.isUrlCard || !card.content.urlContent) { throw new Error('Card is not a URL card'); }
// Find collections this card belongs to by querying the collection repository const allCollections = this.collectionRepository.getAllCollections(); const collections: { id: string; name: string; authorId: string }[] = [];
for (const collection of allCollections) { if ( collection.cardIds.some( (cardId) => cardId.getStringValue() === card.cardId.getStringValue(), ) ) { collections.push({ id: collection.collectionId.getStringValue(), name: collection.name.value, authorId: collection.authorId.value, }); } }
// Find note cards with matching URL const allCards = this.cardRepository.getAllCards(); const noteCard = allCards.find( (c) => c.type.value === 'NOTE' && c.url?.value === card.url?.value, );
const note = noteCard ? { id: noteCard.cardId.getStringValue(), text: noteCard.content.noteContent?.text || '', } : undefined;
return { id: card.cardId.getStringValue(), type: CardTypeEnum.URL, url: card.content.urlContent.url.value, cardContent: { url: card.content.urlContent.url.value, title: card.content.urlContent.metadata?.title, description: card.content.urlContent.metadata?.description, author: card.content.urlContent.metadata?.author, thumbnailUrl: card.content.urlContent.metadata?.imageUrl, }, libraryCount: this.getLibraryCountForCard(card.cardId.getStringValue()), createdAt: card.createdAt, updatedAt: card.updatedAt, collections, note, }; }
private getLibraryCountForCard(cardId: string): number { const card = this.cardRepository.getStoredCard({ getStringValue: () => cardId, } as any); return card ? card.libraryMembershipCount : 0; }
async getCardsInCollection( collectionId: string, options: CardQueryOptions, ): Promise<PaginatedQueryResult<CollectionCardQueryResultDTO>> { try { // Get the collection from the repository const collectionIdObj = CollectionId.createFromString(collectionId); if (collectionIdObj.isErr()) { throw new Error(`Invalid collection ID: ${collectionId}`); }
const collectionResult = await this.collectionRepository.findById( collectionIdObj.value, ); if (collectionResult.isErr()) { throw collectionResult.error; }
const collection = collectionResult.value; if (!collection) { return { items: [], totalCount: 0, hasMore: false, }; }
// Get cards that are in this collection const allCards = this.cardRepository.getAllCards(); const collectionCardIds = new Set( collection.cardIds.map((id) => id.getStringValue()), ); const collectionCards = allCards .filter( (card) => collectionCardIds.has(card.cardId.getStringValue()) && card.isUrlCard, ) .map((card) => this.toCollectionCardQueryResult(this.cardToUrlCardQueryResult(card)), );
// Sort cards const sortedCards = this.sortCollectionCards( collectionCards, options.sortBy, options.sortOrder, );
// Apply pagination const startIndex = (options.page - 1) * options.limit; const endIndex = startIndex + options.limit; const paginatedCards = sortedCards.slice(startIndex, endIndex);
return { items: paginatedCards, totalCount: collectionCards.length, hasMore: endIndex < collectionCards.length, }; } catch (error) { throw new Error( `Failed to query collection cards: ${error instanceof Error ? error.message : String(error)}`, ); } }
private sortCollectionCards( cards: CollectionCardQueryResultDTO[], sortBy: CardSortField, sortOrder: SortOrder, ): CollectionCardQueryResultDTO[] { const sorted = [...cards].sort((a, b) => { let comparison = 0;
switch (sortBy) { case CardSortField.CREATED_AT: comparison = a.createdAt.getTime() - b.createdAt.getTime(); break; case CardSortField.UPDATED_AT: comparison = a.updatedAt.getTime() - b.updatedAt.getTime(); break; case CardSortField.LIBRARY_COUNT: comparison = a.libraryCount - b.libraryCount; break; default: comparison = 0; }
return sortOrder === SortOrder.DESC ? -comparison : comparison; });
return sorted; }
private toCollectionCardQueryResult( card: UrlCardQueryResultDTO, ): CollectionCardQueryResultDTO { return { id: card.id, type: CardTypeEnum.URL, url: card.url, cardContent: card.cardContent, libraryCount: card.libraryCount, createdAt: card.createdAt, updatedAt: card.updatedAt, note: card.note, }; }
async getUrlCardView(cardId: string): Promise<UrlCardViewDTO | null> { const allCards = this.cardRepository.getAllCards(); const card = allCards.find((c) => c.cardId.getStringValue() === cardId); if (!card || !card.isUrlCard) { return null; }
const urlCardResult = this.cardToUrlCardQueryResult(card);
// Get library memberships from the card itself const libraries = card.libraryMemberships.map((membership) => ({ userId: membership.curatorId.value, }));
// Find note cards with matching URL const noteCard = allCards.find( (c) => c.type.value === 'NOTE' && c.url?.value === card.url?.value, );
const note = noteCard ? { id: noteCard.cardId.getStringValue(), text: noteCard.content.noteContent?.text || '', } : undefined;
return { ...urlCardResult, libraries, note, }; }
async getLibrariesForCard(cardId: string): Promise<string[]> { const allCards = this.cardRepository.getAllCards(); const card = allCards.find((c) => c.cardId.getStringValue() === cardId);
if (!card) { return []; }
return card.libraryMemberships.map( (membership) => membership.curatorId.value, ); }
async getLibrariesForUrl( url: string, options: CardQueryOptions, ): Promise<PaginatedQueryResult<LibraryForUrlDTO>> { try { // Get all cards and filter by URL const allCards = this.cardRepository.getAllCards(); const urlCards = allCards.filter( (card) => card.isUrlCard && card.url?.value === url, );
// Create library entries for each card const libraries: LibraryForUrlDTO[] = []; for (const card of urlCards) { for (const membership of card.libraryMemberships) { libraries.push({ userId: membership.curatorId.value, cardId: card.cardId.getStringValue(), }); } }
// Sort libraries (by userId for consistency) const sortedLibraries = this.sortLibraries( libraries, options.sortBy, options.sortOrder, );
// Apply pagination const startIndex = (options.page - 1) * options.limit; const endIndex = startIndex + options.limit; const paginatedLibraries = sortedLibraries.slice(startIndex, endIndex);
return { items: paginatedLibraries, totalCount: libraries.length, hasMore: endIndex < libraries.length, }; } catch (error) { throw new Error( `Failed to query libraries for URL: ${error instanceof Error ? error.message : String(error)}`, ); } }
private sortLibraries( libraries: LibraryForUrlDTO[], sortBy: CardSortField, sortOrder: SortOrder, ): LibraryForUrlDTO[] { const sorted = [...libraries].sort((a, b) => { let comparison = 0;
switch (sortBy) { case CardSortField.CREATED_AT: case CardSortField.UPDATED_AT: case CardSortField.LIBRARY_COUNT: // For libraries, we'll sort by userId as a fallback comparison = a.userId.localeCompare(b.userId); break; default: comparison = a.userId.localeCompare(b.userId); }
return sortOrder === SortOrder.DESC ? -comparison : comparison; });
return sorted; }
async getNoteCardsForUrl( url: string, options: CardQueryOptions, ): Promise<PaginatedQueryResult<NoteCardForUrlDTO>> { try { // Get all note cards with the specified URL const allCards = this.cardRepository.getAllCards(); const noteCards = allCards .filter((card) => card.isNoteCard && card.url?.value === url) .map((card) => ({ id: card.cardId.getStringValue(), note: card.content.noteContent?.text || '', authorId: card.curatorId.value, createdAt: card.createdAt, updatedAt: card.updatedAt, }));
// Sort note cards const sortedNotes = this.sortNoteCards( noteCards, options.sortBy, options.sortOrder, );
// Apply pagination const startIndex = (options.page - 1) * options.limit; const endIndex = startIndex + options.limit; const paginatedNotes = sortedNotes.slice(startIndex, endIndex);
return { items: paginatedNotes, totalCount: noteCards.length, hasMore: endIndex < noteCards.length, }; } catch (error) { throw new Error( `Failed to query note cards for URL: ${error instanceof Error ? error.message : String(error)}`, ); } }
private sortNoteCards( notes: NoteCardForUrlDTO[], sortBy: CardSortField, sortOrder: SortOrder, ): NoteCardForUrlDTO[] { const sorted = [...notes].sort((a, b) => { let comparison = 0;
switch (sortBy) { case CardSortField.CREATED_AT: comparison = a.createdAt.getTime() - b.createdAt.getTime(); break; case CardSortField.UPDATED_AT: comparison = a.updatedAt.getTime() - b.updatedAt.getTime(); break; case CardSortField.LIBRARY_COUNT: // For note cards, sort by authorId as fallback comparison = a.authorId.localeCompare(b.authorId); break; default: comparison = 0; }
return sortOrder === SortOrder.DESC ? -comparison : comparison; });
return sorted; }
clear(): void { // No separate state to clear }}