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:
@@ -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([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user