270 lines
11 KiB
JavaScript
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
|