feat: add Stalker products functionality with filtering, pagination, and purge option

This commit is contained in:
Victor Noguera
2026-05-19 19:37:05 -04:00
parent aed0c11017
commit f6178a665c
6 changed files with 444 additions and 20 deletions

View File

@@ -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 {