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.
 
 
 
 

142 lines
3.4 KiB

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<CategoryTreeNode[]> {
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<string, CategoryTreeNode>();
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;
}