139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
import { Router, Request, Response } from "express";
|
|
import { pool } from "../db/client";
|
|
import { listVendors } from "../db/queries";
|
|
|
|
export const vendorRouter = Router();
|
|
|
|
// GET /api/vendors — List all vendors
|
|
vendorRouter.get("/", async (req: Request, res: Response) => {
|
|
try {
|
|
const vendors = await listVendors(req.query.type ? String(req.query.type) : undefined);
|
|
res.json({ success: true, data: vendors, total: vendors.length });
|
|
} catch (err) {
|
|
console.error("List vendors error:", err);
|
|
res.status(500).json({ success: false, error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// POST /api/vendors — Create a new vendor + queue auto-crawl
|
|
vendorRouter.post("/", async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
name,
|
|
type,
|
|
website,
|
|
shop_url,
|
|
headquarters,
|
|
country,
|
|
founded_year,
|
|
revenue_usd,
|
|
employee_count,
|
|
market_position,
|
|
specialties,
|
|
is_competitor,
|
|
} = req.body as Record<string, any>;
|
|
|
|
if (!name || typeof name !== "string" || !name.trim()) {
|
|
return res.status(400).json({ success: false, error: "name is required" });
|
|
}
|
|
|
|
const validTypes = ["manufacturer", "distributor", "oem", "reseller", "compatible"];
|
|
const resolvedType = validTypes.includes(type) ? type : "compatible";
|
|
|
|
// Generate slug from name
|
|
const slug = name
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "");
|
|
|
|
// Build specialties array
|
|
const specialtiesArr: string[] = Array.isArray(specialties)
|
|
? specialties
|
|
: typeof specialties === "string" && specialties.trim()
|
|
? specialties.split(",").map((s: string) => s.trim()).filter(Boolean)
|
|
: [];
|
|
|
|
// Insert vendor
|
|
const insertResult = await pool.query(
|
|
`INSERT INTO vendors
|
|
(name, slug, type, website, shop_url, headquarters, country,
|
|
founded_year, revenue_usd, employee_count, market_position, specialties,
|
|
is_competitor, scrape_config)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
RETURNING *`,
|
|
[
|
|
name.trim(),
|
|
slug,
|
|
resolvedType,
|
|
website || null,
|
|
shop_url || website || null,
|
|
headquarters || null,
|
|
country || null,
|
|
founded_year ? Number(founded_year) : null,
|
|
revenue_usd ? Number(revenue_usd) : null,
|
|
employee_count ? Number(employee_count) : null,
|
|
market_position || null,
|
|
specialtiesArr,
|
|
is_competitor === true || is_competitor === "true",
|
|
website ? JSON.stringify({ url: website, enabled: true, auto_queued: true }) : "{}",
|
|
]
|
|
);
|
|
|
|
const vendor = insertResult.rows[0];
|
|
|
|
// Queue crawl job: insert into crawler_llm_log as a pending task signal
|
|
// The scraper fleet polls for new vendors with scrape_config.enabled=true
|
|
if (website) {
|
|
await pool
|
|
.query(
|
|
`INSERT INTO crawler_llm_log
|
|
(vendor_id, url, action, model, tokens_used, created_at)
|
|
VALUES ($1, $2, 'vendor_created_auto_crawl', 'system', 0, NOW())`,
|
|
[vendor.id, website]
|
|
)
|
|
.catch(() => null); // Non-fatal — vendor is created regardless
|
|
}
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
vendor,
|
|
crawl_queued: !!website,
|
|
message: website
|
|
? `Vendor "${name}" created. Auto-crawl queued for ${website}.`
|
|
: `Vendor "${name}" created. Add a website URL to enable auto-crawl.`,
|
|
});
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
// Unique constraint violation (name or slug)
|
|
return res.status(409).json({ success: false, error: "A vendor with this name already exists." });
|
|
}
|
|
console.error("Create vendor error:", err);
|
|
return res.status(500).json({ success: false, error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// GET /api/vendors/:id — Get single vendor with full stats
|
|
vendorRouter.get("/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const result = await pool.query(
|
|
`SELECT v.*,
|
|
(SELECT COUNT(*) FROM transceivers t WHERE t.vendor_id = v.id)::int AS transceiver_count,
|
|
(SELECT COUNT(*) FROM switches s WHERE s.vendor_id = v.id)::int AS switch_count,
|
|
(SELECT COUNT(*) FROM price_observations po
|
|
JOIN transceivers t ON po.transceiver_id = t.id
|
|
WHERE t.vendor_id = v.id)::int AS price_obs_count
|
|
FROM vendors v
|
|
WHERE v.id::text = $1 OR v.slug = $1`,
|
|
[id]
|
|
);
|
|
const vendor = result.rows[0];
|
|
if (!vendor) return res.status(404).json({ success: false, error: "Vendor not found" });
|
|
return res.json({ success: true, vendor });
|
|
} catch (err) {
|
|
console.error("Get vendor error:", err);
|
|
return res.status(500).json({ success: false, error: "Internal server error" });
|
|
}
|
|
});
|