feat: add Stalker products functionality with filtering, pagination, and purge option
This commit is contained in:
162
src/server.ts
162
src/server.ts
@@ -71,6 +71,20 @@ type StalkerResultRecord = {
|
||||
inventory_sample_asins: string | null;
|
||||
};
|
||||
|
||||
type StalkerProductRecord = {
|
||||
runId: number;
|
||||
started_at: string;
|
||||
seller_id: string;
|
||||
seller_name: string | null;
|
||||
rating: number | null;
|
||||
rating_count: number | null;
|
||||
asin: string;
|
||||
can_sell: number;
|
||||
sellability_status: string;
|
||||
sellability_reason: string | null;
|
||||
last_seen_at: string;
|
||||
};
|
||||
|
||||
const DB_PATH = process.env.RESULTS_DB_PATH || path.join("db", "results.db");
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const MAX_PAGE_SIZE = 200;
|
||||
@@ -799,6 +813,143 @@ function getStalkerResults(filters: URLSearchParams) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseStalkerProductFilters(filters: URLSearchParams) {
|
||||
const q = filters.get("q")?.trim() || "";
|
||||
const sellerId = filters.get("sellerId")?.trim().toUpperCase() || "";
|
||||
const runIdRaw = filters.get("runId")?.trim() || "";
|
||||
|
||||
const conditions = [
|
||||
"inv.can_sell = 1",
|
||||
"inv.sellability_status = 'available'",
|
||||
];
|
||||
const params: Array<string | number> = [];
|
||||
|
||||
if (runIdRaw) {
|
||||
const runId = Number(runIdRaw);
|
||||
if (Number.isInteger(runId) && runId > 0) {
|
||||
conditions.push("r.id = ?");
|
||||
params.push(runId);
|
||||
}
|
||||
}
|
||||
|
||||
if (sellerId) {
|
||||
conditions.push("s.seller_id = ?");
|
||||
params.push(sellerId);
|
||||
}
|
||||
|
||||
if (q) {
|
||||
const wildcard = `%${q}%`;
|
||||
conditions.push(
|
||||
"(inv.asin LIKE ? OR s.seller_id LIKE ? OR s.seller_name LIKE ?)",
|
||||
);
|
||||
params.push(wildcard, wildcard, wildcard);
|
||||
}
|
||||
|
||||
return {
|
||||
where: `WHERE ${conditions.join(" AND ")}`,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
function parseStalkerProductSort(sortParam: string | null): string {
|
||||
const allowedSort = new Set([
|
||||
"runId",
|
||||
"started_at",
|
||||
"seller_id",
|
||||
"seller_name",
|
||||
"rating",
|
||||
"rating_count",
|
||||
"asin",
|
||||
"last_seen_at",
|
||||
]);
|
||||
return parseSort(sortParam, allowedSort, "last_seen_at DESC, asin ASC");
|
||||
}
|
||||
|
||||
function getStalkerProducts(filters: URLSearchParams) {
|
||||
const page = parseIntParam(filters.get("page"), 1);
|
||||
const pageSize = Math.min(
|
||||
parseIntParam(filters.get("pageSize"), DEFAULT_PAGE_SIZE),
|
||||
MAX_PAGE_SIZE,
|
||||
);
|
||||
const offset = (page - 1) * pageSize;
|
||||
const { where, params } = parseStalkerProductFilters(filters);
|
||||
const orderBy = parseStalkerProductSort(filters.get("sort"));
|
||||
|
||||
const baseSelect = `
|
||||
SELECT
|
||||
r.id AS runId,
|
||||
r.started_at,
|
||||
s.seller_id,
|
||||
s.seller_name,
|
||||
s.rating,
|
||||
s.rating_count,
|
||||
inv.asin,
|
||||
inv.can_sell,
|
||||
inv.sellability_status,
|
||||
inv.sellability_reason,
|
||||
inv.last_seen_at
|
||||
FROM stalker_seller_inventory inv
|
||||
JOIN stalker_runs r ON r.id = inv.run_id
|
||||
JOIN stalker_sellers s ON s.seller_id = inv.seller_id
|
||||
${where}
|
||||
`;
|
||||
|
||||
const totalRow = db
|
||||
.query(`SELECT COUNT(*) AS total FROM (${baseSelect}) stalker_products`)
|
||||
.get(...params) as { total: number };
|
||||
|
||||
const summary = db
|
||||
.query(
|
||||
`SELECT
|
||||
COUNT(DISTINCT runId) AS runs,
|
||||
COUNT(DISTINCT seller_id) AS sellers,
|
||||
COUNT(DISTINCT asin) AS products
|
||||
FROM (${baseSelect}) stalker_products`,
|
||||
)
|
||||
.get(...params) as {
|
||||
runs: number;
|
||||
sellers: number;
|
||||
products: number;
|
||||
};
|
||||
|
||||
const items = db
|
||||
.query(
|
||||
`SELECT * FROM (${baseSelect}) stalker_products
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT ? OFFSET ?`,
|
||||
)
|
||||
.all(...params, pageSize, offset) as StalkerProductRecord[];
|
||||
|
||||
return {
|
||||
items,
|
||||
summary,
|
||||
page,
|
||||
pageSize,
|
||||
total: totalRow.total,
|
||||
totalPages: Math.max(1, Math.ceil(totalRow.total / pageSize)),
|
||||
};
|
||||
}
|
||||
|
||||
function purgeStalkerData() {
|
||||
const counts = {
|
||||
inventory: (db.query("SELECT COUNT(*) AS count FROM stalker_seller_inventory").get() as { count: number }).count,
|
||||
asinSellers: (db.query("SELECT COUNT(*) AS count FROM stalker_asin_sellers").get() as { count: number }).count,
|
||||
sellers: (db.query("SELECT COUNT(*) AS count FROM stalker_sellers").get() as { count: number }).count,
|
||||
scans: (db.query("SELECT COUNT(*) AS count FROM stalker_asin_scans").get() as { count: number }).count,
|
||||
runs: (db.query("SELECT COUNT(*) AS count FROM stalker_runs").get() as { count: number }).count,
|
||||
};
|
||||
|
||||
db.transaction(() => {
|
||||
db.run("DELETE FROM stalker_seller_inventory");
|
||||
db.run("DELETE FROM stalker_asin_sellers");
|
||||
db.run("DELETE FROM stalker_sellers");
|
||||
db.run("DELETE FROM stalker_asin_scans");
|
||||
db.run("DELETE FROM stalker_runs");
|
||||
})();
|
||||
|
||||
return { ok: true, deleted: counts };
|
||||
}
|
||||
|
||||
function getRun(processType: ProcessType, runId: number) {
|
||||
if (processType === "lead_analysis") {
|
||||
const run = db
|
||||
@@ -1430,6 +1581,7 @@ const server = Bun.serve({
|
||||
"/": index,
|
||||
"/products": index,
|
||||
"/stalker": index,
|
||||
"/stalker/products": index,
|
||||
"/runs/:processType/:runId": index,
|
||||
"/api/runs": (req) => {
|
||||
const url = new URL(req.url);
|
||||
@@ -1443,6 +1595,16 @@ const server = Bun.serve({
|
||||
const url = new URL(req.url);
|
||||
return json(getStalkerResults(url.searchParams));
|
||||
},
|
||||
"/api/stalker/products": (req) => {
|
||||
const url = new URL(req.url);
|
||||
return json(getStalkerProducts(url.searchParams));
|
||||
},
|
||||
"/api/stalker/purge": (req) => {
|
||||
if (req.method !== "DELETE" && req.method !== "POST") {
|
||||
return json({ error: "Method not allowed" }, 405);
|
||||
}
|
||||
return json(purgeStalkerData());
|
||||
},
|
||||
"/api/upc/map": async (req) => {
|
||||
let upcs: string[];
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user