220 lines
6.7 KiB
TypeScript
220 lines
6.7 KiB
TypeScript
import { Router, type IRouter } from "express";
|
|
import { runAllIntegrationTests, testRapidApi, testSerpApi, testCloudinary, testApify, testDatabase } from "../lib/integration-tests";
|
|
import { validateServicesConfig } from "../config/services";
|
|
import { fetchSheinCategories, SHEIN_CATEGORIES_PRESET, type SheinCategory } from "../lib/shein-scraper";
|
|
import { db, categoriesTable } from "@workspace/db";
|
|
import { eq, sql } from "drizzle-orm";
|
|
import { requireAdmin } from "../middleware/auth";
|
|
|
|
const router: IRouter = Router();
|
|
|
|
router.get("/integrations/status", async (_req, res) => {
|
|
try {
|
|
const configCheck = validateServicesConfig();
|
|
const results = await runAllIntegrationTests();
|
|
const allConnected = results.every(r => r.connected);
|
|
res.json({
|
|
ready: allConnected,
|
|
config: configCheck,
|
|
services: results,
|
|
summary: results.map(r => `${r.connected ? "✅" : "❌"} ${r.service}: ${r.message}`),
|
|
});
|
|
} catch (err: unknown) {
|
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
res.status(500).json({ error: errMsg });
|
|
}
|
|
});
|
|
|
|
router.get("/integrations/rapidapi", async (_req, res) => {
|
|
res.json(await testRapidApi());
|
|
});
|
|
|
|
router.get("/integrations/serpapi", async (_req, res) => {
|
|
res.json(await testSerpApi());
|
|
});
|
|
|
|
router.get("/integrations/cloudinary", async (_req, res) => {
|
|
res.json(await testCloudinary());
|
|
});
|
|
|
|
router.get("/integrations/apify", async (_req, res) => {
|
|
res.json(await testApify());
|
|
});
|
|
|
|
router.get("/integrations/database", async (_req, res) => {
|
|
res.json(await testDatabase());
|
|
});
|
|
|
|
async function saveSheinCategoriesToDb(categories: SheinCategory[]): Promise<{ saved: number; errors: number }> {
|
|
let saved = 0;
|
|
let errors = 0;
|
|
|
|
const mainCats = categories.filter(c => c.level === 1);
|
|
const subCats = categories.filter(c => c.level === 2);
|
|
|
|
const parentIdMap = new Map<string, number>();
|
|
|
|
for (const cat of mainCats) {
|
|
try {
|
|
const existing = await db
|
|
.select({ id: categoriesTable.id })
|
|
.from(categoriesTable)
|
|
.where(eq(categoriesTable.slug, cat.slug))
|
|
.limit(1);
|
|
|
|
if (existing.length > 0) {
|
|
await db.update(categoriesTable).set({
|
|
name: cat.name_ar,
|
|
name_en: cat.name_en,
|
|
icon: cat.icon ?? null,
|
|
sort_order: cat.sort_order,
|
|
source: "shein",
|
|
shein_cat_id: cat.shein_cat_id,
|
|
shein_url: cat.shein_url,
|
|
slug: cat.slug,
|
|
parent_id: null,
|
|
}).where(eq(categoriesTable.id, existing[0]!.id));
|
|
parentIdMap.set(cat.slug, existing[0]!.id);
|
|
saved++;
|
|
} else {
|
|
const [inserted] = await db.insert(categoriesTable).values({
|
|
name: cat.name_ar,
|
|
name_en: cat.name_en,
|
|
slug: cat.slug,
|
|
icon: cat.icon ?? null,
|
|
sort_order: cat.sort_order,
|
|
parent_id: null,
|
|
source: "shein",
|
|
shein_cat_id: cat.shein_cat_id,
|
|
shein_url: cat.shein_url,
|
|
}).returning({ id: categoriesTable.id });
|
|
if (inserted) {
|
|
parentIdMap.set(cat.slug, inserted.id);
|
|
saved++;
|
|
}
|
|
}
|
|
} catch {
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
for (const cat of subCats) {
|
|
try {
|
|
const parentId = cat.parent_slug ? parentIdMap.get(cat.parent_slug) : undefined;
|
|
|
|
const existing = await db
|
|
.select({ id: categoriesTable.id })
|
|
.from(categoriesTable)
|
|
.where(eq(categoriesTable.slug, cat.slug))
|
|
.limit(1);
|
|
|
|
if (existing.length > 0) {
|
|
await db.update(categoriesTable).set({
|
|
name: cat.name_ar,
|
|
name_en: cat.name_en,
|
|
slug: cat.slug,
|
|
sort_order: cat.sort_order,
|
|
parent_id: parentId ?? null,
|
|
source: "shein",
|
|
shein_cat_id: cat.shein_cat_id,
|
|
shein_url: cat.shein_url,
|
|
}).where(eq(categoriesTable.id, existing[0]!.id));
|
|
saved++;
|
|
} else {
|
|
await db.insert(categoriesTable).values({
|
|
name: cat.name_ar,
|
|
name_en: cat.name_en,
|
|
slug: cat.slug,
|
|
sort_order: cat.sort_order,
|
|
parent_id: parentId ?? null,
|
|
source: "shein",
|
|
shein_cat_id: cat.shein_cat_id,
|
|
shein_url: cat.shein_url,
|
|
});
|
|
saved++;
|
|
}
|
|
} catch {
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
return { saved, errors };
|
|
}
|
|
|
|
router.get("/integrations/shein-categories", async (req, res) => {
|
|
const mode = (req.query["mode"] as string) ?? "preset";
|
|
|
|
try {
|
|
let categories: SheinCategory[] = [];
|
|
let source = "preset";
|
|
let scrapeResult: Awaited<ReturnType<typeof fetchSheinCategories>> | null = null;
|
|
|
|
if (mode === "scrape") {
|
|
scrapeResult = await fetchSheinCategories();
|
|
if (scrapeResult.success && scrapeResult.categories && scrapeResult.categories.length > 0) {
|
|
categories = scrapeResult.categories;
|
|
source = "apify-scrape";
|
|
} else {
|
|
categories = SHEIN_CATEGORIES_PRESET;
|
|
source = "preset-fallback";
|
|
}
|
|
} else {
|
|
categories = SHEIN_CATEGORIES_PRESET;
|
|
}
|
|
|
|
const dbTotal = await db
|
|
.select({ count: sql<number>`count(*)::int` })
|
|
.from(categoriesTable)
|
|
.where(eq(categoriesTable.source, "shein"));
|
|
|
|
const sections = categories.filter(c => c.level === 1);
|
|
const subcats = categories.filter(c => c.level === 2);
|
|
|
|
res.json({
|
|
success: true,
|
|
source,
|
|
totalCategories: categories.length,
|
|
sections: sections.length,
|
|
subcategories: subcats.length,
|
|
dbTotal: dbTotal[0]?.count ?? 0,
|
|
scrapeResult: scrapeResult ? {
|
|
success: scrapeResult.success,
|
|
error: scrapeResult.error,
|
|
runId: scrapeResult.runId,
|
|
} : null,
|
|
data: {
|
|
sections: sections.map(s => ({
|
|
...s,
|
|
subcategories: subcats.filter(sc => sc.parent_slug === s.slug),
|
|
})),
|
|
},
|
|
});
|
|
} catch (err: unknown) {
|
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
res.status(500).json({ error: errMsg });
|
|
}
|
|
});
|
|
|
|
router.post("/integrations/shein-categories/save", requireAdmin, async (_req, res) => {
|
|
try {
|
|
const dbResult = await saveSheinCategoriesToDb(SHEIN_CATEGORIES_PRESET);
|
|
const total = await db
|
|
.select({ count: sql<number>`count(*)::int` })
|
|
.from(categoriesTable)
|
|
.where(eq(categoriesTable.source, "shein"));
|
|
|
|
res.json({
|
|
success: true,
|
|
saved: dbResult.saved,
|
|
errors: dbResult.errors,
|
|
totalInDb: total[0]?.count ?? 0,
|
|
message: `تم حفظ ${dbResult.saved} فئة من متجر Shein في قاعدة البيانات`,
|
|
});
|
|
} catch (err: unknown) {
|
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
res.status(500).json({ error: errMsg });
|
|
}
|
|
});
|
|
|
|
export default router;
|