You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

196 lines
5.0 KiB

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<Set<number>> {
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<number>`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<number, { id: number; url: string; sortOrder: number }[]>();
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<number, { id: number; name: string; slug: string }[]>();
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) ?? [],
articles: [],
}));
return {
items,
total,
page,
pageSize,
hasMore: page * pageSize < total,
};
}