import { dbGlobal } from "drizzle-pkg/lib/db"; import { favorites, cards, cardImages, cardTags, tags } from "drizzle-pkg/lib/schema/content"; import { eq, and, inArray, desc, sql, asc } from "drizzle-orm"; import type { CardWithRelations } from "../card"; // ============ Types ============ export interface ListFavoritesOptions { page?: number pageSize?: number } // ============ Toggle ============ /** * Toggle favorite status for a card. * Returns the new favorited state. */ export async function toggleFavorite( userId: number, cardId: number, ): Promise<{ favorited: boolean }> { const [existing] = await dbGlobal .select() .from(favorites) .where( and( eq(favorites.userId, userId), eq(favorites.cardId, cardId), ), ) .limit(1); if (existing) { await dbGlobal .delete(favorites) .where( and( eq(favorites.userId, userId), eq(favorites.cardId, cardId), ), ); return { favorited: false }; } await dbGlobal.insert(favorites).values({ userId, cardId, }); return { favorited: true }; } // ============ Batch check ============ /** * Given a list of card IDs, return which ones the user has favorited. */ export async function batchIsFavorited( userId: number, cardIds: number[], ): Promise> { if (cardIds.length === 0) return new Set(); const rows = await dbGlobal .select({ cardId: favorites.cardId }) .from(favorites) .where( and( eq(favorites.userId, userId), inArray(favorites.cardId, cardIds), ), ); return new Set(rows.map((r) => r.cardId)); } // ============ List ============ /** * List favorited card IDs for a user. */ export async function listFavoriteCardIds( userId: number, opts: ListFavoritesOptions = {}, ): Promise<{ cardIds: number[]; total: number }> { const page = opts.page ?? 1; const pageSize = opts.pageSize ?? 12; const [rows, countResult] = await Promise.all([ dbGlobal .select({ cardId: favorites.cardId }) .from(favorites) .where(eq(favorites.userId, userId)) .orderBy(desc(favorites.createdAt)) .limit(pageSize) .offset((page - 1) * pageSize), dbGlobal .select({ count: sql`count(*)` }) .from(favorites) .where(eq(favorites.userId, userId)), ]); return { cardIds: rows.map((r) => r.cardId), total: countResult[0]?.count ?? 0, }; } /** * List favorited cards with full card data. */ export async function listFavoriteCards( userId: number, opts: ListFavoritesOptions = {}, ): Promise<{ items: CardWithRelations[]; total: number; page: number; pageSize: number; hasMore: boolean }> { const page = opts.page ?? 1; const pageSize = opts.pageSize ?? 12; const { cardIds, total } = await listFavoriteCardIds(userId, { page, pageSize }); if (cardIds.length === 0) { return { items: [], total, page, pageSize, hasMore: false }; } // Fetch full card data via existing listCards with id filter // Build a simple query with the IDs const rows = await dbGlobal .select() .from(cards) .where(inArray(cards.id, cardIds)) .orderBy(desc(cards.createdAt)); // Load relations const allImages = await dbGlobal .select() .from(cardImages) .where(inArray(cardImages.cardId, cardIds)); const allCardTags = await dbGlobal .select() .from(cardTags) .where(inArray(cardTags.cardId, cardIds)); const tagIds = [...new Set(allCardTags.map((ct) => ct.tagId))]; const tagRows = tagIds.length > 0 ? await dbGlobal.select().from(tags).where(inArray(tags.id, tagIds)) : []; const tagMap = new Map(tagRows.map((t) => [t.id, t])); const imagesByCard = new Map(); for (const img of allImages) { if (!imagesByCard.has(img.cardId)) imagesByCard.set(img.cardId, []); imagesByCard.get(img.cardId)!.push({ id: img.id, url: img.url, sortOrder: img.sortOrder, }); } const tagsByCard = new Map(); for (const ct of allCardTags) { const tag = tagMap.get(ct.tagId); if (!tag) continue; if (!tagsByCard.has(ct.cardId)) tagsByCard.set(ct.cardId, []); tagsByCard.get(ct.cardId)!.push({ id: tag.id, name: tag.name, slug: tag.slug, }); } const items: CardWithRelations[] = rows .filter((row) => cardIds.includes(row.id)) .sort((a, b) => cardIds.indexOf(a.id) - cardIds.indexOf(b.id)) .map((row) => ({ id: row.id, type: row.type as CardWithRelations["type"], title: row.title, description: row.description, aspectRatio: row.aspectRatio, categoryId: row.categoryId, createdAt: row.createdAt, updatedAt: row.updatedAt, images: imagesByCard.get(row.id) ?? [], tags: tagsByCard.get(row.id) ?? [], })); return { items, total, page, pageSize, hasMore: page * pageSize < total, }; }