Files
homaroma/apps/backend/prisma/seed.cjs
T
2026-06-23 20:02:29 -04:00

267 lines
11 KiB
JavaScript

// seed.cjs - Prisma 7 Seed Script
// Note: Bun loads .env automatically - no need for dotenv
const fs = require('fs');
const path = require('path');
const { PrismaClient } = require('@prisma/client');
const { Pool } = require('pg');
const { PrismaPg } = require('@prisma/adapter-pg');
const bcrypt = require('bcryptjs');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const brandNames = [
'ตะไคร้', 'ลาเวนเดอร์', 'วานิลลา', 'ซากุระ', 'มะลิ', 'ยูคาลิปตัส', 'เฟรชลาเวนเดอร์',
'เปเปอร์มิ้น', 'โอเชี่ยน', 'เซเว่นซี', 'ดอกแก้ว', 'ดอกโมก', 'ลีลาวดี', 'ส้ม', 'มะนาว',
'แคนตาลูป', 'แอปเปิ้ลแดง', 'แอปเปิ้ลเขียว', 'ดอกลิลลี่', 'โรสเมรี่', 'อัญชัน',
'กุหลาบแดง', 'หมากฝรั่ง', 'ผลไม้รวม', 'มะกรูด', 'แซนด้าวูส', 'ดอกปีบ', 'ดอกบัว',
'โอโซน', 'ไทยสปา', 'กล้วย', 'กาแฟ', 'ดอกชมนาด', 'กลิ่นน้ำนม', 'ดอกการะเวก',
'ดอกจำปา', 'ดอกพิกุล', 'ดอกพุด', 'ดอกราตรี', 'ดอกลำดวน', 'ทับทิม', 'มะละกอสุก',
'ดอกราชาวดี', 'กลิ่นลูกพีช', 'กลิ่นองุ่น'
];
const categoryConfigs = [
{
name: 'Oil Ozone',
sizes: ['30ml', '60ml', '85ml', '120ml'],
priceMap: { '30ml': 69, '60ml': 100, '85ml': 120, '120ml': 150 }
},
{
name: 'Aroma Oil',
sizes: ['30ml', '60ml', '85ml', '120ml'],
priceMap: { '30ml': 90, '60ml': 150, '85ml': 165, '120ml': 225 } // 120ml ปรับเป็น 225 (฿1.875/ml) ให้ถูกกว่า 85ml ต่อ ml
},
{
name: 'Reed Diffuser',
sizes: ['60ml', '85ml', '120ml'],
priceMap: { '60ml': 120, '85ml': 160, '120ml': 190 }
},
{
name: 'Room Spray',
sizes: ['85ml'],
priceMap: { '85ml': 150 }
},
{
name: 'Car Diffuser',
sizes: ['25ml'],
priceMap: { '25ml': 60 }
}
];
async function main() {
process.stdout.write('🚀 Starting Advanced Seeding (Products & Variants)...\n');
// Load or create category description files
const descriptionsDir = path.join(__dirname, 'descriptions');
if (!fs.existsSync(descriptionsDir)) {
fs.mkdirSync(descriptionsDir);
}
const categoryDescriptions = {};
for (const config of categoryConfigs) {
const safeName = config.name.replace(/\s+/g, '_');
const descPath = path.join(descriptionsDir, `${safeName}.txt`);
// Auto-create with default template if not exists
if (!fs.existsSync(descPath)) {
const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{brand}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
fs.writeFileSync(descPath, defaultDesc, 'utf8');
}
categoryDescriptions[config.name] = fs.readFileSync(descPath, 'utf8');
// Auto-create category details
const detailsDir = path.join(__dirname, 'details');
if (!fs.existsSync(detailsDir)) {
fs.mkdirSync(detailsDir);
}
const detailPath = path.join(detailsDir, `${safeName}.txt`);
if (!fs.existsSync(detailPath)) {
const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{brand}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
fs.writeFileSync(detailPath, defaultDetail, 'utf8');
}
categoryConfigs[config.name] = categoryConfigs[config.name] || {};
categoryConfigs[config.name].detailsTemplate = fs.readFileSync(detailPath, 'utf8');
}
// 0. Seed Admin User
const adminPassword = await bcrypt.hash('homaroma123', 10);
await prisma.user.upsert({
where: { username: 'admin' },
update: { password: adminPassword },
create: {
username: 'admin',
password: adminPassword,
role: 'ADMIN'
}
});
process.stdout.write('✅ Seeded Admin User (admin / homaroma123).\n');
// 0.5 Seed Core Menus
const coreMenus = [
{ name: "main", items: [{ id: 1, label: "Home", url: "/", order: 1 }, { id: 2, label: "Shop", url: "/shop", order: 2 }, { id: 3, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 4, label: "About Us", url: "/about", order: 4 }] },
{ name: "footer-explore", items: [{ id: 5, label: "Home", url: "/", order: 1 }, { id: 6, label: "Shop", url: "/shop", order: 2 }, { id: 7, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 8, label: "About Us", url: "/about", order: 4 }] },
{ name: "footer-support", items: [{ id: 9, label: "Track Order", url: "/track-order", order: 1 }, { id: 10, label: "FAQ", url: "/faq", order: 2 }, { id: 11, label: "Contact Us", url: "/contact", order: 3 }, { id: 12, label: "Delivery & Returns", url: "/delivery", order: 4 }] }
];
for (const menu of coreMenus) {
const m = await prisma.menu.upsert({
where: { name: menu.name },
update: {},
create: { name: menu.name }
});
for (const item of menu.items) {
await prisma.menuItem.upsert({
where: { id: item.id },
update: { label: item.label, url: item.url, order: item.order, menuId: m.id },
create: { id: item.id, label: item.label, url: item.url, order: item.order, menuId: m.id }
});
}
}
process.stdout.write('✅ Seeded Core Menus.\n');
// 1. Seed Brands & Categories
const brands = [];
for (const name of brandNames) {
const s = await prisma.productBrand.upsert({
where: { name },
update: {},
create: { name }
});
brands.push(s);
}
process.stdout.write(`✅ Seeded ${brands.length} Brands.\n`);
const categories = [];
for (const config of categoryConfigs) {
const c = await prisma.category.upsert({
where: { name: config.name },
update: {},
create: { name: config.name }
});
categories.push({ ...c, allowedSizes: config.sizes, priceMap: config.priceMap, detailsTemplate: categoryConfigs[config.name].detailsTemplate });
}
process.stdout.write(`✅ Seeded ${categories.length} Categories.\n`);
// 2. Cross Brands x Categories to create Products
process.stdout.write('📦 Generating Products and Variants...\n');
let productCount = 0;
let variantCount = 0;
for (const brand of brands) {
for (const category of categories) {
const productName = `${brand.name} ${category.name}`;
// Get category description template and replace {{brand}}
let productDescription = categoryDescriptions[category.name] || `Product ${brand.name} in category ${category.name}`;
productDescription = productDescription.replace(/{{brand}}/g, brand.name);
productDescription = productDescription.replace(/{{scent}}/g, brand.name);
let productDetails = category.detailsTemplate || '';
productDetails = productDetails.replace(/{{brand}}/g, brand.name);
productDetails = productDetails.replace(/{{scent}}/g, brand.name);
// Upsert Product without unique constraint on brandId + categoryId
let product = await prisma.product.findFirst({
where: { brandId: brand.id, categoryId: category.id }
});
if (product) {
product = await prisma.product.update({
where: { id: product.id },
data: {
name: productName,
description: productDescription,
details: productDetails
}
});
} else {
product = await prisma.product.create({
data: {
name: productName,
brandId: brand.id,
categoryId: category.id,
description: productDescription,
details: productDetails,
options: category.allowedSizes.length > 0 ? [{ name: "Size", values: category.allowedSizes }] : []
}
});
}
productCount++;
// 3. Create Default Product Image
// Using existing category-based image paths as initial seed data
const defaultImagePath = category.name === 'Oil Ozone' || category.name === 'Aroma Oil' || category.name === 'Car Diffuser'
? '/images/oil_ozone.png'
: '/images/reed_diffuser.png';
await prisma.productImage.upsert({
where: { id: (product.id * 10) + 1 }, // Pseudo-stable ID for seed
update: { url: defaultImagePath },
create: {
id: (product.id * 10) + 1,
productId: product.id,
url: defaultImagePath,
order: 0
}
}).catch(() => {
// Fallback for cases where ID might conflict or upsert on non-unique field
return prisma.productImage.create({
data: {
productId: product.id,
url: defaultImagePath,
order: 0
}
});
});
// 4. Create Variants (Sizes) with prices from priceMap
for (const size of category.allowedSizes) {
const price = category.priceMap?.[size] ?? 0;
await prisma.productVariant.upsert({
where: {
productId_option1_option2_option3: {
productId: product.id,
option1: size,
option2: "",
option3: ""
}
},
update: { price }, // อัปเดตราคาเมื่อ re-seed
create: {
productId: product.id,
sku: `${brand.id}-${category.id}-${size}`.replace(/\s+/g, '-').toUpperCase(),
option1: size,
option2: "",
option3: "",
price: price
}
});
variantCount++;
}
}
process.stdout.write(`.`); // Progress indicator
}
process.stdout.write(`\n✨ Seeding Complete!\n`);
process.stdout.write(`📊 Summary:\n`);
process.stdout.write(` - Brands: ${brands.length}\n`);
process.stdout.write(` - Categories: ${categories.length}\n`);
process.stdout.write(` - Products: ${productCount}\n`);
process.stdout.write(` - Variants: ${variantCount}\n`);
// Fix PostgreSQL sequence desync for tables where we hardcoded IDs
process.stdout.write('🔄 Resetting sequences for hardcoded ID tables...\n');
await prisma.$executeRawUnsafe(`SELECT setval('"MenuItem_id_seq"', COALESCE((SELECT MAX(id) FROM "MenuItem") + 1, 1), false);`);
await prisma.$executeRawUnsafe(`SELECT setval('"ProductImage_id_seq"', COALESCE((SELECT MAX(id) FROM "ProductImage") + 1, 1), false);`);
process.stdout.write('✅ Sequences reset successfully.\n');
}
main()
.catch((e) => {
process.stderr.write(`❌ Error: ${e.message}\n`);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});