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
+35
View File
@@ -0,0 +1,35 @@
import {
mediaKind,
mediaSource,
mediaUsageType,
portfolioAssetKind,
portfolioProjectViewMode,
portfolioSectionType,
} from "./schema";
/**
* Prisma-compatible enum objects + types, derived from the Drizzle pgEnums, so
* existing consumers can keep writing `MediaKind.IMAGE` (value) and `: MediaKind`
* (type) — only the import path changes from `@prisma/client` to `@/lib/db/enums`.
*/
function asEnum<T extends string>(values: readonly T[]): { [K in T]: K } {
return Object.fromEntries(values.map((v) => [v, v])) as { [K in T]: K };
}
export const MediaKind = asEnum(mediaKind.enumValues);
export type MediaKind = (typeof mediaKind.enumValues)[number];
export const MediaSource = asEnum(mediaSource.enumValues);
export type MediaSource = (typeof mediaSource.enumValues)[number];
export const MediaUsageType = asEnum(mediaUsageType.enumValues);
export type MediaUsageType = (typeof mediaUsageType.enumValues)[number];
export const PortfolioAssetKind = asEnum(portfolioAssetKind.enumValues);
export type PortfolioAssetKind = (typeof portfolioAssetKind.enumValues)[number];
export const PortfolioProjectViewMode = asEnum(portfolioProjectViewMode.enumValues);
export type PortfolioProjectViewMode = (typeof portfolioProjectViewMode.enumValues)[number];
export const PortfolioSectionType = asEnum(portfolioSectionType.enumValues);
export type PortfolioSectionType = (typeof portfolioSectionType.enumValues)[number];
+26
View File
@@ -0,0 +1,26 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
/**
* The Drizzle database client (postgres.js driver), matching the house standard
* used by the other projects. Replaces the old Prisma client (`lib/prisma.ts`).
* A single connection is reused across hot reloads in dev.
*/
const connectionString =
process.env.DATABASE_URL ??
"postgresql://postgres:postgres@localhost:5432/moh_sass?schema=public";
const globalForDb = globalThis as unknown as {
dbClient: ReturnType<typeof postgres> | undefined;
};
const client = globalForDb.dbClient ?? postgres(connectionString);
if (process.env.NODE_ENV !== "production") {
globalForDb.dbClient = client;
}
export const db = drizzle(client, { schema });
export { schema };
+125
View File
@@ -0,0 +1,125 @@
CREATE TYPE "public"."media_kind" AS ENUM('IMAGE', 'DOCUMENT');--> statement-breakpoint
CREATE TYPE "public"."media_source" AS ENUM('UPLOAD', 'EXTERNAL');--> statement-breakpoint
CREATE TYPE "public"."media_usage_type" AS ENUM('PORTFOLIO_COVER', 'PORTFOLIO_SECTION', 'PORTFOLIO_ASSET', 'GENERIC');--> statement-breakpoint
CREATE TYPE "public"."portfolio_asset_kind" AS ENUM('IMAGE', 'DOCUMENT');--> statement-breakpoint
CREATE TYPE "public"."portfolio_project_view_mode" AS ENUM('GRID', 'STORY', 'CASE_STUDY');--> statement-breakpoint
CREATE TYPE "public"."portfolio_section_type" AS ENUM('RICH_TEXT', 'GALLERY', 'STATS', 'DELIVERABLES', 'LINK');--> statement-breakpoint
CREATE TABLE "app_config" (
"id" text PRIMARY KEY NOT NULL,
"key" text NOT NULL,
"value" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "app_config_key_unique" UNIQUE("key")
);
--> statement-breakpoint
CREATE TABLE "category" (
"id" text PRIMARY KEY NOT NULL,
"slug" text NOT NULL,
"name_ar" text NOT NULL,
"name_en" text NOT NULL,
"name_de" text NOT NULL,
"description_ar" text NOT NULL,
"description_en" text NOT NULL,
"description_de" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "category_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "media_asset" (
"id" text PRIMARY KEY NOT NULL,
"source" "media_source" NOT NULL,
"kind" "media_kind" NOT NULL,
"url" text NOT NULL,
"file_name" text NOT NULL,
"label" text NOT NULL,
"alt_text" text,
"mime_type" text,
"size" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "media_usage" (
"id" text PRIMARY KEY NOT NULL,
"asset_id" text NOT NULL,
"usage_type" "media_usage_type" NOT NULL,
"entity_type" text NOT NULL,
"entity_id" text NOT NULL,
"field_key" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "portfolio_asset" (
"id" text PRIMARY KEY NOT NULL,
"project_id" text NOT NULL,
"kind" "portfolio_asset_kind" NOT NULL,
"file_path" text NOT NULL,
"alt_ar" text NOT NULL,
"alt_en" text NOT NULL,
"alt_de" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "portfolio_project" (
"id" text PRIMARY KEY NOT NULL,
"category_id" text NOT NULL,
"slug" text NOT NULL,
"view_mode" "portfolio_project_view_mode" DEFAULT 'GRID' NOT NULL,
"title_ar" text NOT NULL,
"title_en" text NOT NULL,
"title_de" text NOT NULL,
"summary_ar" text NOT NULL,
"summary_en" text NOT NULL,
"summary_de" text NOT NULL,
"client_name" text NOT NULL,
"project_year" integer NOT NULL,
"service_label_ar" text NOT NULL,
"service_label_en" text NOT NULL,
"service_label_de" text NOT NULL,
"preview_url" text,
"cover_image_path" text,
"is_featured" boolean DEFAULT false NOT NULL,
"is_published" boolean DEFAULT false NOT NULL,
"published_at" timestamp with time zone,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "portfolio_project_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "portfolio_section" (
"id" text PRIMARY KEY NOT NULL,
"project_id" text NOT NULL,
"type" "portfolio_section_type" NOT NULL,
"title_ar" text NOT NULL,
"title_en" text NOT NULL,
"title_de" text NOT NULL,
"body_ar" text NOT NULL,
"body_en" text NOT NULL,
"body_de" text NOT NULL,
"image_path" text,
"link_url" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "media_usage" ADD CONSTRAINT "media_usage_asset_id_media_asset_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."media_asset"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "portfolio_asset" ADD CONSTRAINT "portfolio_asset_project_id_portfolio_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."portfolio_project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "portfolio_project" ADD CONSTRAINT "portfolio_project_category_id_category_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."category"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "portfolio_section" ADD CONSTRAINT "portfolio_section_project_id_portfolio_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."portfolio_project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "media_asset_kind_created_idx" ON "media_asset" USING btree ("kind","created_at");--> statement-breakpoint
CREATE UNIQUE INDEX "media_usage_unique_slot" ON "media_usage" USING btree ("usage_type","entity_type","entity_id","field_key");--> statement-breakpoint
CREATE INDEX "media_usage_asset_idx" ON "media_usage" USING btree ("asset_id");--> statement-breakpoint
CREATE INDEX "media_usage_entity_idx" ON "media_usage" USING btree ("entity_type","entity_id");--> statement-breakpoint
CREATE INDEX "portfolio_asset_project_sort_idx" ON "portfolio_asset" USING btree ("project_id","sort_order");--> statement-breakpoint
CREATE INDEX "portfolio_project_category_published_sort_idx" ON "portfolio_project" USING btree ("category_id","is_published","sort_order");--> statement-breakpoint
CREATE INDEX "portfolio_project_published_sort_idx" ON "portfolio_project" USING btree ("is_published","sort_order");--> statement-breakpoint
CREATE INDEX "portfolio_section_project_sort_idx" ON "portfolio_section" USING btree ("project_id","sort_order");
+956
View File
@@ -0,0 +1,956 @@
{
"id": "a001b9c1-c931-4e9f-b21c-50cdfbffb6a6",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.app_config": {
"name": "app_config",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"app_config_key_unique": {
"name": "app_config_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.category": {
"name": "category",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name_ar": {
"name": "name_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name_en": {
"name": "name_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name_de": {
"name": "name_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description_ar": {
"name": "description_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description_en": {
"name": "description_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description_de": {
"name": "description_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"category_slug_unique": {
"name": "category_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.media_asset": {
"name": "media_asset",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"source": {
"name": "source",
"type": "media_source",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "media_kind",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"file_name": {
"name": "file_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"label": {
"name": "label",
"type": "text",
"primaryKey": false,
"notNull": true
},
"alt_text": {
"name": "alt_text",
"type": "text",
"primaryKey": false,
"notNull": false
},
"mime_type": {
"name": "mime_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"media_asset_kind_created_idx": {
"name": "media_asset_kind_created_idx",
"columns": [
{
"expression": "kind",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.media_usage": {
"name": "media_usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"asset_id": {
"name": "asset_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"usage_type": {
"name": "usage_type",
"type": "media_usage_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"entity_type": {
"name": "entity_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"entity_id": {
"name": "entity_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"field_key": {
"name": "field_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"media_usage_unique_slot": {
"name": "media_usage_unique_slot",
"columns": [
{
"expression": "usage_type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "entity_type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "entity_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "field_key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"media_usage_asset_idx": {
"name": "media_usage_asset_idx",
"columns": [
{
"expression": "asset_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"media_usage_entity_idx": {
"name": "media_usage_entity_idx",
"columns": [
{
"expression": "entity_type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "entity_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"media_usage_asset_id_media_asset_id_fk": {
"name": "media_usage_asset_id_media_asset_id_fk",
"tableFrom": "media_usage",
"tableTo": "media_asset",
"columnsFrom": [
"asset_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.portfolio_asset": {
"name": "portfolio_asset",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "portfolio_asset_kind",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"file_path": {
"name": "file_path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"alt_ar": {
"name": "alt_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"alt_en": {
"name": "alt_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"alt_de": {
"name": "alt_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"portfolio_asset_project_sort_idx": {
"name": "portfolio_asset_project_sort_idx",
"columns": [
{
"expression": "project_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "sort_order",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"portfolio_asset_project_id_portfolio_project_id_fk": {
"name": "portfolio_asset_project_id_portfolio_project_id_fk",
"tableFrom": "portfolio_asset",
"tableTo": "portfolio_project",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.portfolio_project": {
"name": "portfolio_project",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"category_id": {
"name": "category_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"view_mode": {
"name": "view_mode",
"type": "portfolio_project_view_mode",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'GRID'"
},
"title_ar": {
"name": "title_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"title_en": {
"name": "title_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"title_de": {
"name": "title_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary_ar": {
"name": "summary_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary_en": {
"name": "summary_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary_de": {
"name": "summary_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"client_name": {
"name": "client_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"project_year": {
"name": "project_year",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"service_label_ar": {
"name": "service_label_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"service_label_en": {
"name": "service_label_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"service_label_de": {
"name": "service_label_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"preview_url": {
"name": "preview_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"cover_image_path": {
"name": "cover_image_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"is_featured": {
"name": "is_featured",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"is_published": {
"name": "is_published",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"published_at": {
"name": "published_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"portfolio_project_category_published_sort_idx": {
"name": "portfolio_project_category_published_sort_idx",
"columns": [
{
"expression": "category_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "is_published",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "sort_order",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"portfolio_project_published_sort_idx": {
"name": "portfolio_project_published_sort_idx",
"columns": [
{
"expression": "is_published",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "sort_order",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"portfolio_project_category_id_category_id_fk": {
"name": "portfolio_project_category_id_category_id_fk",
"tableFrom": "portfolio_project",
"tableTo": "category",
"columnsFrom": [
"category_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"portfolio_project_slug_unique": {
"name": "portfolio_project_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.portfolio_section": {
"name": "portfolio_section",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "portfolio_section_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"title_ar": {
"name": "title_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"title_en": {
"name": "title_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"title_de": {
"name": "title_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body_ar": {
"name": "body_ar",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body_en": {
"name": "body_en",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body_de": {
"name": "body_de",
"type": "text",
"primaryKey": false,
"notNull": true
},
"image_path": {
"name": "image_path",
"type": "text",
"primaryKey": false,
"notNull": false
},
"link_url": {
"name": "link_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"portfolio_section_project_sort_idx": {
"name": "portfolio_section_project_sort_idx",
"columns": [
{
"expression": "project_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "sort_order",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"portfolio_section_project_id_portfolio_project_id_fk": {
"name": "portfolio_section_project_id_portfolio_project_id_fk",
"tableFrom": "portfolio_section",
"tableTo": "portfolio_project",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.media_kind": {
"name": "media_kind",
"schema": "public",
"values": [
"IMAGE",
"DOCUMENT"
]
},
"public.media_source": {
"name": "media_source",
"schema": "public",
"values": [
"UPLOAD",
"EXTERNAL"
]
},
"public.media_usage_type": {
"name": "media_usage_type",
"schema": "public",
"values": [
"PORTFOLIO_COVER",
"PORTFOLIO_SECTION",
"PORTFOLIO_ASSET",
"GENERIC"
]
},
"public.portfolio_asset_kind": {
"name": "portfolio_asset_kind",
"schema": "public",
"values": [
"IMAGE",
"DOCUMENT"
]
},
"public.portfolio_project_view_mode": {
"name": "portfolio_project_view_mode",
"schema": "public",
"values": [
"GRID",
"STORY",
"CASE_STUDY"
]
},
"public.portfolio_section_type": {
"name": "portfolio_section_type",
"schema": "public",
"values": [
"RICH_TEXT",
"GALLERY",
"STATS",
"DELIVERABLES",
"LINK"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1786049545718,
"tag": "0000_fixed_venom",
"breakpoints": true
}
]
}
+241
View File
@@ -0,0 +1,241 @@
import { relations } from "drizzle-orm";
import {
boolean,
index,
integer,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";
/**
* Drizzle schema — the single source of truth for the database, replacing the
* old Prisma schema (see docs). The database is Postgres; migrations are
* generated with `drizzle-kit generate`. IDs are app-generated opaque strings
* (was Prisma `cuid()`), timestamps default in the DB and bump on update.
*/
// `crypto.randomUUID()` is a global in Node 20+ and browsers (no node: import),
// so the schema stays safe to pull into a client bundle via lib/db/enums.
const id = () =>
text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID());
const createdAt = timestamp("created_at", { withTimezone: true }).notNull().defaultNow();
const updatedAt = timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date());
// --- Enums ------------------------------------------------------------------
export const portfolioSectionType = pgEnum("portfolio_section_type", [
"RICH_TEXT",
"GALLERY",
"STATS",
"DELIVERABLES",
"LINK",
]);
export const portfolioAssetKind = pgEnum("portfolio_asset_kind", ["IMAGE", "DOCUMENT"]);
export const portfolioProjectViewMode = pgEnum("portfolio_project_view_mode", [
"GRID",
"STORY",
"CASE_STUDY",
]);
export const mediaSource = pgEnum("media_source", ["UPLOAD", "EXTERNAL"]);
export const mediaKind = pgEnum("media_kind", ["IMAGE", "DOCUMENT"]);
export const mediaUsageType = pgEnum("media_usage_type", [
"PORTFOLIO_COVER",
"PORTFOLIO_SECTION",
"PORTFOLIO_ASSET",
"GENERIC",
]);
// --- Tables -----------------------------------------------------------------
export const appConfig = pgTable("app_config", {
id: id(),
key: text("key").notNull().unique(),
value: text("value").notNull(),
createdAt,
updatedAt,
});
export const category = pgTable("category", {
id: id(),
slug: text("slug").notNull().unique(),
nameAr: text("name_ar").notNull(),
nameEn: text("name_en").notNull(),
nameDe: text("name_de").notNull(),
descriptionAr: text("description_ar").notNull(),
descriptionEn: text("description_en").notNull(),
descriptionDe: text("description_de").notNull(),
sortOrder: integer("sort_order").notNull().default(0),
isActive: boolean("is_active").notNull().default(true),
createdAt,
updatedAt,
});
export const portfolioProject = pgTable(
"portfolio_project",
{
id: id(),
categoryId: text("category_id")
.notNull()
.references(() => category.id, { onDelete: "restrict" }),
slug: text("slug").notNull().unique(),
viewMode: portfolioProjectViewMode("view_mode").notNull().default("GRID"),
titleAr: text("title_ar").notNull(),
titleEn: text("title_en").notNull(),
titleDe: text("title_de").notNull(),
summaryAr: text("summary_ar").notNull(),
summaryEn: text("summary_en").notNull(),
summaryDe: text("summary_de").notNull(),
clientName: text("client_name").notNull(),
projectYear: integer("project_year").notNull(),
serviceLabelAr: text("service_label_ar").notNull(),
serviceLabelEn: text("service_label_en").notNull(),
serviceLabelDe: text("service_label_de").notNull(),
previewUrl: text("preview_url"),
coverImagePath: text("cover_image_path"),
isFeatured: boolean("is_featured").notNull().default(false),
isPublished: boolean("is_published").notNull().default(false),
publishedAt: timestamp("published_at", { withTimezone: true }),
sortOrder: integer("sort_order").notNull().default(0),
createdAt,
updatedAt,
},
(t) => [
index("portfolio_project_category_published_sort_idx").on(t.categoryId, t.isPublished, t.sortOrder),
index("portfolio_project_published_sort_idx").on(t.isPublished, t.sortOrder),
],
);
export const portfolioSection = pgTable(
"portfolio_section",
{
id: id(),
projectId: text("project_id")
.notNull()
.references(() => portfolioProject.id, { onDelete: "cascade" }),
type: portfolioSectionType("type").notNull(),
titleAr: text("title_ar").notNull(),
titleEn: text("title_en").notNull(),
titleDe: text("title_de").notNull(),
bodyAr: text("body_ar").notNull(),
bodyEn: text("body_en").notNull(),
bodyDe: text("body_de").notNull(),
imagePath: text("image_path"),
linkUrl: text("link_url"),
sortOrder: integer("sort_order").notNull().default(0),
createdAt,
updatedAt,
},
(t) => [index("portfolio_section_project_sort_idx").on(t.projectId, t.sortOrder)],
);
export const portfolioAsset = pgTable(
"portfolio_asset",
{
id: id(),
projectId: text("project_id")
.notNull()
.references(() => portfolioProject.id, { onDelete: "cascade" }),
kind: portfolioAssetKind("kind").notNull(),
filePath: text("file_path").notNull(),
altAr: text("alt_ar").notNull(),
altEn: text("alt_en").notNull(),
altDe: text("alt_de").notNull(),
sortOrder: integer("sort_order").notNull().default(0),
createdAt,
updatedAt,
},
(t) => [index("portfolio_asset_project_sort_idx").on(t.projectId, t.sortOrder)],
);
export const mediaAsset = pgTable(
"media_asset",
{
id: id(),
source: mediaSource("source").notNull(),
kind: mediaKind("kind").notNull(),
url: text("url").notNull(),
fileName: text("file_name").notNull(),
label: text("label").notNull(),
altText: text("alt_text"),
mimeType: text("mime_type"),
size: integer("size"),
createdAt,
updatedAt,
},
(t) => [index("media_asset_kind_created_idx").on(t.kind, t.createdAt)],
);
export const mediaUsage = pgTable(
"media_usage",
{
id: id(),
assetId: text("asset_id")
.notNull()
.references(() => mediaAsset.id, { onDelete: "cascade" }),
usageType: mediaUsageType("usage_type").notNull(),
entityType: text("entity_type").notNull(),
entityId: text("entity_id").notNull(),
fieldKey: text("field_key").notNull(),
createdAt,
updatedAt,
},
(t) => [
uniqueIndex("media_usage_unique_slot").on(t.usageType, t.entityType, t.entityId, t.fieldKey),
index("media_usage_asset_idx").on(t.assetId),
index("media_usage_entity_idx").on(t.entityType, t.entityId),
],
);
// --- Relations (for the relational query API: db.query.*.findMany({ with })) --
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],
}),
}));