import { dbGlobal } from "drizzle-pkg/lib/db"; import { categories, cards } from "drizzle-pkg/lib/schema/content"; import { eq, asc } from "drizzle-orm"; // ============ Types ============ export interface CategoryTreeNode { id: string; name: string; slug: string; image: string | null; parentId: string | null; sortOrder: number; count: number; createdAt: Date; updatedAt: Date; children: CategoryTreeNode[]; } export interface CreateCategoryInput { name: string; slug: string; image?: string; parentId?: string | null; sortOrder?: number; } export interface UpdateCategoryInput { name?: string; slug?: string; image?: string | null; parentId?: string | null; sortOrder?: number; count?: number; } // ============ 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, slug: input.slug, image: input.image ?? null, parentId: input.parentId ?? null, sortOrder: input.sortOrder ?? 0, count: 0, }); 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.slug !== undefined && { slug: input.slug }), ...(input.image !== undefined && { image: input.image }), ...(input.parentId !== undefined && { parentId: input.parentId }), ...(input.sortOrder !== undefined && { sortOrder: input.sortOrder }), ...(input.count !== undefined && { count: input.count }), 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, slug: row.slug, image: row.image, parentId: row.parentId, sortOrder: row.sortOrder, count: row.count, 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; }