feat: implement SQLite persistent storage for analysis results
- Implemented database initialization and connection management in `database.ts` using Bun's SQLite. - Created `runs` and `results` tables to track historical analysis metadata and detailed product performance. - Updated `writer.ts` to persist analysis results to the database within a transaction, replacing the previous CSV output logic. - Updated README and `.gitignore` to reflect the new persistent storage capability.
This commit is contained in:
135
src/writer.ts
135
src/writer.ts
@@ -1,4 +1,4 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { getDb } from "./database.ts";
|
||||
import type { AnalysisResult } from "./types.ts";
|
||||
|
||||
function buildRow(r: AnalysisResult) {
|
||||
@@ -7,6 +7,12 @@ function buildRow(r: AnalysisResult) {
|
||||
r.product.record.sellingPriceFromSheet ??
|
||||
r.product.spApi.estimatedSalePrice;
|
||||
const rank = r.product.keepa?.salesRank ?? r.product.record.amazonRank;
|
||||
const canSellStatus =
|
||||
r.product.spApi.canSell == null
|
||||
? "unknown"
|
||||
: r.product.spApi.canSell
|
||||
? "yes"
|
||||
: "no";
|
||||
|
||||
return {
|
||||
ASIN: r.product.record.asin,
|
||||
@@ -44,12 +50,7 @@ function buildRow(r: AnalysisResult) {
|
||||
"FBA Fee": r.product.spApi.fbaFee,
|
||||
"FBM Fee": r.product.spApi.fbmFee,
|
||||
"Referral %": r.product.spApi.referralFeePercent,
|
||||
"Can Sell":
|
||||
r.product.spApi.canSell == null
|
||||
? "unknown"
|
||||
: r.product.spApi.canSell
|
||||
? "yes"
|
||||
: "no",
|
||||
"Can Sell": canSellStatus,
|
||||
Sellability: r.product.spApi.sellabilityStatus,
|
||||
"Sellability Reason": r.product.spApi.sellabilityReason ?? "",
|
||||
Verdict: r.verdict.verdict,
|
||||
@@ -58,6 +59,113 @@ function buildRow(r: AnalysisResult) {
|
||||
};
|
||||
}
|
||||
|
||||
export function writeResultsToDb(
|
||||
results: AnalysisResult[],
|
||||
dbPath: string,
|
||||
inputFile: string,
|
||||
outputFile: string | undefined,
|
||||
): void {
|
||||
const database = getDb(dbPath);
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const fbaCount = results.filter((r) => r.verdict.verdict === "FBA").length;
|
||||
const fbmCount = results.filter((r) => r.verdict.verdict === "FBM").length;
|
||||
const skipCount = results.filter((r) => r.verdict.verdict === "SKIP").length;
|
||||
|
||||
const insertRun = database.prepare(
|
||||
`INSERT INTO runs (
|
||||
timestamp,
|
||||
input_file,
|
||||
output_file,
|
||||
total_products,
|
||||
fba_count,
|
||||
fbm_count,
|
||||
skip_count
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
);
|
||||
const runInfo = insertRun.run(
|
||||
timestamp,
|
||||
inputFile,
|
||||
outputFile ?? null,
|
||||
results.length,
|
||||
fbaCount,
|
||||
fbmCount,
|
||||
skipCount,
|
||||
);
|
||||
const runId =
|
||||
(runInfo.changes as number) > 0
|
||||
? (runInfo.lastInsertRowid as number)
|
||||
: null;
|
||||
|
||||
if (runId === null) {
|
||||
console.error("Failed to insert run record into SQLite.");
|
||||
return;
|
||||
}
|
||||
|
||||
const insertResult = database.prepare(
|
||||
`INSERT INTO results (
|
||||
run_id, asin, product_name, brand, category, unit_cost, current_price,
|
||||
avg_price_90d, avg_price_90d_sheet, selling_price_sheet, sales_rank, rank_avg_90d,
|
||||
sellers, monthly_sold, rank_drops_30d, rank_drops_90d, fba_net_sheet,
|
||||
gross_profit_dollar, gross_profit_pct, net_profit_sheet, roi_sheet, moq, moq_cost,
|
||||
qty_available, supplier, source_url, asin_link, promo_coupon_code, notes, lead_date,
|
||||
fba_fee, fbm_fee, referral_percent, can_sell, sellability_status, sellability_reason,
|
||||
verdict, confidence, reasoning, fetched_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)`,
|
||||
);
|
||||
|
||||
database.transaction(() => {
|
||||
for (const r of results) {
|
||||
const row = buildRow(r);
|
||||
insertResult.run(
|
||||
runId,
|
||||
row.ASIN,
|
||||
row.Name,
|
||||
row.Brand,
|
||||
row.Category,
|
||||
row["Unit Cost"] ?? null,
|
||||
row["Current Price"] ?? null,
|
||||
row["Avg Price 90d"] ?? null,
|
||||
row["Avg Price 90d (sheet)"] ?? null,
|
||||
row["Selling Price (sheet)"] ?? null,
|
||||
row["Sales Rank"] ?? null,
|
||||
row["Rank Avg 90d"] ?? null,
|
||||
row.Sellers ?? null,
|
||||
row["Monthly Sold"] ?? null,
|
||||
row["Rank Drops 30d"] ?? null,
|
||||
row["Rank Drops 90d"] ?? null,
|
||||
row["FBA Net (sheet)"] ?? null,
|
||||
row["Gross Profit $"] ?? null,
|
||||
row["Gross Profit %"] ?? null,
|
||||
row["Net Profit (sheet)"] ?? null,
|
||||
row["ROI (sheet)"] ?? null,
|
||||
row.MOQ ?? null,
|
||||
row["MOQ Cost"] ?? null,
|
||||
row["Qty Available"] ?? null,
|
||||
row.Supplier ?? null,
|
||||
row["Source URL"] ?? null,
|
||||
row["ASIN Link"] ?? null,
|
||||
row["Promo/Coupon Code"] ?? null,
|
||||
row.Notes ?? null,
|
||||
row["Lead Date"] ?? null,
|
||||
row["FBA Fee"] ?? null,
|
||||
row["FBM Fee"] ?? null,
|
||||
row["Referral %"] ?? null,
|
||||
row["Can Sell"],
|
||||
row.Sellability,
|
||||
row["Sellability Reason"] ?? null,
|
||||
row.Verdict,
|
||||
row.Confidence ?? null,
|
||||
row.Reasoning,
|
||||
r.product.fetchedAt,
|
||||
);
|
||||
}
|
||||
})();
|
||||
console.log(`Results written to SQLite database for run_id: ${runId}`);
|
||||
}
|
||||
export function printResults(results: AnalysisResult[]): void {
|
||||
const rows = results
|
||||
.filter((r) => r.verdict.verdict === "FBA" || r.verdict.verdict === "FBM")
|
||||
@@ -144,16 +252,3 @@ export function printResults(results: AnalysisResult[]): void {
|
||||
`Sellability: ${summary.Available} available | ${summary.Restricted} restricted | ${summary.NotAvailable} not_available | ${summary.Unknown} unknown\n`,
|
||||
);
|
||||
}
|
||||
|
||||
export function writeResultsCsv(
|
||||
results: AnalysisResult[],
|
||||
outputPath: string,
|
||||
): void {
|
||||
const rows = results.map(buildRow);
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(rows);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Results");
|
||||
XLSX.writeFile(wb, outputPath);
|
||||
console.log(`Results written to ${outputPath}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user