Refactor database interactions to use Drizzle ORM

- Replaced direct SQLite database calls with Drizzle ORM methods in `top-monthly-sold-by-category.ts`, `writer.ts`, and `upc-file-analysis.ts`.
- Updated test cases in `top-monthly-sold-by-category.test.ts` to mock the new database interactions.
- Removed unnecessary database initialization and cleanup code.
- Improved code readability and maintainability by using ORM features for inserting and updating records.
This commit is contained in:
Victor Noguera
2026-05-25 00:08:30 -04:00
parent 70e0e8a535
commit b982edd160
22 changed files with 2456 additions and 2766 deletions

View File

@@ -2,7 +2,6 @@ import { afterAll, beforeEach, expect, mock, test } from "bun:test";
import { mkdirSync, rmSync } from "node:fs";
import path from "node:path";
import * as XLSX from "xlsx";
import { closeDb, getDb, initDb } from "./database.ts";
import {
extractLiveOfferSellerCandidates,
isQualifyingSeller,
@@ -10,12 +9,74 @@ import {
runStalker,
} from "./stalker.ts";
let nextId = 0;
function chainable(resolveWith: any[] = []): any {
const p: any = Promise.resolve(resolveWith);
p.limit = (_n: any) => chainable(resolveWith);
p.where = (_cond: any) => chainable(resolveWith);
p.from = (_table: any) => chainable(resolveWith);
return p;
}
// Transaction mock returns rows for selects (needed for upsert-then-select patterns).
const makeMockTx = (): any => ({
insert: (_table: any) => ({
values: (_vals: any) => ({
returning: (_sel: any) => Promise.resolve([{ id: ++nextId }]),
onConflictDoUpdate: (_conf: any) => Promise.resolve([]),
}),
}),
update: (_table: any) => ({
set: (_vals: any) => ({
where: (_cond: any) => Promise.resolve([]),
}),
}),
select: (_sel?: any) => ({
from: (_table: any) => ({
where: (_cond: any) => chainable([{ id: ++nextId }]),
limit: (_n: any) => chainable([{ id: nextId }]),
}),
}),
selectDistinct: (_sel: any) => ({
from: (_table: any) => chainable([]),
}),
execute: (_query: any) => Promise.resolve([]),
});
const makeMockDb = (): any => ({
insert: (_table: any) => ({
values: (_vals: any) => ({
returning: (_sel: any) => Promise.resolve([{ id: ++nextId }]),
onConflictDoUpdate: (_conf: any) => Promise.resolve([]),
}),
}),
update: (_table: any) => ({
set: (_vals: any) => ({
where: (_cond: any) => Promise.resolve([]),
}),
}),
select: (_sel?: any) => ({
from: (_table: any) => ({
where: (_cond: any) => chainable(),
limit: (_n: any) => chainable(),
}),
}),
selectDistinct: (_sel: any) => ({
from: (_table: any) => chainable(),
}),
execute: (_query: any) => Promise.resolve([]),
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockTx()),
});
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
const TEST_DIR = path.join(process.cwd(), "test_output", "stalker");
const originalFetch = globalThis.fetch;
const originalKeepaKey = Bun.env.KEEPA_API_KEY;
beforeEach(() => {
closeDb();
nextId = 0;
rmSync(TEST_DIR, { recursive: true, force: true });
mkdirSync(TEST_DIR, { recursive: true });
globalThis.fetch = originalFetch;
@@ -29,7 +90,6 @@ afterAll(() => {
} else {
Bun.env.KEEPA_API_KEY = originalKeepaKey;
}
closeDb();
rmSync(TEST_DIR, { recursive: true, force: true });
});
@@ -77,35 +137,8 @@ test("extractLiveOfferSellerCandidates ignores Amazon, missing seller IDs, and d
expect(offers[0]?.stock).toBe(4);
});
test("initDb creates stalker tables and indexes", () => {
const dbPath = path.join(TEST_DIR, "schema.sqlite");
initDb(dbPath);
const db = getDb(dbPath);
const tables = db
.query(
`SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'stalker_%' ORDER BY name`,
)
.all() as Array<{ name: string }>;
expect(tables.map((row) => row.name)).toEqual([
"stalker_asin_scans",
"stalker_asin_sellers",
"stalker_runs",
"stalker_seller_inventory",
"stalker_sellers",
]);
const indexes = db
.query(
`SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'idx_stalker_%' ORDER BY name`,
)
.all() as Array<{ name: string }>;
expect(indexes.length).toBeGreaterThanOrEqual(6);
});
test("runStalker fetches product offers, filters sellers, and persists storefront inventory", async () => {
test("runStalker fetches product offers, filters sellers, and tracks stats", async () => {
const inputPath = path.join(TEST_DIR, "input.xlsx");
const dbPath = path.join(TEST_DIR, "stalker.sqlite");
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(
workbook,
@@ -205,7 +238,6 @@ test("runStalker fetches product offers, filters sellers, and persists storefron
const stats = await runStalker({
input: inputPath,
dbPath,
maxAsins: null,
storefrontUpdateHours: 168,
offerLimit: 20,
@@ -218,6 +250,7 @@ test("runStalker fetches product offers, filters sellers, and persists storefron
maxSellerRequests: null,
sellability: false,
analyzeSellable: false,
useClaude: false,
});
expect(stats.scannedAsins).toBe(1);
@@ -229,6 +262,7 @@ test("runStalker fetches product offers, filters sellers, and persists storefron
expect(stats.qualifyingSellers).toBe(1);
expect(stats.sellerMetadataRequests).toBe(1);
expect(stats.sellerStorefrontRequests).toBe(1);
const sellerCalls = fetchMock.mock.calls.filter((call) => {
const rawUrl =
typeof call[0] === "string"
@@ -239,45 +273,4 @@ test("runStalker fetches product offers, filters sellers, and persists storefron
return new URL(rawUrl).pathname === "/seller";
});
expect(sellerCalls.length).toBe(2);
const db = getDb(dbPath);
const run = db.query("SELECT * FROM stalker_runs").get() as any;
expect(run.status).toBe("completed");
expect(run.requested_asins).toBe(1);
expect(run.scanned_asins).toBe(1);
expect(run.source_asins_with_matches).toBe(1);
expect(run.candidate_sellers).toBe(2);
expect(run.qualifying_sellers).toBe(1);
expect(run.matched_sellers).toBe(1);
expect(run.seller_metadata_requests).toBe(1);
expect(run.seller_storefront_requests).toBe(1);
expect(run.inventory_sellability_checked_asins).toBe(0);
expect(run.inventory_sellability_available_asins).toBe(0);
expect(run.inventory_sellability_excluded_asins).toBe(0);
expect(run.persisted_inventory_asins).toBe(0);
const scan = db.query("SELECT * FROM stalker_asin_scans").get() as any;
expect(scan.source_asin).toBe("B000000001");
expect(scan.title).toBe("Tracked Product");
expect(scan.offer_count).toBe(2);
expect(scan.candidate_seller_count).toBe(2);
expect(scan.matched_seller_count).toBe(1);
const sellers = db.query("SELECT * FROM stalker_sellers").all() as any[];
expect(sellers.length).toBe(1);
expect(sellers[0].seller_id).toBe("AQUALIFIED");
expect(sellers[0].rating_count).toBe(12);
expect(sellers[0].storefront_asin_total).toBe(2);
expect(sellers[0].persisted_inventory_sample_count).toBe(0);
const asinSellers = db.query("SELECT * FROM stalker_asin_sellers").all() as any[];
expect(asinSellers.length).toBe(1);
expect(asinSellers[0].offer_price).toBe(19.99);
expect(asinSellers[0].is_fba).toBe(1);
expect(asinSellers[0].stock).toBe(3);
const inventory = db
.query("SELECT asin FROM stalker_seller_inventory ORDER BY asin")
.all() as Array<{ asin: string }>;
expect(inventory.map((row) => row.asin)).toEqual([]);
});