164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
import { readProducts } from "./reader.ts";
|
|
import { connectCache, disconnectCache } from "./cache.ts";
|
|
import {
|
|
printResults,
|
|
writeResultsToDb,
|
|
writeResultsWorkbook,
|
|
} from "./writer.ts";
|
|
import { initDb, closeDb } from "./database.ts";
|
|
import {
|
|
chunkArray,
|
|
processProductChunk,
|
|
type SellabilityFilter,
|
|
} from "./analysis-pipeline.ts";
|
|
import path from "node:path";
|
|
import type { AnalysisResult } from "./types.ts";
|
|
|
|
const DB_PATH = process.env.RESULTS_DB_PATH || path.join("db", "results.db");
|
|
const INPUT_BATCH_SIZE = 50;
|
|
|
|
function parseSellabilityArg(args: string[]): SellabilityFilter {
|
|
const sellabilityArg = args.find((a) => a.startsWith("--sellability="));
|
|
const sellabilityValueFromEquals = sellabilityArg?.split("=")[1];
|
|
const sellabilityIdx = args.indexOf("--sellability");
|
|
const sellabilityValueFromNext =
|
|
sellabilityIdx !== -1 ? args[sellabilityIdx + 1] : undefined;
|
|
const rawSellability = sellabilityValueFromEquals ?? sellabilityValueFromNext;
|
|
|
|
if (!rawSellability) return "available";
|
|
|
|
const normalized = rawSellability.toLowerCase();
|
|
if (normalized === "available" || normalized === "all") {
|
|
return normalized;
|
|
}
|
|
|
|
console.error(
|
|
`Invalid --sellability value: \"${rawSellability}\". Use \"available\" or \"all\".`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
function parseArgs(): {
|
|
inputFile: string;
|
|
outputFile?: string;
|
|
sellability: SellabilityFilter;
|
|
} {
|
|
const args = process.argv.slice(2);
|
|
const outputFile = readFlagValue(args, "--out", "--output");
|
|
const inputFile = readInputFileArg(
|
|
args,
|
|
"--out",
|
|
"--output",
|
|
"--sellability",
|
|
);
|
|
const sellability = parseSellabilityArg(args);
|
|
|
|
if (!inputFile) {
|
|
console.error(
|
|
"Usage: bun run src/index.ts <input.csv|xlsx> [--out results.xlsx|--output results.xlsx] [--sellability available|all]",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
return { inputFile, outputFile, sellability };
|
|
}
|
|
|
|
function readFlagValue(args: string[], ...flags: string[]): string | undefined {
|
|
for (const flag of flags) {
|
|
const equalsArg = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
if (equalsArg) {
|
|
const value = equalsArg.slice(flag.length + 1);
|
|
if (value) return value;
|
|
}
|
|
|
|
const flagIdx = args.indexOf(flag);
|
|
if (flagIdx !== -1) {
|
|
return args[flagIdx + 1];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function readInputFileArg(
|
|
args: string[],
|
|
...flagsWithValues: string[]
|
|
): string | undefined {
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i]!;
|
|
if (flagsWithValues.includes(arg)) {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (flagsWithValues.some((flag) => arg.startsWith(`${flag}=`))) {
|
|
continue;
|
|
}
|
|
if (!arg.startsWith("--")) {
|
|
return arg;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function resolveBaseOutputPath(inputFile: string, outputFile?: string): string {
|
|
if (outputFile) return outputFile;
|
|
|
|
const parsedInput = path.parse(inputFile);
|
|
return path.join("output", `${parsedInput.name}_results.xlsx`);
|
|
}
|
|
|
|
async function main() {
|
|
const { inputFile, outputFile, sellability } = parseArgs();
|
|
|
|
console.log(`Sellability filter: ${sellability}`);
|
|
|
|
console.log("Connecting to Redis...");
|
|
await connectCache();
|
|
|
|
console.log("Initializing SQLite database...");
|
|
initDb(DB_PATH);
|
|
|
|
try {
|
|
console.log(`\nReading ${inputFile}...`);
|
|
const products = readProducts(inputFile);
|
|
|
|
if (products.length === 0) {
|
|
console.error("No valid products found in input file.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const productChunks = chunkArray(products, INPUT_BATCH_SIZE);
|
|
const allResults: AnalysisResult[] = [];
|
|
const resolvedBaseOutputPath = resolveBaseOutputPath(inputFile, outputFile);
|
|
|
|
if (productChunks.length > 1) {
|
|
console.log(
|
|
`\nLarge input detected (${products.length} products). Processing in chunks of ${INPUT_BATCH_SIZE}.`,
|
|
);
|
|
console.log(`Output base path: ${resolvedBaseOutputPath}`);
|
|
}
|
|
|
|
for (let chunkIndex = 0; chunkIndex < productChunks.length; chunkIndex++) {
|
|
const chunk = productChunks[chunkIndex]!;
|
|
console.log(
|
|
`\n=== Input chunk ${chunkIndex + 1}/${productChunks.length} (${chunk.length} products) ===`,
|
|
);
|
|
const chunkResults = await processProductChunk(chunk, { sellability });
|
|
allResults.push(...chunkResults);
|
|
}
|
|
|
|
printResults(allResults);
|
|
writeResultsWorkbook(allResults, resolvedBaseOutputPath);
|
|
writeResultsToDb(allResults, DB_PATH, inputFile, resolvedBaseOutputPath);
|
|
} finally {
|
|
await disconnectCache();
|
|
closeDb();
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Fatal error:", err);
|
|
process.exit(1);
|
|
});
|