- 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:
@@ -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";
|
||||
Reference in New Issue
Block a user