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
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;
|
|
}
|
|
|