import { env } from "../config/env.js"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { normalizeIrMobile, SECTION_KEYS, sectionSchemas } from "@imen/shared"; import { hashPassword } from "../auth/auth.service.js"; import { S3Storage, toWebp } from "../media/s3-storage.js"; import { createPrismaClient } from "../prisma/prisma.service.js"; import { CATEGORIES } from "./data/categories.js"; import { CATEGORY_COVERS, SECTIONS } from "./data/sections.js"; const ASSETS_DIR = process.env.SEED_ASSETS_DIR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../seed-assets"); const OVERWRITE_SECTIONS = ["1", "true"].includes(process.env.SEED_OVERWRITE_SECTIONS ?? ""); const prisma = createPrismaClient(); const storage = new S3Storage(); const uploaded = new Map(); async function asset(rel, alt = "") { const cached = uploaded.get(rel); if (cached) return cached; const key = `seed/${rel.replace(/\.[^.]+$/, "")}.webp`; let media = await prisma.media.findUnique({ where: { key } }); if (!media) { const image = await toWebp(await readFile(path.join(ASSETS_DIR, rel))); try { await storage.put(key, image.data, "image/webp"); } catch { try { const targetPath = path.resolve(ASSETS_DIR, "../../.s3data/imen-media", key); await mkdir(path.dirname(targetPath), { recursive: true }); await writeFile(targetPath, image.data); } catch { } } media = await prisma.media.create({ data: { key, mime: "image/webp", size: image.size, width: image.width, height: image.height, alt, originalName: path.basename(rel), }, }); } const result = { id: media.id, url: storage.publicUrl(key) }; uploaded.set(rel, result); return result; } async function resolveAssets(value) { if (Array.isArray(value)) return Promise.all(value.map(resolveAssets)); if (value && typeof value === "object") { const entries = await Promise.all(Object.entries(value).map(async ([k, v]) => { if (k === "url" && typeof v === "string" && v.startsWith("asset:")) { return [k, (await asset(v.slice("asset:".length))).url]; } return [k, await resolveAssets(v)]; })); return Object.fromEntries(entries); } return value; } async function seedSections() { for (const key of SECTION_KEYS) { const exists = await prisma.siteSection.findUnique({ where: { key } }); if (exists && !OVERWRITE_SECTIONS) continue; const data = sectionSchemas[key].parse(await resolveAssets(SECTIONS[key])); await prisma.siteSection.upsert({ where: { key }, create: { key, data }, update: { data } }); console.log(` section ${key}`); } } async function seedCatalog() { for (const [ci, c] of CATEGORIES.entries()) { if (await prisma.category.findUnique({ where: { slug: c.slug } })) continue; const coverPath = CATEGORY_COVERS[c.slug]; const cover = coverPath ? await asset(coverPath, c.name) : null; const category = await prisma.category.create({ data: { slug: c.slug, code: c.code, name: c.name, nameEn: c.en, shortDesc: c.shortDesc, fullDesc: c.fullDesc, icon: c.icon, coverImageId: cover?.id ?? null, sortOrder: ci, groups: { create: (c.groups ?? []).map((g, i) => ({ slug: g.slug, name: g.name, desc: g.desc, sortOrder: i, })), }, }, include: { groups: true }, }); const groupIds = new Map(category.groups.map((g) => [g.slug, g.id])); for (const [pi, p] of c.products.entries()) { const image = p.image ? await asset(p.image.replace(/^\/images\//, ""), p.name) : null; await prisma.product.create({ data: { categoryId: category.id, groupId: p.group ? (groupIds.get(p.group) ?? null) : null, code: p.code, slug: p.code.toLowerCase(), name: p.name, nameEn: p.en, desc: p.desc, specs: [...p.specs], variants: [...(p.variants ?? [])], imageId: image?.id ?? null, sortOrder: pi, }, }); } console.log(` category ${c.slug} (${c.products.length} products)`); } } async function seedAdmin() { if (!env.ADMIN_PHONE || !env.ADMIN_PASSWORD) { console.log(" ADMIN_PHONE / ADMIN_PASSWORD not set — skipping admin user"); return; } const phone = normalizeIrMobile(env.ADMIN_PHONE); if (!/^09\d{9}$/.test(phone)) { console.log(` ADMIN_PHONE invalid (${env.ADMIN_PHONE}) — skipping admin user`); return; } const existing = await prisma.adminUser.findUnique({ where: { phone } }); if (existing) return; const legacy = await prisma.adminUser.findFirst({ where: { role: "ADMIN", phone: { contains: "@" } }, orderBy: { id: "asc" }, }); if (legacy) { await prisma.adminUser.update({ where: { id: legacy.id }, data: { phone, name: env.ADMIN_NAME, passwordHash: await hashPassword(env.ADMIN_PASSWORD) }, }); console.log(` admin user migrated to ${phone}`); return; } await prisma.adminUser.create({ data: { phone, name: env.ADMIN_NAME, role: "ADMIN", passwordHash: await hashPassword(env.ADMIN_PASSWORD), }, }); console.log(` admin user ${phone}`); } async function seedMessages() { const count = await prisma.contactMessage.count(); if (count > 0) return; const samples = [ { name: "مهندس رضا کاظمی", org: "مجتمع پتروشیمی خلیج فارس", phone: "09121112233", message: "درخواست پیش‌فاکتور ۵۰۰ عدد هارنس ایمنی دو قلاب و کلاه ایمنی استاندارد EN 397 برای فاز ۲ پتروشیمی.", status: "NEW", createdAt: new Date(Date.now() - 1000 * 60 * 35), }, { name: "سارا محمدی", org: "هلدینگ ساختمانی آرکان", phone: "09122223344", message: "استعلام قیمت و هماهنگی جلسه بازدید جهت عقد قرارداد سالانه تأمین تجهیزات حفاظت فردی ۱۲ پروژه فعال.", status: "NEW", createdAt: new Date(Date.now() - 1000 * 60 * 120), }, { name: "امیرحسین راد", org: "صنایع معدنی کاوه", phone: "09123334455", message: "درخواست تأمین فوری ۲۰۰ جفت دستکش ایمنی ضدبرش و کفش ایمنی ضداسید و پنجه کامپوزیت برای سایت یزد.", status: "READ", createdAt: new Date(Date.now() - 1000 * 60 * 60 * 8), }, { name: "مهندس علیرضا بهرامی", org: "شرکت نفت و گاز پارس", phone: "09124445566", message: "تقاضای ارسال کاتالوگ و نمونه عینک ایمنی استاندارد EN 166 و محافظ گوش ایرماف جهت آزمایش واحد HSE.", status: "NEW", createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), }, { name: "مریم رضایی", org: "داروسازی رازک", phone: "09125556677", message: "درخواست مشاوره تخصصی در رابطه با ماسک‌های فیلتردار ذرات شیمیایی و لباس‌های یکبار مصرف آزمایشگاهی.", status: "READ", createdAt: new Date(Date.now() - 1000 * 60 * 60 * 36), }, { name: "مهندس مهدی یوسفی", org: "فولاد مبارکه اصفهان", phone: "09126667788", message: "درخواست برگزاری جلسه فنی برای تمدید قرارداد تأمین لباس کار نسوز، شیلد محافظ صورت و پابند نسوز.", status: "ARCHIVED", createdAt: new Date(Date.now() - 1000 * 60 * 60 * 72), }, { name: "پیمان نادری", org: "خطوط لوله و مخابرات نفت", phone: "09127778899", message: "استعلام موجودی تجهیزات نجات در فضاهای بسته، سه‌پایه نجات (Tripod) و وینچ همراه با گواهی کالیبراسیون.", status: "NEW", createdAt: new Date(Date.now() - 1000 * 60 * 60 * 96), }, { name: "دکتر مجید اسدی", org: "سیمان تهران", phone: "09128889900", message: "درخواست برگزاری دوره آموزشی کار در ارتفاع برای پرسنل پیمانکار و بازرسی دوره‌ای خطوط حیات (Lifeline).", status: "READ", createdAt: new Date(Date.now() - 1000 * 60 * 60 * 120), }, ]; for (const msg of samples) { await prisma.contactMessage.create({ data: msg }); } console.log(` seeded ${samples.length} sample contact messages`); } async function main() { if (env.S3_ENSURE_BUCKET) { try { await storage.ensureBucket(); } catch { console.log(" ensureBucket skipped"); } } console.log("seeding sections…"); await seedSections(); console.log("seeding catalog…"); await seedCatalog(); console.log("seeding admin…"); await seedAdmin(); console.log("seeding messages…"); await seedMessages(); const [categories, products, media] = await Promise.all([ prisma.category.count(), prisma.product.count(), prisma.media.count(), ]); console.log(`done: ${categories} categories, ${products} products, ${media} media files`); } try { await main(); } catch (e) { console.error(e); process.exitCode = 1; } finally { await prisma.$disconnect(); } //# sourceMappingURL=seed.js.map