import { and, eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; import { appConfig, category, mediaAsset, mediaUsage, portfolioAsset, portfolioProject, portfolioSection, } from "./schema"; const connectionString = ( process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5432/moh_sass" ).split("?")[0]; const client = postgres(connectionString, { max: 1 }); const db = drizzle(client, { schema }); type MediaAssetInput = { source: "UPLOAD" | "EXTERNAL"; kind: "IMAGE" | "DOCUMENT"; url: string; fileName: string; label: string; altText: string | null; mimeType: string | null; size: number | null; }; async function upsertMediaAsset(input: MediaAssetInput) { const [existing] = await db .select() .from(mediaAsset) .where(and(eq(mediaAsset.label, input.label), eq(mediaAsset.url, input.url))) .limit(1); if (existing) { const [updated] = await db .update(mediaAsset) .set({ ...input, updatedAt: new Date() }) .where(eq(mediaAsset.id, existing.id)) .returning(); return updated; } const [created] = await db.insert(mediaAsset).values(input).returning(); return created; } async function upsertAppConfig(key: string, value: string) { await db .insert(appConfig) .values({ key, value }) .onConflictDoUpdate({ target: appConfig.key, set: { value, updatedAt: new Date() } }); } async function upsertCategory(values: typeof category.$inferInsert) { const [row] = await db .insert(category) .values(values) .onConflictDoUpdate({ target: category.slug, set: { nameAr: values.nameAr, nameEn: values.nameEn, nameDe: values.nameDe, descriptionAr: values.descriptionAr, descriptionEn: values.descriptionEn, descriptionDe: values.descriptionDe, sortOrder: values.sortOrder, isActive: values.isActive, updatedAt: new Date(), }, }) .returning(); return row; } async function syncProjectContent( projectId: string, sections: Array>, assets: Array>, ) { await db.delete(portfolioSection).where(eq(portfolioSection.projectId, projectId)); await db.delete(portfolioAsset).where(eq(portfolioAsset.projectId, projectId)); const createdSections = []; for (const section of sections) { const [row] = await db .insert(portfolioSection) .values({ projectId, ...section }) .returning(); createdSections.push(row); } const createdAssets = []; for (const asset of assets) { const [row] = await db .insert(portfolioAsset) .values({ projectId, ...asset }) .returning(); createdAssets.push(row); } return { createdSections, createdAssets }; } async function syncProjectMediaUsages( projectId: string, mediaMap: { coverAssetId: string | null | undefined; sectionUsages: Array<{ fieldKey: string; assetId: string | null | undefined }>; assetUsages: Array<{ fieldKey: string; assetId: string | null | undefined }>; }, ) { await db .delete(mediaUsage) .where(and(eq(mediaUsage.entityType, "portfolio-project"), eq(mediaUsage.entityId, projectId))); const usages: (typeof mediaUsage.$inferInsert)[] = []; if (mediaMap.coverAssetId) { usages.push({ assetId: mediaMap.coverAssetId, usageType: "PORTFOLIO_COVER", entityType: "portfolio-project", entityId: projectId, fieldKey: "cover", }); } for (const sectionUsage of mediaMap.sectionUsages) { if (!sectionUsage.assetId) continue; usages.push({ assetId: sectionUsage.assetId, usageType: "PORTFOLIO_SECTION", entityType: "portfolio-project", entityId: projectId, fieldKey: sectionUsage.fieldKey, }); } for (const assetUsage of mediaMap.assetUsages) { if (!assetUsage.assetId) continue; usages.push({ assetId: assetUsage.assetId, usageType: "PORTFOLIO_ASSET", entityType: "portfolio-project", entityId: projectId, fieldKey: assetUsage.fieldKey, }); } if (usages.length > 0) { await db.insert(mediaUsage).values(usages); } } const SITE_SETTINGS_VALUE = JSON.stringify({ titleTemplate: "{pageTitle} | moh-sass", locales: { ar: { siteName: "moh-sass", titleTemplate: "{pageTitle} | {siteName}", siteDescription: "Multilingual Next.js base project", subhead: "", }, en: { siteName: "moh-sass", titleTemplate: "{pageTitle} | {siteName}", siteDescription: "Multilingual Next.js base project", subhead: "", }, de: { siteName: "moh-sass", titleTemplate: "{pageTitle} | {siteName}", siteDescription: "Multilingual Next.js base project", subhead: "", }, }, }); async function main() { await db .delete(mediaUsage) .where(eq(mediaUsage.entityType, "portfolio-project")); await db.delete(portfolioSection); await db.delete(portfolioAsset); await db.delete(portfolioProject); await db.delete(category); await upsertAppConfig("siteName", "moh-sass"); await upsertAppConfig("site_settings", SITE_SETTINGS_VALUE); const brandCategory = await upsertCategory({ slug: "branding", nameAr: "الهوية البصرية", nameEn: "Branding", nameDe: "Branding", descriptionAr: "مشاريع هوية بصرية وشعارات وأنظمة علامة.", descriptionEn: "Brand identity, logo, and design system work.", descriptionDe: "Branding, Logos und visuelle Systeme.", sortOrder: 1, isActive: true, }); const webCategory = await upsertCategory({ slug: "web-experiences", nameAr: "تجارب الويب", nameEn: "Web Experiences", nameDe: "Web Experiences", descriptionAr: "مواقع وصفحات هبوط وتجارب رقمية سريعة.", descriptionEn: "Websites, landing pages, and digital experiences.", descriptionDe: "Webseiten, Landingpages und digitale Erlebnisse.", sortOrder: 2, isActive: true, }); const commerceCategory = await upsertCategory({ slug: "commerce", nameAr: "التجارة الرقمية", nameEn: "Commerce", nameDe: "Commerce", descriptionAr: "متاجر وتجارب شراء رقمية مع تركيز على الوضوح والتحويل.", descriptionEn: "Commerce experiences with a focus on clarity and conversion.", descriptionDe: "Commerce-Projekte mit Fokus auf Klarheit und Conversion.", sortOrder: 3, isActive: true, }); const gridCover = await upsertMediaAsset({ source: "UPLOAD", kind: "IMAGE", url: "/uploads/portfolio/demo-cover.svg", fileName: "demo-cover.svg", label: "Portfolio Grid Cover", altText: "Portfolio Grid Cover", mimeType: "image/svg+xml", size: 1024, }); const storyCover = await upsertMediaAsset({ source: "UPLOAD", kind: "IMAGE", url: "/uploads/portfolio/demo-cover.svg", fileName: "demo-cover.svg", label: "Portfolio Story Cover", altText: "Portfolio Story Cover", mimeType: "image/svg+xml", size: 1024, }); const caseStudyCover = await upsertMediaAsset({ source: "UPLOAD", kind: "IMAGE", url: "/uploads/portfolio/demo-cover.svg", fileName: "demo-cover.svg", label: "Portfolio Case Study Cover", altText: "Portfolio Case Study Cover", mimeType: "image/svg+xml", size: 1024, }); const projects = [ { slug: "grid-product-launch", categoryId: commerceCategory.id, viewMode: "GRID" as const, titleAr: "إطلاق منتج رقمي", titleEn: "Grid Product Launch", titleDe: "Grid Product Launch", summaryAr: "مثال عرض شبكي لمشروع سريع مع أقسام قصيرة وأصول داعمة.", summaryEn: "Grid view example for a fast product launch page.", summaryDe: "Grid-Ansicht als Beispiel fuer einen schnellen Produktlaunch.", clientName: "Launch Studio", projectYear: 2026, serviceLabelAr: "تجربة إطلاق", serviceLabelEn: "Launch Experience", serviceLabelDe: "Launch Experience", previewUrl: "https://example.com/preview/grid-product-launch", coverImagePath: gridCover.url, isFeatured: true, isPublished: true, publishedAt: new Date("2026-01-12T09:00:00.000Z"), sortOrder: 1, coverAssetId: gridCover.id, sections: [ { type: "RICH_TEXT" as const, titleAr: "الفكرة", titleEn: "Concept", titleDe: "Konzept", bodyAr: "واجهة سريعة لعرض المنتج والتركيز على الرسالة الأساسية.", bodyEn: "A fast modular presentation focused on the main launch message.", bodyDe: "Eine schnelle modulare Darstellung mit Fokus auf die Hauptbotschaft.", imagePath: null, linkUrl: null, sortOrder: 0, mediaAssetId: null, }, { type: "GALLERY" as const, titleAr: "الصورة الرئيسية", titleEn: "Hero Visual", titleDe: "Hero Visual", bodyAr: "", bodyEn: "", bodyDe: "", imagePath: gridCover.url, linkUrl: null, sortOrder: 1, mediaAssetId: gridCover.id, }, ], assets: [ { kind: "IMAGE" as const, filePath: gridCover.url, altAr: "غلاف مشروع Grid", altEn: "Grid project cover", altDe: "Grid Projekt Cover", sortOrder: 0, mediaAssetId: gridCover.id, }, ], }, { slug: "campaign-site", categoryId: webCategory.id, viewMode: "STORY" as const, titleAr: "موقع حملة", titleEn: "Campaign Site", titleDe: "Campaign Site", summaryAr: "مثال عرض قصصي لمشروع ويب مع تسلسل سردي أوضح.", summaryEn: "Story view example for a launch campaign website.", summaryDe: "Story-Ansicht als Beispiel fuer eine Kampagnenseite.", clientName: "Launch Client", projectYear: 2024, serviceLabelAr: "موقع تسويقي", serviceLabelEn: "Marketing Website", serviceLabelDe: "Marketing Website", previewUrl: "https://example.com/preview/campaign-site", coverImagePath: storyCover.url, isFeatured: false, isPublished: true, publishedAt: new Date("2024-09-05T09:00:00.000Z"), sortOrder: 2, coverAssetId: storyCover.id, sections: [ { type: "RICH_TEXT" as const, titleAr: "السياق", titleEn: "Context", titleDe: "Kontext", bodyAr: "الحملة احتاجت صفحة مرنة وسريعة تتبدل بين أكثر من مرحلة.", bodyEn: "The campaign needed a flexible page that could adapt across phases.", bodyDe: "Die Kampagne brauchte eine flexible Seite fuer mehrere Phasen.", imagePath: null, linkUrl: null, sortOrder: 0, mediaAssetId: null, }, { type: "GALLERY" as const, titleAr: "العرض البصري", titleEn: "Visual Flow", titleDe: "Visueller Ablauf", bodyAr: "", bodyEn: "", bodyDe: "", imagePath: storyCover.url, linkUrl: null, sortOrder: 1, mediaAssetId: storyCover.id, }, { type: "LINK" as const, titleAr: "المعاينة", titleEn: "Preview", titleDe: "Vorschau", bodyAr: "رابط العرض المباشر.", bodyEn: "Direct preview link.", bodyDe: "Direkter Vorschau-Link.", imagePath: null, linkUrl: "https://example.com/preview/campaign-site", sortOrder: 2, mediaAssetId: null, }, ], assets: [ { kind: "IMAGE" as const, filePath: storyCover.url, altAr: "غلاف مشروع Story", altEn: "Story project cover", altDe: "Story Projekt Cover", sortOrder: 0, mediaAssetId: storyCover.id, }, ], }, { slug: "brand-redesign", categoryId: brandCategory.id, viewMode: "CASE_STUDY" as const, titleAr: "إعادة تصميم الهوية", titleEn: "Brand Redesign", titleDe: "Brand Redesign", summaryAr: "مثال عرض دراسة حالة يركز على التحدي والحل والنتيجة.", summaryEn: "Case study example focused on challenge, solution, and outcome.", summaryDe: "Case-Study-Ansicht mit Fokus auf Herausforderung, Loesung und Ergebnis.", clientName: "Studio Client", projectYear: 2025, serviceLabelAr: "هوية بصرية", serviceLabelEn: "Brand Identity", serviceLabelDe: "Brand Identity", previewUrl: "https://example.com/preview/brand-redesign", coverImagePath: caseStudyCover.url, isFeatured: true, isPublished: true, publishedAt: new Date("2025-01-10T09:00:00.000Z"), sortOrder: 3, coverAssetId: caseStudyCover.id, sections: [ { type: "RICH_TEXT" as const, titleAr: "التحدي", titleEn: "Challenge", titleDe: "Herausforderung", bodyAr: "كان المطلوب تحديث الهوية بدون خسارة التعرف البصري الحالي.", bodyEn: "The brief required a refreshed identity without losing recognition.", bodyDe: "Die Marke sollte modernisiert werden, ohne die Wiedererkennbarkeit zu verlieren.", imagePath: null, linkUrl: null, sortOrder: 0, mediaAssetId: null, }, { type: "RICH_TEXT" as const, titleAr: "الحل", titleEn: "Solution", titleDe: "Loesung", bodyAr: "تم بناء نظام مرئي أوضح مع قواعد استخدام قابلة للتوسع.", bodyEn: "A clearer visual system with scalable usage rules was created.", bodyDe: "Es wurde ein klareres visuelles System mit skalierbaren Regeln aufgebaut.", imagePath: null, linkUrl: null, sortOrder: 1, mediaAssetId: null, }, { type: "GALLERY" as const, titleAr: "التنفيذ البصري", titleEn: "Visual Execution", titleDe: "Visuelle Umsetzung", bodyAr: "", bodyEn: "", bodyDe: "", imagePath: caseStudyCover.url, linkUrl: null, sortOrder: 2, mediaAssetId: caseStudyCover.id, }, ], assets: [ { kind: "IMAGE" as const, filePath: caseStudyCover.url, altAr: "غلاف مشروع Case Study", altEn: "Case study project cover", altDe: "Case Study Projekt Cover", sortOrder: 0, mediaAssetId: caseStudyCover.id, }, ], }, ]; for (const projectConfig of projects) { const { sections, assets, coverAssetId, ...projectValues } = projectConfig; const [project] = await db .insert(portfolioProject) .values(projectValues) .onConflictDoUpdate({ target: portfolioProject.slug, set: { categoryId: projectValues.categoryId, viewMode: projectValues.viewMode, titleAr: projectValues.titleAr, titleEn: projectValues.titleEn, titleDe: projectValues.titleDe, summaryAr: projectValues.summaryAr, summaryEn: projectValues.summaryEn, summaryDe: projectValues.summaryDe, clientName: projectValues.clientName, projectYear: projectValues.projectYear, serviceLabelAr: projectValues.serviceLabelAr, serviceLabelEn: projectValues.serviceLabelEn, serviceLabelDe: projectValues.serviceLabelDe, previewUrl: projectValues.previewUrl, coverImagePath: projectValues.coverImagePath, isFeatured: projectValues.isFeatured, isPublished: projectValues.isPublished, publishedAt: projectValues.publishedAt, sortOrder: projectValues.sortOrder, updatedAt: new Date(), }, }) .returning(); const created = await syncProjectContent( project.id, sections.map((section) => ({ type: section.type, titleAr: section.titleAr, titleEn: section.titleEn, titleDe: section.titleDe, bodyAr: section.bodyAr, bodyEn: section.bodyEn, bodyDe: section.bodyDe, imagePath: section.imagePath, linkUrl: section.linkUrl, sortOrder: section.sortOrder, })), assets.map((asset) => ({ kind: asset.kind, filePath: asset.filePath, altAr: asset.altAr, altEn: asset.altEn, altDe: asset.altDe, sortOrder: asset.sortOrder, })), ); await syncProjectMediaUsages(project.id, { coverAssetId, sectionUsages: created.createdSections.map((sectionRow, index) => ({ fieldKey: sectionRow.id, assetId: sections[index]?.mediaAssetId, })), assetUsages: created.createdAssets.map((assetRow, index) => ({ fieldKey: assetRow.id, assetId: assets[index]?.mediaAssetId, })), }); } } main() .then(async () => { await client.end(); }) .catch(async (error) => { console.error("Seed failed:", error); await client.end(); process.exit(1); });