This commit is contained in:
Your Name committed 2026-06-24 19:15:04 -04:00
1 parent 81ed9af406
commit 4536db6741
56 files changed
+1127 -198

No files matched your search

+162 -59
View File
@@ -2,6 +2,20 @@ import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const productsRoute = new Elysia({ prefix: "/products" })
.decorate("validateVariants", (options: any[] = [], variants: any[] = []) => {
if (options.length > 3) throw new Error("Options array length cannot exceed 3.");
for (const v of variants) {
if (v.option1 && options[0] && !options[0].values.includes(v.option1)) {
throw new Error(`Variant option1 '${v.option1}' is not found in options[0].values [${options[0].values.join(', ')}]`);
}
if (v.option2 && options[1] && !options[1].values.includes(v.option2)) {
throw new Error(`Variant option2 '${v.option2}' is not found in options[1].values [${options[1].values.join(', ')}]`);
}
if (v.option3 && options[2] && !options[2].values.includes(v.option3)) {
throw new Error(`Variant option3 '${v.option3}' is not found in options[2].values [${options[2].values.join(', ')}]`);
}
}
})
.get("/brands", async () => {
return await prisma.productBrand.findMany({
orderBy: { name: "asc" },
@@ -177,13 +191,22 @@ export const productsRoute = new Elysia({ prefix: "/products" })
})
.put(
"/:id",
async ({ params, body, status }) => {
async ({ params, body, status, validateVariants }) => {
const id = parseInt(params.id);
if (isNaN(id)) return status(400, { message: "Invalid product ID" });
const { categoryId, description, details, variants, images } = body as any;
let options = (body as any).options;
try {
if (variants && Array.isArray(variants)) {
// If options is not passed but variants are, we might need to fetch existing options
if (options === undefined) {
const existingProduct = await prisma.product.findUnique({ where: { id } });
options = existingProduct?.options || [];
}
validateVariants(options, variants);
}
const updatedProduct = await prisma.$transaction(async (tx) => {
// 1. Update basic product info
const updateData: any = {};
@@ -196,48 +219,93 @@ export const productsRoute = new Elysia({ prefix: "/products" })
});
// 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,
sku: v.sku || null,
barcode: v.barcode || null,
stock: v.stock || 0,
option1: v.option1 || "",
option2: v.option2 || "",
option3: v.option3 || "",
price: v.price || 0,
bomItems: v.bomItems && v.bomItems.length > 0 ? {
create: v.bomItems.map((b: any) => ({
rawMaterialId: b.rawMaterialId,
quantity: b.quantity,
}))
} : undefined
}
if (variants && Array.isArray(variants)) {
const existingVariants = await tx.productVariant.findMany({ where: { productId: id } });
const incomingIds = variants.map((v: any) => v.id).filter(Boolean);
// Delete variants not in incoming payload
const variantsToDelete = existingVariants.filter(ev => !incomingIds.includes(ev.id));
if (variantsToDelete.length > 0) {
await tx.productVariant.deleteMany({
where: { id: { in: variantsToDelete.map(ev => ev.id) } }
});
}
for (const v of variants) {
if (v.id) {
// Check if it really belongs to this product
const exists = existingVariants.find(ev => ev.id === v.id);
if (exists) {
await tx.productVariant.update({
where: { id: v.id },
data: {
sku: v.sku || null,
barcode: v.barcode || null,
stock: v.stock || 0,
option1: v.option1 || "",
option2: v.option2 || "",
option3: v.option3 || "",
price: v.price || 0,
}
});
}
} else {
await tx.productVariant.create({
data: {
productId: id,
sku: v.sku || null,
barcode: v.barcode || null,
stock: v.stock || 0,
option1: v.option1 || "",
option2: v.option2 || "",
option3: v.option3 || "",
price: v.price || 0,
bomItems: v.bomItems && v.bomItems.length > 0 ? {
create: v.bomItems.map((b: any) => ({
rawMaterialId: b.rawMaterialId,
quantity: b.quantity,
}))
} : undefined
}
});
}
}
}
// 3. Sync Images
await tx.productImage.deleteMany({
where: { productId: id },
});
if (images && Array.isArray(images)) {
const existingImages = await tx.productImage.findMany({ where: { productId: id } });
const incomingImgIds = images.map((img: any) => img.id).filter(Boolean);
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),
}
const imagesToDelete = existingImages.filter(ei => !incomingImgIds.includes(ei.id));
if (imagesToDelete.length > 0) {
await tx.productImage.deleteMany({
where: { id: { in: imagesToDelete.map(ei => ei.id) } }
});
}
for (const [idx, img] of images.entries()) {
if (img.id) {
const exists = existingImages.find(ei => ei.id === img.id);
if (exists) {
await tx.productImage.update({
where: { id: img.id },
data: {
url: img.url,
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
}
});
}
} else {
await tx.productImage.create({
data: {
productId: id,
url: img.url,
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
}
});
}
}
}
return prod;
@@ -246,7 +314,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
return { success: true, product: updatedProduct };
} catch (err: any) {
console.error("Update product error:", err);
return status(500, { success: false, message: "Failed to update product" });
return status(500, { success: false, message: err?.message || "Failed to update product" });
}
},
{
@@ -283,7 +351,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
return { success: true, image: newImage };
} catch (err: any) {
console.error("Add product image error:", err);
return status(500, { success: false, message: "Failed to add product image" });
return status(500, { success: false, message: err?.message || "Failed to add product image" });
}
},
{
@@ -294,36 +362,71 @@ export const productsRoute = new Elysia({ prefix: "/products" })
)
.post(
"/",
async ({ body, status }) => {
async ({ body, status, validateVariants }) => {
const payload = body as any;
try {
const product = await prisma.product.create({
data: {
categoryId: payload.categoryId,
brandId: payload.brandId,
name: payload.name || "New Product",
price: payload.price || 0,
description: payload.description || "",
details: payload.details || "",
options: payload.options || [],
if (payload.variants && Array.isArray(payload.variants)) {
validateVariants(payload.options || [], payload.variants);
}
const product = await prisma.$transaction(async (tx) => {
const prod = await tx.product.create({
data: {
categoryId: payload.categoryId,
brandId: payload.brandId,
name: payload.name || "New Product",
price: payload.price || 0,
description: payload.description || "",
details: payload.details || "",
options: payload.options || [],
}
});
await tx.productTranslation.create({
data: {
productId: prod.id,
langCode: 'th',
name: payload.name || "New Product",
description: payload.description || "",
details: payload.details || "",
}
});
if (payload.variants && payload.variants.length > 0) {
for (const v of payload.variants) {
await tx.productVariant.create({
data: {
productId: prod.id,
sku: v.sku || null,
barcode: v.barcode || null,
stock: v.stock || 0,
option1: v.option1 || "",
option2: v.option2 || "",
option3: v.option3 || "",
price: v.price || 0,
}
});
}
}
});
// Also create the TH translation by default
await prisma.productTranslation.create({
data: {
productId: product.id,
langCode: 'th',
name: payload.name || "New Product",
description: payload.description || "",
details: payload.details || "",
if (payload.images && payload.images.length > 0) {
for (const [idx, img] of payload.images.entries()) {
await tx.productImage.create({
data: {
productId: prod.id,
url: img.url,
order: img.order ?? idx,
}
});
}
}
return prod;
});
return { success: true, product };
} catch (err: any) {
console.error("Create product error:", err);
return status(500, { success: false, message: "Failed to create product" });
return status(500, { success: false, message: err?.message || "Failed to create product" });
}
},
{
@@ -402,7 +505,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
return { success: true, product: prod };
} catch (err: any) {
console.error("Patch product error:", err);
return status(500, { success: false, message: "Failed to patch product" });
return status(500, { success: false, message: err?.message || "Failed to patch product" });
}
},
{