REFACTORED - Replace Prisma with Drizzle ORM
CI / quality (push) Waiting to run

- Add lib/db (schema, postgres.js client, enums, seed, migrations) on Drizzle
- Rewrite all lib and admin action queries from Prisma to Drizzle
- Keep existing table/column names so no data migration is needed
- Preserve signed-cookie admin auth unchanged
- Map unique-violation handling from Prisma P2002 to SQLSTATE 23505
- Swap deps, scripts, Makefile, and Dockerfile from Prisma to Drizzle
This commit is contained in:
Moh
2026-08-08 02:09:03 +02:00
parent 0f48381894
commit ba63f75ea8
38 changed files with 3964 additions and 2520 deletions
+248
View File
@@ -0,0 +1,248 @@
import { createId } from "@paralleldrive/cuid2";
import { relations } from "drizzle-orm";
import {
boolean,
index,
integer,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";
import {
MediaKind,
MediaSource,
MediaUsageType,
PortfolioAssetKind,
PortfolioProjectViewMode,
PortfolioSectionType,
enumValues,
} from "./enums";
// Postgres enum types — names match the ones Prisma created, so no DB migration
// is needed for the ORM swap.
export const portfolioSectionTypeEnum = pgEnum("PortfolioSectionType", enumValues(PortfolioSectionType));
export const portfolioAssetKindEnum = pgEnum("PortfolioAssetKind", enumValues(PortfolioAssetKind));
export const portfolioProjectViewModeEnum = pgEnum("PortfolioProjectViewMode", enumValues(PortfolioProjectViewMode));
export const mediaSourceEnum = pgEnum("MediaSource", enumValues(MediaSource));
export const mediaKindEnum = pgEnum("MediaKind", enumValues(MediaKind));
export const mediaUsageTypeEnum = pgEnum("MediaUsageType", enumValues(MediaUsageType));
// Shared column builders (Prisma parity): cuid ids, precision-3 timestamps.
const id = () => text("id").primaryKey().$defaultFn(() => createId());
const createdAt = () => timestamp("createdAt", { precision: 3, mode: "date" }).defaultNow().notNull();
const updatedAt = () =>
timestamp("updatedAt", { precision: 3, mode: "date" })
.defaultNow()
.notNull()
.$onUpdate(() => new Date());
export const appConfig = pgTable("AppConfig", {
id: id(),
key: text("key").notNull().unique(),
value: text("value").notNull(),
createdAt: createdAt(),
updatedAt: updatedAt(),
});
export const category = pgTable(
"Category",
{
id: id(),
slug: text("slug").notNull().unique(),
nameAr: text("nameAr").notNull(),
nameEn: text("nameEn").notNull(),
nameDe: text("nameDe").notNull(),
descriptionAr: text("descriptionAr").notNull(),
descriptionEn: text("descriptionEn").notNull(),
descriptionDe: text("descriptionDe").notNull(),
sortOrder: integer("sortOrder").notNull().default(0),
isActive: boolean("isActive").notNull().default(true),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
);
export const portfolioProject = pgTable(
"PortfolioProject",
{
id: id(),
categoryId: text("categoryId")
.notNull()
.references(() => category.id, { onDelete: "restrict" }),
slug: text("slug").notNull().unique(),
viewMode: portfolioProjectViewModeEnum("viewMode").notNull().default("GRID"),
titleAr: text("titleAr").notNull(),
titleEn: text("titleEn").notNull(),
titleDe: text("titleDe").notNull(),
summaryAr: text("summaryAr").notNull(),
summaryEn: text("summaryEn").notNull(),
summaryDe: text("summaryDe").notNull(),
clientName: text("clientName").notNull(),
projectYear: integer("projectYear").notNull(),
serviceLabelAr: text("serviceLabelAr").notNull(),
serviceLabelEn: text("serviceLabelEn").notNull(),
serviceLabelDe: text("serviceLabelDe").notNull(),
previewUrl: text("previewUrl"),
coverImagePath: text("coverImagePath"),
isFeatured: boolean("isFeatured").notNull().default(false),
isPublished: boolean("isPublished").notNull().default(false),
publishedAt: timestamp("publishedAt", { precision: 3, mode: "date" }),
sortOrder: integer("sortOrder").notNull().default(0),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(table) => [
index("PortfolioProject_categoryId_isPublished_sortOrder_idx").on(
table.categoryId,
table.isPublished,
table.sortOrder,
),
index("PortfolioProject_isPublished_sortOrder_idx").on(table.isPublished, table.sortOrder),
],
);
export const portfolioSection = pgTable(
"PortfolioSection",
{
id: id(),
projectId: text("projectId")
.notNull()
.references(() => portfolioProject.id, { onDelete: "cascade" }),
type: portfolioSectionTypeEnum("type").notNull(),
titleAr: text("titleAr").notNull(),
titleEn: text("titleEn").notNull(),
titleDe: text("titleDe").notNull(),
bodyAr: text("bodyAr").notNull(),
bodyEn: text("bodyEn").notNull(),
bodyDe: text("bodyDe").notNull(),
imagePath: text("imagePath"),
linkUrl: text("linkUrl"),
sortOrder: integer("sortOrder").notNull().default(0),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(table) => [index("PortfolioSection_projectId_sortOrder_idx").on(table.projectId, table.sortOrder)],
);
export const portfolioAsset = pgTable(
"PortfolioAsset",
{
id: id(),
projectId: text("projectId")
.notNull()
.references(() => portfolioProject.id, { onDelete: "cascade" }),
kind: portfolioAssetKindEnum("kind").notNull(),
filePath: text("filePath").notNull(),
altAr: text("altAr").notNull(),
altEn: text("altEn").notNull(),
altDe: text("altDe").notNull(),
sortOrder: integer("sortOrder").notNull().default(0),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(table) => [index("PortfolioAsset_projectId_sortOrder_idx").on(table.projectId, table.sortOrder)],
);
export const mediaAsset = pgTable(
"MediaAsset",
{
id: id(),
source: mediaSourceEnum("source").notNull(),
kind: mediaKindEnum("kind").notNull(),
url: text("url").notNull(),
fileName: text("fileName").notNull(),
label: text("label").notNull(),
altText: text("altText"),
mimeType: text("mimeType"),
size: integer("size"),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(table) => [index("MediaAsset_kind_createdAt_idx").on(table.kind, table.createdAt)],
);
export const mediaUsage = pgTable(
"MediaUsage",
{
id: id(),
assetId: text("assetId")
.notNull()
.references(() => mediaAsset.id, { onDelete: "cascade" }),
usageType: mediaUsageTypeEnum("usageType").notNull(),
entityType: text("entityType").notNull(),
entityId: text("entityId").notNull(),
fieldKey: text("fieldKey").notNull(),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(table) => [
uniqueIndex("MediaUsage_usageType_entityType_entityId_fieldKey_key").on(
table.usageType,
table.entityType,
table.entityId,
table.fieldKey,
),
index("MediaUsage_assetId_idx").on(table.assetId),
index("MediaUsage_entityType_entityId_idx").on(table.entityType, table.entityId),
],
);
// Relations (enable db.query.* `with:` includes).
export const categoryRelations = relations(category, ({ many }) => ({
projects: many(portfolioProject),
}));
export const portfolioProjectRelations = relations(portfolioProject, ({ one, many }) => ({
category: one(category, {
fields: [portfolioProject.categoryId],
references: [category.id],
}),
sections: many(portfolioSection),
assets: many(portfolioAsset),
}));
export const portfolioSectionRelations = relations(portfolioSection, ({ one }) => ({
project: one(portfolioProject, {
fields: [portfolioSection.projectId],
references: [portfolioProject.id],
}),
}));
export const portfolioAssetRelations = relations(portfolioAsset, ({ one }) => ({
project: one(portfolioProject, {
fields: [portfolioAsset.projectId],
references: [portfolioProject.id],
}),
}));
export const mediaAssetRelations = relations(mediaAsset, ({ many }) => ({
usages: many(mediaUsage),
}));
export const mediaUsageRelations = relations(mediaUsage, ({ one }) => ({
asset: one(mediaAsset, {
fields: [mediaUsage.assetId],
references: [mediaAsset.id],
}),
}));
// Inferred row types (replace the old `@prisma/client` model type imports).
export type AppConfig = typeof appConfig.$inferSelect;
export type Category = typeof category.$inferSelect;
export type PortfolioProject = typeof portfolioProject.$inferSelect;
export type PortfolioSection = typeof portfolioSection.$inferSelect;
export type PortfolioAsset = typeof portfolioAsset.$inferSelect;
export type MediaAsset = typeof mediaAsset.$inferSelect;
export type MediaUsage = typeof mediaUsage.$inferSelect;
export {
MediaKind,
MediaSource,
MediaUsageType,
PortfolioAssetKind,
PortfolioProjectViewMode,
PortfolioSectionType,
} from "./enums";