Files
EpicNext-Cms/src/lib/services/catalog-tree.test.ts
T
openhands 56061e41d4
CI / check (push) Failing after 12s
CI / deploy (push) Skipped
CI / release (push) Skipped
refactor: replace Prisma ORM runtime with Drizzle ORM facade
- Replace Prisma client runtime with Drizzle ORM (zero Prisma engine/query engine in production)
- Add Prisma-compatible facade (@/lib/prisma-facade.ts) backed by Drizzle for backwards compatibility
- Runtime queries route through Drizzle ORM; @prisma/client is now devDependency (types only)
- Remove @prisma/adapter-mariadb dependency; delete prisma-pool.ts and types/prisma.ts
- New Drizzle schema layer: src/db/schema.ts (176 tables) and src/lib/db.ts (connection)
- Update README documenting the dual-layer ORM architecture
- Restore src/generated/ gitignore (build artifact for local type generation)
- 0 TypeScript errors, 583 tests passing

The facade intentionally uses `any` types to match the Prisma Client API surface,
allowing existing code to run unmodified while routing queries through Drizzle at runtime.
2026-07-31 14:11:03 +02:00

221 lines
5.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const insertValues = vi.hoisted(() => vi.fn());
const selectLimit = vi.hoisted(() => vi.fn());
const updateSetData = vi.hoisted(() => vi.fn());
const updateWhere = vi.hoisted(() => vi.fn());
const deleteWhere = vi.hoisted(() => vi.fn());
const executeMock = vi.hoisted(() => vi.fn());
vi.mock("@/lib/db", () => ({
db: {
insert: () => ({ values: insertValues }),
select: () => ({
from: () => ({
where: () => ({
limit: () => Promise.resolve(selectLimit()),
}),
orderBy: () => ({
where: () => ({
limit: () => Promise.resolve(selectLimit()),
}),
}),
}),
}),
update: () => ({
set: (data: Record<string, unknown>) => {
updateSetData(data);
return {
where: () => updateWhere(data),
};
},
}),
delete: () => ({
where: () => Promise.resolve(deleteWhere()),
}),
execute: executeMock,
},
CatalogPages: { id: "CatalogPages.id", parentId: "CatalogPages.parentId" },
}));
vi.mock("@/env", () => ({ env: {} }));
import type { TreeNode } from "@/types/catalog";
import {
buildNestedTree,
createPage,
deletePage,
getAncestors,
movePage,
toInt,
} from "./catalog-tree";
beforeEach(() => {
vi.clearAllMocks();
});
describe("toInt", () => {
it("converts valid numbers", () => {
expect(toInt(42)).toBe(42);
expect(toInt("42")).toBe(42);
});
it("uses fallback for invalid", () => {
expect(toInt("abc", -1)).toBe(-1);
expect(toInt(NaN, 0)).toBe(0);
});
it("defaults fallback to 0", () => {
expect(toInt("abc")).toBe(0);
});
});
describe("buildNestedTree", () => {
const flat: TreeNode[] = [
{
id: 1,
caption: "Root",
parentId: -1,
depth: 0,
orderNum: 1,
enabled: "1",
visible: "1",
iconImage: 0,
iconColor: 0,
pageLayout: "default_3x3",
childCount: 2,
itemCount: 0,
},
{
id: 2,
caption: "Child",
parentId: 1,
depth: 1,
orderNum: 1,
enabled: "1",
visible: "1",
iconImage: 0,
iconColor: 0,
pageLayout: "default_3x3",
childCount: 1,
itemCount: 5,
},
{
id: 3,
caption: "Grandchild",
parentId: 2,
depth: 2,
orderNum: 1,
enabled: "1",
visible: "1",
iconImage: 0,
iconColor: 0,
pageLayout: "default_3x3",
childCount: 0,
itemCount: 0,
},
];
it("builds nested tree from flat nodes", () => {
const tree = buildNestedTree(flat);
expect(tree).toHaveLength(1);
expect(tree[0].id).toBe(1);
expect(tree[0].children).toHaveLength(1);
expect(tree[0].children[0].children[0].id).toBe(3);
});
it("returns empty array for empty input", () => {
expect(buildNestedTree([])).toEqual([]);
});
it("uses custom rootParentId", () => {
expect(buildNestedTree(flat, 999)).toHaveLength(0);
});
});
describe("createPage", () => {
it("creates page with default values", async () => {
insertValues.mockResolvedValue([{ insertId: 1 }]);
await createPage({ parentId: 0, caption: "Test" });
const data = insertValues.mock.calls[0][0];
expect(data.caption).toBe("Test");
expect(data.pageLayout).toBe("default_3x3");
expect(data.visible).toBe("1");
expect(data.enabled).toBe("1");
expect(data.clubOnly).toBe("0");
expect(data.minRank).toBe(1);
});
it("truncates captionSave to 25 chars", async () => {
insertValues.mockResolvedValue([{ insertId: 1 }]);
await createPage({ parentId: 0, caption: "a".repeat(50) });
expect(insertValues.mock.calls[0][0].captionSave).toBe("a".repeat(25));
});
it("converts boolean visible/enabled to string", async () => {
insertValues.mockResolvedValue([{ insertId: 1 }]);
await createPage({
parentId: 0,
caption: "x",
visible: false,
enabled: false,
});
expect(insertValues.mock.calls[0][0].visible).toBe("0");
expect(insertValues.mock.calls[0][0].enabled).toBe("0");
});
});
describe("movePage", () => {
it("throws on circular hierarchy", async () => {
selectLimit.mockResolvedValue([{ parentId: 1 }]);
await expect(movePage(1, 2)).rejects.toThrow("circular hierarchy");
});
it("allows valid move to root", async () => {
updateWhere.mockResolvedValue([{ affectedRows: 1 }]);
await movePage(1, 0);
expect(updateSetData.mock.calls[0][0]).toEqual({ parentId: 0 });
});
it("allows valid move to other parent", async () => {
selectLimit.mockResolvedValue([{ parentId: 0 }]);
updateWhere.mockResolvedValue([{ affectedRows: 1 }]);
await movePage(1, 2);
expect(updateSetData.mock.calls[0][0]).toEqual({ parentId: 2 });
});
});
describe("getAncestors", () => {
it("returns breadcrumb from root to page", async () => {
selectLimit
.mockResolvedValueOnce([{ id: 3, caption: "Grandchild", parentId: 2 }])
.mockResolvedValueOnce([{ id: 2, caption: "Child", parentId: 1 }])
.mockResolvedValueOnce([{ id: 1, caption: "Root", parentId: 0 }]);
const ancestors = await getAncestors(3);
expect(ancestors).toHaveLength(3);
expect(ancestors[0].caption).toBe("Root");
expect(ancestors[1].caption).toBe("Child");
expect(ancestors[2].caption).toBe("Grandchild");
});
it("stops when page not found", async () => {
selectLimit.mockResolvedValue([]);
expect(await getAncestors(999)).toEqual([]);
});
});
describe("deletePage", () => {
it("reparent mode moves children to parent", async () => {
selectLimit.mockResolvedValue([{ parentId: 1 }]);
updateWhere.mockResolvedValue([{ affectedRows: 2 }]);
deleteWhere.mockResolvedValue([{ affectedRows: 1 }]);
const result = await deletePage(5, "reparent");
expect(result).toEqual({ deletedPages: 1, movedChildren: 2 });
expect(updateSetData.mock.calls[0][0]).toEqual({ parentId: 1 });
});
});