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
249 lines
8.0 KiB
TypeScript
249 lines
8.0 KiB
TypeScript
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";
|