Project Files
src / engine / csv-engine.ts
import fs from "node:fs/promises";
import { parse } from "csv-parse/sync";
import { stringify } from "csv-stringify/sync";
import type { ColumnDef, ParseOptions, StringifyOptions } from "../types";
import { CSVParseError, FilesystemError } from "../errors";
export async function parseCSV(
path: string,
options?: ParseOptions,
): Promise<Record<string, unknown>[]> {
let raw: string;
try {
raw = await fs.readFile(path, "utf-8");
} catch (cause) {
throw new FilesystemError(`Cannot read file: ${path}`);
}
if (options?.bom !== false && raw.charCodeAt(0) === 0xfeff) {
raw = raw.slice(1);
}
try {
const records = parse(raw, {
columns: true,
skip_empty_lines: options?.skipEmptyLines ?? true,
delimiter: options?.delimiter ?? ",",
bom: false,
relax_column_count: true,
});
return records as Record<string, unknown>[];
} catch (cause) {
throw new CSVParseError(`Failed to parse CSV: ${(cause as Error).message}`);
}
}
export async function stringifyCSV(
rows: Record<string, unknown>[],
columns: ColumnDef[],
options?: StringifyOptions,
): Promise<string> {
const colNames = columns.map((c) => c.name);
const data = rows.map((row) => {
const ordered: Record<string, unknown> = {};
for (const name of colNames) {
ordered[name] = row[name] ?? "";
}
return ordered;
});
try {
const out = stringify(data, {
header: options?.header ?? true,
delimiter: options?.delimiter ?? ",",
quoted_string: options?.quotedString ?? true,
});
return out;
} catch (cause) {
throw new CSVParseError(
`Failed to stringify CSV: ${(cause as Error).message}`,
);
}
}
export async function writeCSV(
path: string,
rows: Record<string, unknown>[],
columns: ColumnDef[],
): Promise<void> {
const content = await stringifyCSV(rows, columns);
try {
await fs.writeFile(path, content, "utf-8");
} catch (cause) {
throw new FilesystemError(`Cannot write file: ${path}`);
}
}
export async function appendToCSV(
path: string,
row: Record<string, unknown>,
columns: ColumnDef[],
): Promise<void> {
try {
await fs.access(path);
const content = await stringifyCSV([row], columns, { header: false });
await fs.appendFile(path, content, "utf-8");
} catch {
// file doesn't exist — create with headers
await writeCSV(path, [row], columns);
}
}
export async function appendRowsToCSV(
path: string,
rows: Record<string, unknown>[],
columns: ColumnDef[],
): Promise<void> {
try {
await fs.access(path);
const content = await stringifyCSV(rows, columns, { header: false });
await fs.appendFile(path, content, "utf-8");
} catch {
await writeCSV(path, rows, columns);
}
}
export async function readCSVHeaders(path: string): Promise<string[]> {
try {
const raw = await fs.readFile(path, "utf-8");
const firstLine = raw.split("\n")[0];
if (!firstLine) return [];
return firstLine.split(",").map((h) => h.trim().replace(/^"|"$/g, ""));
} catch (cause) {
throw new FilesystemError(`Cannot read CSV headers: ${path}`);
}
}