x
This commit is contained in:
1 parent
2ac5809895
commit
3afe32db7d
10 files changed
+1782
-1
No files matched your search
@@ -28,6 +28,7 @@ import { newsletterRoute } from "./routes/newsletter";
|
||||
import { mediaRoute } from "./routes/media";
|
||||
import { contactsRoute } from "./routes/contacts";
|
||||
import { productAttributesRoute } from "./routes/product-attributes";
|
||||
import { backupRoute } from "./routes/backup";
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand";
|
||||
@@ -135,7 +136,7 @@ const app = new Elysia()
|
||||
return file;
|
||||
})
|
||||
.group("/api", (app) =>
|
||||
app.use(authRoute).use(categoriesRoute).use(productsRoute).use(productAttributesRoute).use(uploadRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute).use(contactsRoute)
|
||||
app.use(authRoute).use(categoriesRoute).use(productsRoute).use(productAttributesRoute).use(uploadRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute).use(contactsRoute).use(backupRoute)
|
||||
)
|
||||
.listen({ port: PORT, hostname: "0.0.0.0" });
|
||||
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
import { Elysia } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
import { adminOnly } from "../middleware/auth";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
// ─── Helper: stream JSON as file download ────────────────────────────────────
|
||||
|
||||
function jsonResponse(data: unknown, filename: string) {
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const bytes = Buffer.from(json, "utf-8");
|
||||
return new Response(bytes, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Content-Length": String(bytes.length),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function csvResponse(rows: Record<string, unknown>[], filename: string) {
|
||||
if (!rows || rows.length === 0) {
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Content-Type": "text/csv",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
const firstRow = rows[0];
|
||||
if (!firstRow) {
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Content-Type": "text/csv",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
const headers = Object.keys(firstRow);
|
||||
const csvLines = [
|
||||
headers.join(","),
|
||||
...rows.map((row) =>
|
||||
headers
|
||||
.map((h) => {
|
||||
const val = (row as any)[h];
|
||||
if (val === null || val === undefined) return "";
|
||||
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
})
|
||||
.join(",")
|
||||
),
|
||||
];
|
||||
const csv = csvLines.join("\r\n");
|
||||
const bytes = Buffer.from(csv, "utf-8");
|
||||
return new Response(bytes, {
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Content-Length": String(bytes.length),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// ─── Route ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const backupRoute = new Elysia({ prefix: "/backup" })
|
||||
.use(adminOnly)
|
||||
|
||||
// ── 1. Catalog Export ──────────────────────────────────────────────────────
|
||||
/**
|
||||
* GET /api/backup/catalog?section=all|products|categories|attributes|brands&format=json|csv
|
||||
* Exports product catalog data as JSON or CSV download.
|
||||
*/
|
||||
.get("/catalog", async ({ query, set }: { query: any; set: any }) => {
|
||||
try {
|
||||
const section = (query.section as string) || "all";
|
||||
const format = (query.format as string) === "csv" ? "csv" : "json";
|
||||
const dt = today();
|
||||
|
||||
if (section === "products" || section === "all") {
|
||||
const products = await prisma.product.findMany({
|
||||
include: {
|
||||
variants: true,
|
||||
images: true,
|
||||
translations: true,
|
||||
attributes: { include: { attributeDefinition: true, translations: true } },
|
||||
brand: { include: { translations: true } },
|
||||
category: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
|
||||
if (section === "products") {
|
||||
if (format === "csv") {
|
||||
const rows = products.map((p: any) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
categoryId: p.categoryId,
|
||||
brandId: p.brandId,
|
||||
variantCount: p.variants.length,
|
||||
imageCount: p.images.length,
|
||||
createdAt: p.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_products_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: products.length, data: products }, `homaroma_products_${dt}.json`);
|
||||
}
|
||||
|
||||
// full catalog
|
||||
const categories = await prisma.category.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: { translations: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const attributes = await prisma.attributeDefinition.findMany({
|
||||
include: { translations: true },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
const brands = await prisma.productBrand.findMany({
|
||||
include: { translations: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
|
||||
if (format === "csv") {
|
||||
// For CSV full export, flatten products only (complex nested data → JSON)
|
||||
const rows = products.map((p: any) => ({
|
||||
id: p.id,
|
||||
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,
|
||||
createdAt: p.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_catalog_${dt}.csv`);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
exportedAt: new Date().toISOString(),
|
||||
summary: {
|
||||
products: products.length,
|
||||
categories: categories.length,
|
||||
attributes: attributes.length,
|
||||
brands: brands.length,
|
||||
},
|
||||
data: { products, categories, attributes, brands },
|
||||
},
|
||||
`homaroma_catalog_all_${dt}.json`
|
||||
);
|
||||
}
|
||||
|
||||
if (section === "categories") {
|
||||
const categories = await prisma.category.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: { translations: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
if (format === "csv") {
|
||||
const rows = categories.map((c: any) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
imageUrl: c.imageUrl,
|
||||
createdAt: c.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_categories_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: categories.length, data: categories }, `homaroma_categories_${dt}.json`);
|
||||
}
|
||||
|
||||
if (section === "attributes") {
|
||||
const attributes = await prisma.attributeDefinition.findMany({
|
||||
include: { translations: true },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
if (format === "csv") {
|
||||
const rows = attributes.map((a: any) => ({
|
||||
id: a.id,
|
||||
code: a.code,
|
||||
label: a.label,
|
||||
type: a.type,
|
||||
unit: a.unit,
|
||||
isRequired: a.isRequired,
|
||||
isFilterable: a.isFilterable,
|
||||
sortOrder: a.sortOrder,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_attributes_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: attributes.length, data: attributes }, `homaroma_attributes_${dt}.json`);
|
||||
}
|
||||
|
||||
if (section === "brands") {
|
||||
const brands = await prisma.productBrand.findMany({
|
||||
include: { translations: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
if (format === "csv") {
|
||||
const rows = brands.map((b: any) => ({ id: b.id, name: b.name, createdAt: b.createdAt }));
|
||||
return csvResponse(rows, `homaroma_brands_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: brands.length, data: brands }, `homaroma_brands_${dt}.json`);
|
||||
}
|
||||
|
||||
set.status = 400;
|
||||
return { error: `Unknown section: ${section}. Valid values: all, products, categories, attributes, brands` };
|
||||
} catch (err: any) {
|
||||
console.error("[Backup] Catalog export error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Catalog export failed", detail: err?.message };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 2. Orders & Customers Export ──────────────────────────────────────────
|
||||
/**
|
||||
* GET /api/backup/orders?section=all|orders|customers|coupons|promotions&format=json|csv&from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||
*/
|
||||
.get("/orders", async ({ query, set }: { query: any; set: any }) => {
|
||||
try {
|
||||
const section = (query.section as string) || "all";
|
||||
const format = (query.format as string) === "csv" ? "csv" : "json";
|
||||
const from = query.from as string | undefined;
|
||||
const to = query.to as string | undefined;
|
||||
const dt = today();
|
||||
|
||||
const dateFilter =
|
||||
from || to
|
||||
? {
|
||||
createdAt: {
|
||||
...(from ? { gte: new Date(`${from}T00:00:00.000Z`) } : {}),
|
||||
...(to ? { lte: new Date(`${to}T23:59:59.999Z`) } : {}),
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
if (section === "orders" || section === "all") {
|
||||
const orders = await prisma.order.findMany({
|
||||
where: dateFilter,
|
||||
include: { items: true, customer: { select: { id: true, displayName: true, lineUserId: true } } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
if (section === "orders") {
|
||||
if (format === "csv") {
|
||||
const rows = orders.map((o: any) => ({
|
||||
id: o.id,
|
||||
customerName: o.customerName,
|
||||
customerEmail: o.customerEmail,
|
||||
customerPhone: o.customerPhone,
|
||||
totalAmount: o.totalAmount,
|
||||
status: o.status,
|
||||
trackingNo: o.trackingNo,
|
||||
createdAt: o.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_orders_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: orders.length, data: orders }, `homaroma_orders_${dt}.json`);
|
||||
}
|
||||
|
||||
// full
|
||||
const customers = await prisma.customer.findMany({
|
||||
include: { addresses: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const coupons = await prisma.coupon.findMany({ orderBy: { id: "asc" } });
|
||||
const promotions = await prisma.promotion.findMany({ orderBy: { level: "asc" } });
|
||||
|
||||
if (format === "csv") {
|
||||
const rows = orders.map((o: any) => ({
|
||||
id: o.id,
|
||||
customerName: o.customerName,
|
||||
customerEmail: o.customerEmail,
|
||||
totalAmount: o.totalAmount,
|
||||
status: o.status,
|
||||
createdAt: o.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_orders_all_${dt}.csv`);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
exportedAt: new Date().toISOString(),
|
||||
dateRange: from || to ? { from: from ?? "all", to: to ?? "all" } : "all",
|
||||
summary: {
|
||||
orders: orders.length,
|
||||
customers: customers.length,
|
||||
coupons: coupons.length,
|
||||
promotions: promotions.length,
|
||||
},
|
||||
data: { orders, customers, coupons, promotions },
|
||||
},
|
||||
`homaroma_orders_full_${dt}.json`
|
||||
);
|
||||
}
|
||||
|
||||
if (section === "customers") {
|
||||
const customers = await prisma.customer.findMany({
|
||||
include: { addresses: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
if (format === "csv") {
|
||||
const rows = customers.map((c: any) => ({
|
||||
id: c.id,
|
||||
displayName: c.displayName,
|
||||
email: c.email,
|
||||
phone: c.phone,
|
||||
points: c.points,
|
||||
createdAt: c.createdAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_customers_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: customers.length, data: customers }, `homaroma_customers_${dt}.json`);
|
||||
}
|
||||
|
||||
if (section === "coupons") {
|
||||
const coupons = await prisma.coupon.findMany({ orderBy: { id: "asc" } });
|
||||
if (format === "csv") {
|
||||
const rows = coupons.map((c: any) => ({
|
||||
id: c.id,
|
||||
code: c.code,
|
||||
type: c.type,
|
||||
value: c.value,
|
||||
usageLimit: c.usageLimit,
|
||||
usedCount: c.usedCount,
|
||||
isActive: c.isActive,
|
||||
expiresAt: c.expiresAt,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_coupons_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: coupons.length, data: coupons }, `homaroma_coupons_${dt}.json`);
|
||||
}
|
||||
|
||||
if (section === "promotions") {
|
||||
const promotions = await prisma.promotion.findMany({ orderBy: { level: "asc" } });
|
||||
if (format === "csv") {
|
||||
const rows = promotions.map((p: any) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
level: p.level,
|
||||
type: p.type,
|
||||
value: p.value,
|
||||
minPurchase: p.minPurchase,
|
||||
isActive: p.isActive,
|
||||
}));
|
||||
return csvResponse(rows, `homaroma_promotions_${dt}.csv`);
|
||||
}
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: promotions.length, data: promotions }, `homaroma_promotions_${dt}.json`);
|
||||
}
|
||||
|
||||
set.status = 400;
|
||||
return { error: `Unknown section: ${section}. Valid values: all, orders, customers, coupons, promotions` };
|
||||
} catch (err: any) {
|
||||
console.error("[Backup] Orders export error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Orders export failed", detail: err?.message };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 3. CMS & Content Export ────────────────────────────────────────────────
|
||||
/**
|
||||
* GET /api/backup/cms?section=all|pages|menus|articles|settings
|
||||
* Always returns JSON (CMS data is structured — CSV not useful here).
|
||||
*/
|
||||
.get("/cms", async ({ query, set }: { query: any; set: any }) => {
|
||||
try {
|
||||
const section = (query.section as string) || "all";
|
||||
const dt = today();
|
||||
|
||||
if (section === "pages" || section === "all") {
|
||||
const pages = await prisma.page.findMany({
|
||||
include: { sections: true, translations: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
|
||||
if (section === "pages") {
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: pages.length, data: pages }, `homaroma_cms_pages_${dt}.json`);
|
||||
}
|
||||
|
||||
// fetch rest for full export
|
||||
const menus = await prisma.menu.findMany({
|
||||
include: { items: { include: { translations: true } } },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const articles = await prisma.article.findMany({
|
||||
include: { translations: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
const settings = await prisma.storeSettings.findFirst({
|
||||
include: { translations: true },
|
||||
});
|
||||
const footerMenus = await prisma.footerMenu.findMany({
|
||||
include: { items: true },
|
||||
orderBy: { id: "asc" },
|
||||
}).catch(() => []); // graceful if table not present
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
exportedAt: new Date().toISOString(),
|
||||
summary: {
|
||||
pages: pages.length,
|
||||
menus: menus.length,
|
||||
articles: articles.length,
|
||||
hasSettings: !!settings,
|
||||
},
|
||||
data: { pages, menus, footerMenus, articles, settings },
|
||||
},
|
||||
`homaroma_cms_all_${dt}.json`
|
||||
);
|
||||
}
|
||||
|
||||
if (section === "menus") {
|
||||
const menus = await prisma.menu.findMany({
|
||||
include: { items: { include: { translations: true } } },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const footerMenus = await prisma.footerMenu.findMany({
|
||||
include: { items: true },
|
||||
orderBy: { id: "asc" },
|
||||
}).catch(() => []);
|
||||
return jsonResponse(
|
||||
{ exportedAt: new Date().toISOString(), data: { menus, footerMenus } },
|
||||
`homaroma_cms_menus_${dt}.json`
|
||||
);
|
||||
}
|
||||
|
||||
if (section === "articles") {
|
||||
const articles = await prisma.article.findMany({
|
||||
include: { translations: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), count: articles.length, data: articles }, `homaroma_cms_articles_${dt}.json`);
|
||||
}
|
||||
|
||||
if (section === "settings") {
|
||||
const settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
|
||||
return jsonResponse({ exportedAt: new Date().toISOString(), data: settings }, `homaroma_cms_settings_${dt}.json`);
|
||||
}
|
||||
|
||||
set.status = 400;
|
||||
return { error: `Unknown section: ${section}. Valid values: all, pages, menus, articles, settings` };
|
||||
} catch (err: any) {
|
||||
console.error("[Backup] CMS export error:", err);
|
||||
set.status = 500;
|
||||
return { error: "CMS export failed", detail: err?.message };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 4. Media Files — list ──────────────────────────────────────────────────
|
||||
/**
|
||||
* GET /api/backup/media
|
||||
* Returns JSON list of all media file records + filesystem info.
|
||||
*/
|
||||
.get("/media", async ({ set }: { set: any }) => {
|
||||
try {
|
||||
const media = await prisma.media.findMany({ orderBy: { createdAt: "desc" } });
|
||||
const uploadsDir = path.resolve("public/uploads");
|
||||
|
||||
let fsFiles: string[] = [];
|
||||
try {
|
||||
fsFiles = fs.readdirSync(uploadsDir);
|
||||
} catch {
|
||||
// uploads dir may not exist yet
|
||||
}
|
||||
|
||||
return {
|
||||
exportedAt: new Date().toISOString(),
|
||||
dbRecords: media.length,
|
||||
fsFiles: fsFiles.length,
|
||||
uploadsPath: uploadsDir,
|
||||
media,
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error("[Backup] Media list error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Media list failed", detail: err?.message };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 5. Media Files — zip download ─────────────────────────────────────────
|
||||
/**
|
||||
* GET /api/backup/media/zip
|
||||
* Zips the public/uploads/ directory and streams the .zip file.
|
||||
* Uses Bun's native spawn to call system zip command.
|
||||
*/
|
||||
.get("/media/zip", async ({ set }: { set: any }) => {
|
||||
try {
|
||||
const uploadsDir = path.resolve("public/uploads");
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
set.status = 404;
|
||||
return { error: "uploads directory not found", path: uploadsDir };
|
||||
}
|
||||
|
||||
const dt = today();
|
||||
const tmpZip = path.join("/tmp", `homaroma_media_${dt}_${Date.now()}.zip`);
|
||||
|
||||
// Use Bun.spawn to call system zip
|
||||
const proc = Bun.spawn(["zip", "-r", tmpZip, "."], {
|
||||
cwd: uploadsDir,
|
||||
stderr: "pipe",
|
||||
});
|
||||
await proc.exited;
|
||||
|
||||
if (!fs.existsSync(tmpZip)) {
|
||||
set.status = 500;
|
||||
return { error: "zip creation failed — ensure `zip` is installed on the server" };
|
||||
}
|
||||
|
||||
const fileData = await Bun.file(tmpZip).arrayBuffer();
|
||||
|
||||
// Clean up temp file
|
||||
try { fs.unlinkSync(tmpZip); } catch { /* ignore */ }
|
||||
|
||||
return new Response(fileData, {
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="homaroma_media_${dt}.zip"`,
|
||||
"Content-Length": String(fileData.byteLength),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[Backup] Media zip error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Media zip failed", detail: err?.message };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 6. Database info ──────────────────────────────────────────────────────
|
||||
/**
|
||||
* GET /api/backup/database
|
||||
* Returns table row counts for dashboard display.
|
||||
* Full pg_dump requires shell access — provide instructions.
|
||||
*/
|
||||
.get("/database", async ({ set }: { set: any }) => {
|
||||
try {
|
||||
const [
|
||||
products, categories, customers, orders,
|
||||
articles, pages, menus, coupons, promotions,
|
||||
media, subscribers, rawMaterials, tiktokOrders, shopeeOrders, lazadaOrders,
|
||||
] = await Promise.all([
|
||||
prisma.product.count(),
|
||||
prisma.category.count({ where: { deletedAt: null } }),
|
||||
prisma.customer.count(),
|
||||
prisma.order.count(),
|
||||
prisma.article.count(),
|
||||
prisma.page.count(),
|
||||
prisma.menu.count(),
|
||||
prisma.coupon.count(),
|
||||
prisma.promotion.count(),
|
||||
prisma.media.count(),
|
||||
prisma.newsletterSubscription.count().catch(() => 0),
|
||||
prisma.rawMaterial.count(),
|
||||
prisma.tiktokOrder.count(),
|
||||
prisma.shopeeOrder.count(),
|
||||
prisma.lazadaOrder.count(),
|
||||
]);
|
||||
|
||||
return {
|
||||
scannedAt: new Date().toISOString(),
|
||||
tableCounts: {
|
||||
products,
|
||||
categories,
|
||||
customers,
|
||||
orders,
|
||||
articles,
|
||||
pages,
|
||||
menus,
|
||||
coupons,
|
||||
promotions,
|
||||
media,
|
||||
subscribers,
|
||||
rawMaterials,
|
||||
tiktokOrders,
|
||||
shopeeOrders,
|
||||
lazadaOrders,
|
||||
},
|
||||
pgDumpNote: "Full pg_dump requires shell access. Use SSH: pg_dump -U postgres <dbname> > backup.sql",
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error("[Backup] Database info error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Database info failed", detail: err?.message };
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user