import { dbGlobal } from "drizzle-pkg/lib/db"; import { categories, cards, type CardType } from "drizzle-pkg/lib/schema/content"; import { eq, asc } from "drizzle-orm"; // ============ Types ============ export interface CategoryTreeNode { id: string; name: string; image: string | null; parentId: string | null; sortOrder: number; count: number; defaultCardType: CardType | null; createdAt: Date; updatedAt: Date; children: CategoryTreeNode[]; } export interface CreateCategoryInput { name: string; image?: string; parentId?: string | null; sortOrder?: number; defaultCardType?: CardType | null; } export interface UpdateCategoryInput { name?: string; image?: string | null; parentId?: string | null; sortOrder?: number; count?: number; defaultCardType?: CardType | null; } // ============ Helpers ============ function uuid() { return crypto.randomUUID(); } function now() { return new Date(); } // ============ CRUD ============ export async function listCategories(): Promise { const rows = await dbGlobal .select() .from(categories) .orderBy(asc(categories.sortOrder), asc(categories.createdAt)); return buildTree(rows); } export async function getCategoryById(id: string) { const rows = await dbGlobal .select() .from(categories) .where(eq(categories.id, id)) .limit(1); return rows[0] ?? null; } export async function createCategory(input: CreateCategoryInput) { const id = uuid(); await dbGlobal.insert(categories).values({ id, name: input.name, image: input.image ?? null, parentId: input.parentId ?? null, sortOrder: input.sortOrder ?? 0, count: 0, defaultCardType: input.defaultCardType ?? null, }); return getCategoryById(id); } export async function updateCategory(id: string, input: UpdateCategoryInput) { const existing = await getCategoryById(id); if (!existing) return null; await dbGlobal .update(categories) .set({ ...(input.name !== undefined && { name: input.name }), ...(input.image !== undefined && { image: input.image }), ...(input.parentId !== undefined && { parentId: input.parentId }), ...(input.sortOrder !== undefined && { sortOrder: input.sortOrder }), ...(input.count !== undefined && { count: input.count }), ...(input.defaultCardType !== undefined && { defaultCardType: input.defaultCardType }), updatedAt: now(), }) .where(eq(categories.id, id)); return getCategoryById(id); } export async function deleteCategory(id: string) { // Move cards in this category to uncategorized (null) await dbGlobal .update(cards) .set({ categoryId: null }) .where(eq(cards.categoryId, id)); await dbGlobal.delete(categories).where(eq(categories.id, id)); } // ============ Tree Builder ============ function buildTree( rows: (typeof categories.$inferSelect)[], ): CategoryTreeNode[] { const map = new Map(); const roots: CategoryTreeNode[] = []; // First pass: create nodes for (const row of rows) { map.set(row.id, { id: row.id, name: row.name, image: row.image, parentId: row.parentId, sortOrder: row.sortOrder, count: row.count, defaultCardType: row.defaultCardType, createdAt: row.createdAt, updatedAt: row.updatedAt, children: [], }); } // Second pass: build hierarchy for (const node of map.values()) { if (node.parentId && map.has(node.parentId)) { map.get(node.parentId)!.children.push(node); } else { roots.push(node); } } return roots; }