This commit is contained in:
Your Name committed 2026-06-23 20:02:29 -04:00
1 parent ed69a4b840
commit 992335a52b
22 files changed
+1064 -346

No files matched your search

@@ -0,0 +1,72 @@
/*
Warnings:
- You are about to drop the column `scentId` on the `Product` table. All the data in the column will be lost.
- You are about to drop the `Hero` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `HeroTranslation` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Scent` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `ScentTranslation` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `brandId` to the `Product` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "HeroTranslation" DROP CONSTRAINT "HeroTranslation_heroId_fkey";
-- DropForeignKey
ALTER TABLE "Product" DROP CONSTRAINT "Product_scentId_fkey";
-- DropForeignKey
ALTER TABLE "ScentTranslation" DROP CONSTRAINT "ScentTranslation_scentId_fkey";
-- DropIndex
DROP INDEX "Product_scentId_categoryId_key";
-- AlterTable
ALTER TABLE "Product" DROP COLUMN "scentId",
ADD COLUMN "brandId" INTEGER NOT NULL;
-- DropTable
DROP TABLE "Hero";
-- DropTable
DROP TABLE "HeroTranslation";
-- DropTable
DROP TABLE "Scent";
-- DropTable
DROP TABLE "ScentTranslation";
-- CreateTable
CREATE TABLE "ProductBrand" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductBrand_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductBrandTranslation" (
"id" SERIAL NOT NULL,
"brandId" INTEGER NOT NULL,
"langCode" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductBrandTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProductBrand_name_key" ON "ProductBrand"("name");
-- CreateIndex
CREATE UNIQUE INDEX "ProductBrandTranslation_brandId_langCode_key" ON "ProductBrandTranslation"("brandId", "langCode");
-- AddForeignKey
ALTER TABLE "ProductBrandTranslation" ADD CONSTRAINT "ProductBrandTranslation_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "ProductBrand"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "ProductBrand"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+25 -20
View File
@@ -9,25 +9,25 @@ datasource db {
provider = "postgresql"
}
model Scent {
id Int @id @default(autoincrement())
name String @unique
model ProductBrand {
id Int @id @default(autoincrement())
name String @unique
products Product[]
translations ScentTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
translations ProductBrandTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ScentTranslation {
id Int @id @default(autoincrement())
scentId Int
scent Scent @relation(fields: [scentId], references: [id], onDelete: Cascade)
model ProductBrandTranslation {
id Int @id @default(autoincrement())
brandId Int
brand ProductBrand @relation(fields: [brandId], references: [id], onDelete: Cascade)
langCode String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([scentId, langCode])
@@unique([brandId, langCode])
}
model Category {
@@ -60,16 +60,15 @@ model Product {
name String?
description String?
details String?
scentId Int
scent Scent @relation(fields: [scentId], references: [id])
brandId Int
brand ProductBrand @relation(fields: [brandId], references: [id])
options Json? @default("[]") // Expected shape: [{ name: "Color", values: ["Red", "Blue"] }, ...]
variants ProductVariant[]
images ProductImage[]
reviews Review[]
translations ProductTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([scentId, categoryId])
}
model ProductTranslation {
@@ -96,15 +95,22 @@ model ProductImage {
model ProductVariant {
id Int @id @default(autoincrement())
size String
sku String? @unique
barcode String?
price Float? @default(0)
stock Int @default(0)
option1 String @default("") // Maps to Product.options[0]
option2 String @default("") // Maps to Product.options[1]
option3 String @default("") // Maps to Product.options[2]
productId Int
product Product @relation(fields: [productId], references: [id])
bomItems BomItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([productId, size])
@@unique([productId, option1, option2, option3])
}
model User {
@@ -147,7 +153,6 @@ model CustomerAddress {
updatedAt DateTime @updatedAt
}
model Coupon {
id Int @id @default(autoincrement())
code String @unique
+51 -40
View File
@@ -11,7 +11,7 @@ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const scentNames = [
const brandNames = [
'ตะไคร้', 'ลาเวนเดอร์', 'วานิลลา', 'ซากุระ', 'มะลิ', 'ยูคาลิปตัส', 'เฟรชลาเวนเดอร์',
'เปเปอร์มิ้น', 'โอเชี่ยน', 'เซเว่นซี', 'ดอกแก้ว', 'ดอกโมก', 'ลีลาวดี', 'ส้ม', 'มะนาว',
'แคนตาลูป', 'แอปเปิ้ลแดง', 'แอปเปิ้ลเขียว', 'ดอกลิลลี่', 'โรสเมรี่', 'อัญชัน',
@@ -65,7 +65,7 @@ async function main() {
// Auto-create with default template if not exists
if (!fs.existsSync(descPath)) {
const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{brand}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
fs.writeFileSync(descPath, defaultDesc, 'utf8');
}
@@ -78,7 +78,7 @@ async function main() {
}
const detailPath = path.join(detailsDir, `${safeName}.txt`);
if (!fs.existsSync(detailPath)) {
const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{scent}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{brand}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
fs.writeFileSync(detailPath, defaultDetail, 'utf8');
}
categoryConfigs[config.name] = categoryConfigs[config.name] || {};
@@ -121,17 +121,17 @@ async function main() {
}
process.stdout.write('✅ Seeded Core Menus.\n');
// 1. Seed Scents & Categories
const scents = [];
for (const name of scentNames) {
const s = await prisma.scent.upsert({
// 1. Seed Brands & Categories
const brands = [];
for (const name of brandNames) {
const s = await prisma.productBrand.upsert({
where: { name },
update: {},
create: { name }
});
scents.push(s);
brands.push(s);
}
process.stdout.write(`✅ Seeded ${scents.length} Scents.\n`);
process.stdout.write(`✅ Seeded ${brands.length} Brands.\n`);
const categories = [];
for (const config of categoryConfigs) {
@@ -144,43 +144,49 @@ async function main() {
}
process.stdout.write(`✅ Seeded ${categories.length} Categories.\n`);
// 2. Cross Scents x Categories to create Products
// 2. Cross Brands x Categories to create Products
process.stdout.write('📦 Generating Products and Variants...\n');
let productCount = 0;
let variantCount = 0;
for (const scent of scents) {
for (const brand of brands) {
for (const category of categories) {
const productName = `${scent.name} ${category.name}`;
const productName = `${brand.name} ${category.name}`;
// Get category description template and replace {{scent}}
let productDescription = categoryDescriptions[category.name] || `Product ${scent.name} in category ${category.name}`;
productDescription = productDescription.replace(/{{scent}}/g, scent.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(/{{scent}}/g, scent.name);
productDetails = productDetails.replace(/{{brand}}/g, brand.name);
productDetails = productDetails.replace(/{{scent}}/g, brand.name);
// Upsert Product
const product = await prisma.product.upsert({
where: {
scentId_categoryId: {
scentId: scent.id,
categoryId: category.id
}
},
update: {
name: productName,
description: productDescription,
details: productDetails
},
create: {
name: productName,
scentId: scent.id,
categoryId: category.id,
description: productDescription,
details: productDetails
}
// 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
@@ -214,15 +220,20 @@ async function main() {
const price = category.priceMap?.[size] ?? 0;
await prisma.productVariant.upsert({
where: {
productId_size: {
productId_option1_option2_option3: {
productId: product.id,
size: size
option1: size,
option2: "",
option3: ""
}
},
update: { price }, // อัปเดตราคาเมื่อ re-seed
create: {
productId: product.id,
size: size,
sku: `${brand.id}-${category.id}-${size}`.replace(/\s+/g, '-').toUpperCase(),
option1: size,
option2: "",
option3: "",
price: price
}
});
@@ -234,7 +245,7 @@ async function main() {
process.stdout.write(`\n✨ Seeding Complete!\n`);
process.stdout.write(`📊 Summary:\n`);
process.stdout.write(` - Scents: ${scents.length}\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`);
+139
View File
@@ -0,0 +1,139 @@
--- seed.cjs 2026-06-23
+++ seed.cjs.new 2026-06-23
@@ -12,7 +12,7 @@
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
-const scentNames = [
+const brandNames = [
'ตะไคร้', 'ลาเวนเดอร์', 'วานิลลา', 'ซากุระ', 'มะลิ', 'ยูคาลิปตัส', 'เฟรชลาเวนเดอร์',
'เปเปอร์มิ้น', 'โอเชี่ยน', 'เซเว่นซี', 'ดอกแก้ว', 'ดอกโมก', 'ลีลาวดี', 'ส้ม', 'มะนาว',
'แคนตาลูป', 'แอปเปิ้ลแดง', 'แอปเปิ้ลเขียว', 'ดอกลิลลี่', 'โรสเมรี่', 'อัญชัน',
@@ -65,7 +65,7 @@
// Auto-create with default template if not exists
if (!fs.existsSync(descPath)) {
- const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
+ const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{brand}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
fs.writeFileSync(descPath, defaultDesc, 'utf8');
}
@@ -78,7 +78,7 @@
}
const detailPath = path.join(detailsDir, `${safeName}.txt`);
if (!fs.existsSync(detailPath)) {
- const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{scent}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
+ const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{brand}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
fs.writeFileSync(detailPath, defaultDetail, 'utf8');
}
categoryConfigs[config.name] = categoryConfigs[config.name] || {};
@@ -122,17 +122,17 @@
process.stdout.write('✅ Seeded Core Menus.\n');
- // 1. Seed Scents & Categories
- const scents = [];
- for (const name of scentNames) {
- const s = await prisma.scent.upsert({
+ // 1. Seed Brands & Categories
+ const brands = [];
+ for (const name of brandNames) {
+ const s = await prisma.productBrand.upsert({
where: { name },
update: {},
create: { name }
});
- scents.push(s);
+ brands.push(s);
}
- process.stdout.write(`✅ Seeded ${scents.length} Scents.\n`);
+ process.stdout.write(`✅ Seeded ${brands.length} Brands.\n`);
const categories = [];
for (const config of categoryConfigs) {
@@ -145,43 +145,46 @@
}
process.stdout.write(`✅ Seeded ${categories.length} Categories.\n`);
- // 2. Cross Scents x Categories to create Products
+ // 2. Cross Brands x Categories to create Products
process.stdout.write('📦 Generating Products and Variants...\n');
let productCount = 0;
let variantCount = 0;
- for (const scent of scents) {
+ for (const brand of brands) {
for (const category of categories) {
- const productName = `${scent.name} ${category.name}`;
+ const productName = `${brand.name} ${category.name}`;
- // Get category description template and replace {{scent}}
- let productDescription = categoryDescriptions[category.name] || `Product ${scent.name} in category ${category.name}`;
- productDescription = productDescription.replace(/{{scent}}/g, scent.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);
+ // fallback for old template files that might still have {{scent}}
+ productDescription = productDescription.replace(/{{scent}}/g, brand.name);
let productDetails = category.detailsTemplate || '';
- productDetails = productDetails.replace(/{{scent}}/g, scent.name);
+ productDetails = productDetails.replace(/{{brand}}/g, brand.name);
+ productDetails = productDetails.replace(/{{scent}}/g, brand.name);
- // Upsert Product
- const product = await prisma.product.upsert({
- where: {
- scentId_categoryId: {
- scentId: scent.id,
- categoryId: category.id
- }
- },
- update: {
- name: productName,
- description: productDescription,
- details: productDetails
- },
- create: {
- name: productName,
- scentId: scent.id,
- categoryId: category.id,
- description: productDescription,
- details: productDetails
- }
- });
+ // Upsert Product manually since we don't have a 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
+ }
+ });
+ }
productCount++;
// 3. Create Default Product Image
@@ -235,7 +238,7 @@
process.stdout.write(`\n✨ Seeding Complete!\n`);
process.stdout.write(`📊 Summary:\n`);
- process.stdout.write(` - Scents: ${scents.length}\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`);