REFACTORED - migrate the data layer from Prisma to Drizzle (unify the stack)

- Add lib/db (Drizzle schema: 7 tables + 6 enums + relations, postgres.js client),
  drizzle.config.ts, lib/db/enums.ts (Prisma-compatible enum objects)
- Rewrite all 14 app consumers + 4 admin components to Drizzle
- Move the test DB layer to Drizzle + in-process PGlite; convert all 8 integration
  test files + factories (371 tests green)
- Remove Prisma: deps, prisma/ schema+migrations, lib/prisma.ts, Dockerfile prisma
  generate; wire db:generate/migrate/push/studio to drizzle-kit; update Makefile
- Preserve the old seed as scripts/legacy-prisma-seed.cjs (needs a Drizzle rewrite)
This commit is contained in:
MOH
2026-08-07 14:18:41 +02:00
parent e377877e7e
commit 0a5f77d8de
48 changed files with 3765 additions and 1358 deletions
+72 -147
View File
@@ -1,74 +1,22 @@
import type {
Category,
PortfolioAsset,
PortfolioProject,
PortfolioProjectViewMode,
PortfolioSection,
} from "@prisma/client";
import { cache } from "react";
import { and, asc, desc, eq } from "drizzle-orm";
import { db } from "@/lib/db";
import {
category as categoryTable,
portfolioAsset,
portfolioProject,
portfolioSection,
} from "@/lib/db/schema";
import type { PortfolioProjectViewMode } from "@/lib/db/enums";
import { getPortfolioMediaBindings } from "@/lib/media";
import type { AppLocale } from "@/lib/locale";
import { prisma } from "@/lib/prisma";
type CategoryRecord = Pick<
Category,
| "id"
| "slug"
| "nameAr"
| "nameEn"
| "nameDe"
| "descriptionAr"
| "descriptionEn"
| "descriptionDe"
| "sortOrder"
| "isActive"
>;
type SectionRecord = Pick<
PortfolioSection,
| "id"
| "type"
| "titleAr"
| "titleEn"
| "titleDe"
| "bodyAr"
| "bodyEn"
| "bodyDe"
| "imagePath"
| "linkUrl"
| "sortOrder"
>;
type AssetRecord = Pick<
PortfolioAsset,
"id" | "kind" | "filePath" | "altAr" | "altEn" | "altDe" | "sortOrder"
>;
type ProjectRecord = Pick<
PortfolioProject,
| "id"
| "slug"
| "viewMode"
| "titleAr"
| "titleEn"
| "titleDe"
| "summaryAr"
| "summaryEn"
| "summaryDe"
| "clientName"
| "projectYear"
| "serviceLabelAr"
| "serviceLabelEn"
| "serviceLabelDe"
| "previewUrl"
| "coverImagePath"
| "isFeatured"
| "isPublished"
| "publishedAt"
| "sortOrder"
>;
type CategoryRecord = typeof categoryTable.$inferSelect;
type SectionRecord = typeof portfolioSection.$inferSelect;
type AssetRecord = typeof portfolioAsset.$inferSelect;
type ProjectRecord = typeof portfolioProject.$inferSelect;
export type LocalizedContent = {
ar: string;
@@ -240,40 +188,29 @@ export function getLocalizedValue(
}
export async function getAdminPortfolioCategories() {
const categories = await prisma.category.findMany({
include: {
_count: {
select: {
projects: true,
},
},
},
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
const categories = await db.query.category.findMany({
orderBy: [asc(categoryTable.sortOrder), asc(categoryTable.createdAt)],
with: { projects: { columns: { id: true } } },
});
return categories.map((category) => ({
...mapCategory(category),
projectCount: category._count.projects,
projectCount: category.projects.length,
}));
}
export async function getActivePortfolioCategories() {
const categories = await prisma.category.findMany({
where: {
isActive: true,
},
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
const categories = await db.query.category.findMany({
where: eq(categoryTable.isActive, true),
orderBy: [asc(categoryTable.sortOrder), asc(categoryTable.createdAt)],
});
return categories.map(mapCategory);
}
export async function getActivePortfolioCategoryBySlug(slug: string) {
const category = await prisma.category.findFirst({
where: {
slug,
isActive: true,
},
const category = await db.query.category.findFirst({
where: and(eq(categoryTable.slug, slug), eq(categoryTable.isActive, true)),
});
return category ? mapCategory(category) : null;
@@ -283,90 +220,78 @@ export async function getAdminPortfolioProjects(filters?: {
categoryId?: string;
status?: "all" | "draft" | "published";
}) {
const projects = await prisma.portfolioProject.findMany({
where: {
...(filters?.categoryId ? { categoryId: filters.categoryId } : {}),
...(filters?.status === "draft"
? { isPublished: false }
: filters?.status === "published"
? { isPublished: true }
: {}),
},
include: {
const conditions = [
...(filters?.categoryId ? [eq(portfolioProject.categoryId, filters.categoryId)] : []),
...(filters?.status === "draft"
? [eq(portfolioProject.isPublished, false)]
: filters?.status === "published"
? [eq(portfolioProject.isPublished, true)]
: []),
];
const projects = await db.query.portfolioProject.findMany({
where: conditions.length ? and(...conditions) : undefined,
with: {
category: true,
sections: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
assets: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] },
assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] },
},
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
orderBy: [asc(portfolioProject.sortOrder), desc(portfolioProject.createdAt)],
});
return projects.map((project) => mapProject(project));
}
export async function getPublishedPortfolioProjects(filters?: { categorySlug?: string }) {
const projects = await prisma.portfolioProject.findMany({
where: {
isPublished: true,
category: {
isActive: true,
...(filters?.categorySlug ? { slug: filters.categorySlug } : {}),
},
},
include: {
const projects = await db.query.portfolioProject.findMany({
where: eq(portfolioProject.isPublished, true),
with: {
category: true,
sections: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
assets: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] },
assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] },
},
orderBy: [{ sortOrder: "asc" }, { publishedAt: "desc" }, { createdAt: "desc" }],
orderBy: [
asc(portfolioProject.sortOrder),
desc(portfolioProject.publishedAt),
desc(portfolioProject.createdAt),
],
});
return projects.map((project) => mapProject(project));
// Prisma filtered on the related category (active + optional slug); the
// relational query filters the main table only, so narrow here.
return projects
.filter(
(project) =>
project.category.isActive &&
(!filters?.categorySlug || project.category.slug === filters.categorySlug),
)
.map((project) => mapProject(project));
}
export const getPublishedPortfolioProjectBySlug = cache(async function (slug: string) {
const project = await prisma.portfolioProject.findFirst({
where: {
slug,
isPublished: true,
category: {
isActive: true,
},
},
include: {
const project = await db.query.portfolioProject.findFirst({
where: and(eq(portfolioProject.slug, slug), eq(portfolioProject.isPublished, true)),
with: {
category: true,
sections: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
assets: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] },
assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] },
},
});
return project ? mapProject(project) : null;
if (!project || !project.category.isActive) {
return null;
}
return mapProject(project);
});
export async function getAdminPortfolioProjectById(id: string) {
const project = await prisma.portfolioProject.findUnique({
where: {
id,
},
include: {
const project = await db.query.portfolioProject.findFirst({
where: eq(portfolioProject.id, id),
with: {
category: true,
sections: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
assets: {
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
},
sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] },
assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] },
},
});