fix: generate missing migration for Menu and Newsletter tables

This commit is contained in:
Your Name committed 2026-06-14 00:16:05 -04:00
1 parent 363be70e11
commit f5029fc935
123 files changed
+8683

No files matched your search

+279
View File
@@ -0,0 +1,279 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const productsRoute = new Elysia({ prefix: "/products" })
.get("/scents", async () => {
return await prisma.scent.findMany({
orderBy: { name: "asc" }
});
})
.get("/", async ({ query }) => {
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
const search = query.search?.trim() || undefined;
const lang = query.lang || 'th';
// Pagination params
const page = query.page ? parseInt(query.page) : 1;
const limit = query.limit ? parseInt(query.limit) : 12;
const skip = (page - 1) * limit;
const where = {
...(categoryId && { categoryId }),
...(scentId && { scentId }),
...(search && {
OR: [
{ translations: { some: { name: { contains: search, mode: "insensitive" } } } },
{ scent: { name: { contains: search, mode: "insensitive" } } },
{ category: { name: { contains: search, mode: "insensitive" } } },
],
}),
};
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
include: {
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: { orderBy: { order: "asc" } },
},
skip,
take: limit,
}),
prisma.product.count({ where }),
]);
// Map translations
const mappedProducts = products.map(p => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj, category: { ...p.category, name: p.category.name } }; // Actually for 'all', keep as is, but wait, category name needs resolving if we want it? Let's just resolve it simply below.
}
else {
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
const catTrans = p.category.translations?.find((t) => t.langCode === lang) || p.category.translations?.find((t) => t.langCode === 'th') || p.category.translations?.[0];
return {
...p,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: catTrans?.name || p.category.name }
};
}
});
mappedProducts.sort((a, b) => {
const aName = typeof a.name === 'string' ? a.name : (a.name?.th || '');
const bName = typeof b.name === 'string' ? b.name : (b.name?.th || '');
return aName.localeCompare(bName);
});
return {
data: mappedProducts,
meta: {
total,
page,
limit,
lastPage: Math.ceil(total / limit),
},
};
})
.get("/:id", async ({ params, query, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
const lang = query.lang || 'th';
const product = await prisma.product.findUnique({
where: { id },
include: {
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: { orderBy: { order: "asc" } },
},
});
if (!product)
return status(404, { message: "Product not found" });
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
product.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...product, name: nameObj, description: descObj, details: detailsObj };
}
else {
const trans = product.translations.find(t => t.langCode === lang) || product.translations.find(t => t.langCode === 'th') || product.translations[0];
const catTrans = product.category.translations?.find((t) => t.langCode === lang) || product.category.translations?.find((t) => t.langCode === 'th') || product.category.translations?.[0];
return {
...product,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...product.category, name: catTrans?.name || product.category.name }
};
}
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
const { categoryId, description, details, variants, images } = body;
try {
const updatedProduct = await prisma.$transaction(async (tx) => {
// 1. Update basic product info
const prod = await tx.product.update({
where: { id },
data: { categoryId },
});
// 2. Sync Variants
await tx.productVariant.deleteMany({
where: { productId: id },
});
if (variants && variants.length > 0) {
for (const v of variants) {
await tx.productVariant.create({
data: {
productId: id,
size: v.size,
price: v.price,
bomItems: v.bomItems && v.bomItems.length > 0 ? {
create: v.bomItems.map((b) => ({
rawMaterialId: b.rawMaterialId,
quantity: b.quantity,
}))
} : undefined
}
});
}
}
// 3. Sync Images
await tx.productImage.deleteMany({
where: { productId: id },
});
if (images && images.length > 0) {
for (const [idx, img] of images.entries()) {
await tx.productImage.create({
data: {
productId: id,
url: img.url,
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
}
});
}
}
return prod;
});
return { success: true, product: updatedProduct };
}
catch (err) {
console.error("Update product error:", err);
return status(500, { success: false, message: "Failed to update product" });
}
}, {
body: t.Any(),
detail: {
summary: "Update Product",
tags: ["products"],
},
})
.patch("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
try {
const payload = body;
const prodData = {};
if (payload.categoryId !== undefined)
prodData.categoryId = payload.categoryId;
if (payload.scentId !== undefined)
prodData.scentId = payload.scentId;
if (payload.price !== undefined)
prodData.price = payload.price;
const prod = await prisma.product.update({
where: { id },
data: prodData,
});
// Update translations
// If name, description, details are strings, treat as 'th'
// If they are objects, iterate over them
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of languages) {
const updateData = {};
let shouldUpdate = false;
if (payload.name) {
const val = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name[lang];
if (val !== undefined && val !== null) {
updateData.name = val;
shouldUpdate = true;
}
}
if (payload.description) {
const val = typeof payload.description === 'string' ? (lang === 'th' ? payload.description : null) : payload.description[lang];
if (val !== undefined && val !== null) {
updateData.description = val;
shouldUpdate = true;
}
}
if (payload.details) {
const val = typeof payload.details === 'string' ? (lang === 'th' ? payload.details : null) : payload.details[lang];
if (val !== undefined && val !== null) {
updateData.details = val;
shouldUpdate = true;
}
}
if (shouldUpdate) {
// First check if translation exists to create or update
const existing = await prisma.productTranslation.findUnique({
where: { productId_langCode: { productId: id, langCode: lang } }
});
if (existing) {
await prisma.productTranslation.update({
where: { id: existing.id },
data: updateData
});
}
else if (updateData.name) { // name is required for creation
await prisma.productTranslation.create({
data: {
productId: id,
langCode: lang,
name: updateData.name,
description: updateData.description,
details: updateData.details
}
});
}
}
}
return { success: true, product: prod };
}
catch (err) {
console.error("Patch product error:", err);
return status(500, { success: false, message: "Failed to patch product" });
}
}, {
body: t.Any(),
detail: {
summary: "Partial Update Product",
tags: ["products"],
},
});
//# sourceMappingURL=products.js.map