Files

270 lines
11 KiB
JavaScript

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