x
This commit is contained in:
1 parent
ebc8f2ffeb
commit
7fd3c40ae2
2 files changed
+240
-27
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
productImageErrorResponse,
|
||||
ProductImagePayloadError,
|
||||
} from "../lib/productImages";
|
||||
import { defaultProductDetailSections } from "../lib/productDetailSections";
|
||||
|
||||
// ─── Helper: stream JSON as file download ────────────────────────────────────
|
||||
|
||||
@@ -90,10 +91,19 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
if (section === "products" || section === "all") {
|
||||
const products = await prisma.product.findMany({
|
||||
include: {
|
||||
variants: true,
|
||||
images: true,
|
||||
variants: { orderBy: { id: "asc" } },
|
||||
images: { orderBy: { order: "asc" } },
|
||||
translations: true,
|
||||
attributes: { include: { attributeDefinition: true, translations: true } },
|
||||
attributes: {
|
||||
include: {
|
||||
translations: true,
|
||||
attributeDefinition: {
|
||||
select: { id: true, code: true, label: true, type: true, unit: true },
|
||||
},
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
},
|
||||
detailSections: { orderBy: { order: "asc" } },
|
||||
brand: { include: { translations: true } },
|
||||
category: { select: { id: true, name: true } },
|
||||
},
|
||||
@@ -107,9 +117,14 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
categoryId: p.categoryId,
|
||||
categoryName: p.category?.name,
|
||||
brandId: p.brandId,
|
||||
brandName: p.brand?.name,
|
||||
variantCount: p.variants.length,
|
||||
imageCount: p.images.length,
|
||||
attributeCount: p.attributes.length,
|
||||
detailSectionCount: p.detailSections.length,
|
||||
isDeleted: p.isDeleted,
|
||||
createdAt: p.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_products_${dt}.csv`);
|
||||
@@ -120,11 +135,15 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
// full catalog
|
||||
const categories = await prisma.category.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: { translations: true, media: { orderBy: [{ sortOrder: "asc" }, { id: "asc" }] } },
|
||||
include: {
|
||||
translations: true,
|
||||
media: { orderBy: [{ sortOrder: "asc" }, { id: "asc" }] },
|
||||
sections: { orderBy: { order: "asc" } },
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const attributes = await prisma.attributeDefinition.findMany({
|
||||
include: { translations: true, media: { orderBy: [{ sortOrder: "asc" }, { id: "asc" }] } },
|
||||
include: { translations: true },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
const brands = await prisma.productBrand.findMany({
|
||||
@@ -144,6 +163,9 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
brandName: p.brand?.name,
|
||||
variantCount: p.variants.length,
|
||||
imageCount: p.images.length,
|
||||
attributeCount: p.attributes.length,
|
||||
detailSectionCount: p.detailSections.length,
|
||||
isDeleted: p.isDeleted,
|
||||
createdAt: p.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_catalog_${dt}.csv`);
|
||||
@@ -167,7 +189,11 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
if (section === "categories") {
|
||||
const categories = await prisma.category.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: { translations: true },
|
||||
include: {
|
||||
translations: true,
|
||||
media: { orderBy: [{ sortOrder: "asc" }, { id: "asc" }] },
|
||||
sections: { orderBy: { order: "asc" } },
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
if (format === "csv") {
|
||||
@@ -175,6 +201,9 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
imageUrl: c.imageUrl,
|
||||
layoutType: c.layoutType,
|
||||
mediaCount: c.media.length,
|
||||
sectionCount: c.sections.length,
|
||||
createdAt: c.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_categories_${dt}.csv`);
|
||||
@@ -541,6 +570,7 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
products, categories, customers, orders,
|
||||
articles, pages, menus, coupons, promotions,
|
||||
media, subscribers, rawMaterials, tiktokOrders, shopeeOrders, lazadaOrders,
|
||||
productDetailSections, categorySections, productAttributes,
|
||||
] = await Promise.all([
|
||||
prisma.product.count(),
|
||||
prisma.category.count({ where: { deletedAt: null } }),
|
||||
@@ -557,6 +587,9 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
prisma.tiktokOrder.count(),
|
||||
prisma.shopeeOrder.count(),
|
||||
prisma.lazadaOrder.count(),
|
||||
prisma.productDetailSection.count(),
|
||||
prisma.categorySection.count(),
|
||||
prisma.productAttribute.count(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -577,6 +610,9 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
tiktokOrders,
|
||||
shopeeOrders,
|
||||
lazadaOrders,
|
||||
productDetailSections,
|
||||
categorySections,
|
||||
productAttributes,
|
||||
},
|
||||
pgDumpNote: "Full pg_dump requires shell access. Use SSH: pg_dump -U postgres <dbname> > backup.sql",
|
||||
};
|
||||
@@ -847,10 +883,13 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
|
||||
const summary = {
|
||||
categories: 0,
|
||||
categorySections: 0,
|
||||
attributes: 0,
|
||||
brands: 0,
|
||||
products: 0,
|
||||
variants: 0,
|
||||
productAttributes: 0,
|
||||
detailSections: 0,
|
||||
};
|
||||
|
||||
// 1. Categories
|
||||
@@ -862,14 +901,21 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
const savedCat = await prisma.category.upsert({
|
||||
where: { name: cat.name },
|
||||
update: {
|
||||
imageUrl: cat.imageUrl,
|
||||
imageUrl: cat.imageUrl ?? null,
|
||||
layoutType: cat.layoutType || 'split_showcase',
|
||||
deletedAt: null,
|
||||
featuredSeries: cat.featuredSeries ?? undefined,
|
||||
uspBenefits: cat.uspBenefits ?? undefined,
|
||||
filterConfig: cat.filterConfig ?? undefined,
|
||||
deletedAt: cat.deletedAt ? new Date(cat.deletedAt) : null,
|
||||
},
|
||||
create: {
|
||||
name: cat.name,
|
||||
imageUrl: cat.imageUrl,
|
||||
imageUrl: cat.imageUrl ?? null,
|
||||
layoutType: cat.layoutType || 'split_showcase',
|
||||
featuredSeries: cat.featuredSeries ?? undefined,
|
||||
uspBenefits: cat.uspBenefits ?? undefined,
|
||||
filterConfig: cat.filterConfig ?? undefined,
|
||||
deletedAt: cat.deletedAt ? new Date(cat.deletedAt) : null,
|
||||
},
|
||||
});
|
||||
if (cat.id) categoryMap.set(cat.id, savedCat.id);
|
||||
@@ -909,6 +955,26 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Restore category dynamic sections if present
|
||||
if (Array.isArray(cat.sections) && cat.sections.length > 0) {
|
||||
await prisma.categorySection.deleteMany({ where: { categoryId: savedCat.id } });
|
||||
for (const sec of cat.sections) {
|
||||
if (!sec.type) continue;
|
||||
await prisma.categorySection.create({
|
||||
data: {
|
||||
categoryId: savedCat.id,
|
||||
type: sec.type,
|
||||
order: sec.order ?? 0,
|
||||
payload: sec.payload ?? {},
|
||||
isActive: sec.isActive ?? true,
|
||||
isLocked: sec.isLocked ?? false,
|
||||
},
|
||||
});
|
||||
summary.categorySections++;
|
||||
}
|
||||
}
|
||||
|
||||
summary.categories++;
|
||||
}
|
||||
|
||||
@@ -969,6 +1035,8 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
|
||||
// 3. Attribute Definitions
|
||||
const attributes = Array.isArray(payload.attributes) ? payload.attributes : [];
|
||||
const attrDefMap = new Map<number | string, number>();
|
||||
|
||||
for (const attr of attributes) {
|
||||
if (!attr.code || !attr.label) continue;
|
||||
const mappedCatId = attr.categoryId ? (categoryMap.get(attr.categoryId) || defaultCatId) : null;
|
||||
@@ -999,6 +1067,9 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
},
|
||||
});
|
||||
|
||||
if (attr.id) attrDefMap.set(attr.id, savedAttr.id);
|
||||
attrDefMap.set(attr.code, savedAttr.id);
|
||||
|
||||
if (Array.isArray(attr.translations)) {
|
||||
for (const t of attr.translations) {
|
||||
if (t.langCode && t.label) {
|
||||
@@ -1037,29 +1108,27 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
existing = await prisma.product.findFirst({ where: { name: prod.name } });
|
||||
}
|
||||
|
||||
const prodData = {
|
||||
name: prod.name,
|
||||
titleGraphicUrl: prod.titleGraphicUrl ?? null,
|
||||
description: prod.description,
|
||||
details: prod.details,
|
||||
categoryId: catId,
|
||||
brandId: bId,
|
||||
options: prod.options ?? [],
|
||||
isDeleted: Boolean(prod.isDeleted),
|
||||
deletedAt: prod.deletedAt ? new Date(prod.deletedAt) : null,
|
||||
};
|
||||
|
||||
let savedProduct;
|
||||
if (existing) {
|
||||
savedProduct = await prisma.product.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
name: prod.name,
|
||||
description: prod.description,
|
||||
details: prod.details,
|
||||
categoryId: catId,
|
||||
brandId: bId,
|
||||
options: prod.options ?? [],
|
||||
},
|
||||
data: prodData,
|
||||
});
|
||||
} else {
|
||||
savedProduct = await prisma.product.create({
|
||||
data: {
|
||||
name: prod.name,
|
||||
description: prod.description,
|
||||
details: prod.details,
|
||||
categoryId: catId,
|
||||
brandId: bId,
|
||||
options: prod.options ?? [],
|
||||
},
|
||||
data: prodData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1084,6 +1153,7 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
barcode: v.barcode,
|
||||
price: Number(v.price) || 0,
|
||||
stock: Number(v.stock) || 0,
|
||||
imageUrl: v.imageUrl || null,
|
||||
},
|
||||
create: {
|
||||
productId: savedProduct.id,
|
||||
@@ -1094,6 +1164,7 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
barcode: v.barcode,
|
||||
price: Number(v.price) || 0,
|
||||
stock: Number(v.stock) || 0,
|
||||
imageUrl: v.imageUrl || null,
|
||||
},
|
||||
});
|
||||
summary.variants++;
|
||||
@@ -1145,6 +1216,103 @@ export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
}
|
||||
}
|
||||
|
||||
// Product Attributes
|
||||
if (Array.isArray(prod.attributes) && prod.attributes.length > 0) {
|
||||
for (const attr of prod.attributes) {
|
||||
let targetAttrDefId: number | undefined = undefined;
|
||||
if (attr.attributeDefinitionId && attrDefMap.has(attr.attributeDefinitionId)) {
|
||||
targetAttrDefId = attrDefMap.get(attr.attributeDefinitionId);
|
||||
} else if (attr.attributeDefinition?.code && attrDefMap.has(attr.attributeDefinition.code)) {
|
||||
targetAttrDefId = attrDefMap.get(attr.attributeDefinition.code);
|
||||
} else if (attr.attributeDefinition?.code) {
|
||||
const found = await prisma.attributeDefinition.findUnique({
|
||||
where: { code: attr.attributeDefinition.code },
|
||||
select: { id: true },
|
||||
});
|
||||
if (found) targetAttrDefId = found.id;
|
||||
} else if (attr.attributeDefinitionId) {
|
||||
targetAttrDefId = attr.attributeDefinitionId;
|
||||
}
|
||||
|
||||
if (targetAttrDefId) {
|
||||
const savedProdAttr = await prisma.productAttribute.upsert({
|
||||
where: {
|
||||
productId_attributeDefinitionId: {
|
||||
productId: savedProduct.id,
|
||||
attributeDefinitionId: targetAttrDefId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
valueText: attr.valueText ?? null,
|
||||
valueNumber: attr.valueNumber != null ? Number(attr.valueNumber) : null,
|
||||
valueBoolean: attr.valueBoolean != null ? Boolean(attr.valueBoolean) : null,
|
||||
valueJson: attr.valueJson ?? null,
|
||||
},
|
||||
create: {
|
||||
productId: savedProduct.id,
|
||||
attributeDefinitionId: targetAttrDefId,
|
||||
valueText: attr.valueText ?? null,
|
||||
valueNumber: attr.valueNumber != null ? Number(attr.valueNumber) : null,
|
||||
valueBoolean: attr.valueBoolean != null ? Boolean(attr.valueBoolean) : null,
|
||||
valueJson: attr.valueJson ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
if (Array.isArray(attr.translations)) {
|
||||
for (const t of attr.translations) {
|
||||
if (t.langCode && t.valueText) {
|
||||
await prisma.productAttributeTranslation.upsert({
|
||||
where: {
|
||||
productAttributeId_langCode: {
|
||||
productAttributeId: savedProdAttr.id,
|
||||
langCode: t.langCode,
|
||||
},
|
||||
},
|
||||
update: { valueText: t.valueText },
|
||||
create: {
|
||||
productAttributeId: savedProdAttr.id,
|
||||
langCode: t.langCode,
|
||||
valueText: t.valueText,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
summary.productAttributes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Product Detail Sections
|
||||
if (Array.isArray(prod.detailSections) && prod.detailSections.length > 0) {
|
||||
await prisma.productDetailSection.deleteMany({ where: { productId: savedProduct.id } });
|
||||
for (const sec of prod.detailSections) {
|
||||
if (!sec.type) continue;
|
||||
await prisma.productDetailSection.create({
|
||||
data: {
|
||||
productId: savedProduct.id,
|
||||
type: sec.type,
|
||||
order: sec.order ?? 0,
|
||||
payload: sec.payload ?? {},
|
||||
isActive: sec.isActive ?? true,
|
||||
isLocked: sec.isLocked ?? false,
|
||||
},
|
||||
});
|
||||
summary.detailSections++;
|
||||
}
|
||||
} else {
|
||||
// If no detail sections provided, check if product has existing ones. If not, auto-seed default sections.
|
||||
const existingSectionCount = await prisma.productDetailSection.count({
|
||||
where: { productId: savedProduct.id },
|
||||
});
|
||||
if (existingSectionCount === 0) {
|
||||
await prisma.productDetailSection.createMany({
|
||||
data: defaultProductDetailSections(savedProduct.id),
|
||||
});
|
||||
summary.detailSections += 5;
|
||||
}
|
||||
}
|
||||
|
||||
summary.products++;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user