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.
This commit is contained in:
1 parent
9d1c71d926
commit
56061e41d4
52 files changed
+5825
-748
No files matched your search
@@ -1,27 +1,41 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const findUnique = vi.hoisted(() => vi.fn());
|
||||
const findMany = vi.hoisted(() => vi.fn());
|
||||
const update = vi.hoisted(() => vi.fn());
|
||||
const create = vi.hoisted(() => vi.fn());
|
||||
const queryRaw = vi.hoisted(() => vi.fn());
|
||||
const executeRaw = vi.hoisted(() => vi.fn());
|
||||
const catalogDelete = vi.hoisted(() => vi.fn());
|
||||
const catalogUpdateMany = vi.hoisted(() => vi.fn());
|
||||
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/prisma", () => ({
|
||||
prisma: {
|
||||
catalogPages: {
|
||||
findUnique,
|
||||
findMany,
|
||||
update,
|
||||
create,
|
||||
delete: catalogDelete,
|
||||
updateMany: catalogUpdateMany,
|
||||
},
|
||||
$queryRaw: queryRaw,
|
||||
$executeRaw: executeRaw,
|
||||
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: {} }));
|
||||
@@ -121,10 +135,10 @@ describe("buildNestedTree", () => {
|
||||
|
||||
describe("createPage", () => {
|
||||
it("creates page with default values", async () => {
|
||||
create.mockResolvedValue({ id: 1 });
|
||||
insertValues.mockResolvedValue([{ insertId: 1 }]);
|
||||
await createPage({ parentId: 0, caption: "Test" });
|
||||
|
||||
const data = create.mock.calls[0][0].data;
|
||||
const data = insertValues.mock.calls[0][0];
|
||||
expect(data.caption).toBe("Test");
|
||||
expect(data.pageLayout).toBe("default_3x3");
|
||||
expect(data.visible).toBe("1");
|
||||
@@ -134,57 +148,51 @@ describe("createPage", () => {
|
||||
});
|
||||
|
||||
it("truncates captionSave to 25 chars", async () => {
|
||||
create.mockResolvedValue({ id: 1 });
|
||||
insertValues.mockResolvedValue([{ insertId: 1 }]);
|
||||
await createPage({ parentId: 0, caption: "a".repeat(50) });
|
||||
|
||||
expect(create.mock.calls[0][0].data.captionSave).toBe("a".repeat(25));
|
||||
expect(insertValues.mock.calls[0][0].captionSave).toBe("a".repeat(25));
|
||||
});
|
||||
|
||||
it("converts boolean visible/enabled to string", async () => {
|
||||
create.mockResolvedValue({ id: 1 });
|
||||
insertValues.mockResolvedValue([{ insertId: 1 }]);
|
||||
await createPage({
|
||||
parentId: 0,
|
||||
caption: "x",
|
||||
visible: false,
|
||||
enabled: false,
|
||||
});
|
||||
expect(create.mock.calls[0][0].data.visible).toBe("0");
|
||||
expect(create.mock.calls[0][0].data.enabled).toBe("0");
|
||||
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 () => {
|
||||
findUnique.mockResolvedValue({ parentId: 1 });
|
||||
selectLimit.mockResolvedValue([{ parentId: 1 }]);
|
||||
await expect(movePage(1, 2)).rejects.toThrow("circular hierarchy");
|
||||
});
|
||||
|
||||
it("allows valid move to root", async () => {
|
||||
update.mockResolvedValue({});
|
||||
updateWhere.mockResolvedValue([{ affectedRows: 1 }]);
|
||||
await movePage(1, 0);
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { parentId: 0 },
|
||||
});
|
||||
expect(updateSetData.mock.calls[0][0]).toEqual({ parentId: 0 });
|
||||
});
|
||||
|
||||
it("allows valid move to other parent", async () => {
|
||||
findUnique.mockResolvedValue({ parentId: 0 });
|
||||
update.mockResolvedValue({});
|
||||
selectLimit.mockResolvedValue([{ parentId: 0 }]);
|
||||
updateWhere.mockResolvedValue([{ affectedRows: 1 }]);
|
||||
await movePage(1, 2);
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { parentId: 2 },
|
||||
});
|
||||
expect(updateSetData.mock.calls[0][0]).toEqual({ parentId: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAncestors", () => {
|
||||
it("returns breadcrumb from root to page", async () => {
|
||||
findUnique
|
||||
.mockResolvedValueOnce({ id: 3, caption: "Grandchild", parentId: 2 })
|
||||
.mockResolvedValueOnce({ id: 2, caption: "Child", parentId: 1 })
|
||||
.mockResolvedValueOnce({ id: 1, caption: "Root", parentId: 0 });
|
||||
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);
|
||||
@@ -194,22 +202,19 @@ describe("getAncestors", () => {
|
||||
});
|
||||
|
||||
it("stops when page not found", async () => {
|
||||
findUnique.mockResolvedValue(null);
|
||||
selectLimit.mockResolvedValue([]);
|
||||
expect(await getAncestors(999)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deletePage", () => {
|
||||
it("reparent mode moves children to parent", async () => {
|
||||
findUnique.mockResolvedValue({ parentId: 1 });
|
||||
catalogDelete.mockResolvedValue({});
|
||||
catalogUpdateMany.mockResolvedValue({ count: 2 });
|
||||
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(catalogUpdateMany).toHaveBeenCalledWith({
|
||||
where: { parentId: 5 },
|
||||
data: { parentId: 1 },
|
||||
});
|
||||
expect(updateSetData.mock.calls[0][0]).toEqual({ parentId: 1 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user