Files
ubi-dan-itam/src/lib/csv.ts
T
toanhoang aa7f5515db
CI / test (push) Failing after 8s
Add AssetItem CSV import/export
/assets gets Export CSV (GET /api/assets/export) and Import CSV
(POST /api/assets/import) buttons. Export emits human-readable
manufacturer/model columns instead of raw modelId; import resolves those
back to an AssetModel (case-insensitive match, must already exist -- this
does not auto-create models) so export -> edit in Excel -> re-import is a
real round trip.

Per-row import errors (unmatched model, missing required field, bad date)
are collected and returned alongside the created count rather than
aborting the whole file on the first bad row -- a typo in row 200 should
not cost rows 1-199. warrantyEndDate still auto-fills from the models
warrantyMonths when the CSV omits it, same as manual creation.

Extracted src/lib/csv.ts (splitCsvLine/splitCsvLines/normalizeHeader/
csvField) out of expected-assets.ts so both CSV parsers share one
implementation instead of duplicating it.

Manual smoke test only (tsx script: export shape, valid row + unmatched-
model row + missing-date row all handled correctly), no Vitest coverage,
not through the ba->pm->feature-implementer->test-runner->code-reviewer
pipeline -- same deviation as the rest of this branch, see CLAUDE.md.
tsc/lint clean.
2026-08-23 07:41:52 -04:00

61 lines
1.9 KiB
TypeScript

// Shared minimal CSV parsing helpers -- extracted from
// src/lib/expected-assets.ts (2026-08-21) when src/lib/assets.ts needed
// the same line-splitting logic for AssetItem import (2026-08-23). Handles
// quoted fields (incl. embedded commas/escaped double-quotes) without a
// dependency, which is enough for this app's actual import files (simple
// flat tables, not arbitrary user-supplied CSV).
/**
* Splits one CSV line into fields. A quote only opens a quoted field when
* it's the very first character of that field -- a stray/legitimate quote
* mid-value (e.g. a 24" monitor size) is kept as a literal character
* instead of wrongly starting quoted-content mode.
*/
export function splitCsvLine(line: string): string[] {
const fields: string[] = [];
let current = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (inQuotes) {
if (char === '"') {
if (line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = false;
}
} else {
current += char;
}
} else if (char === '"' && current === "") {
inQuotes = true;
} else if (char === ",") {
fields.push(current);
current = "";
} else {
current += char;
}
}
fields.push(current);
return fields.map((f) => f.trim());
}
/** Splits raw CSV text into non-blank lines (handles \r\n, \r, \n). */
export function splitCsvLines(text: string): string[] {
return text.split(/\r\n|\r|\n/).filter((line) => line.trim() !== "");
}
export function normalizeHeader(header: string): string {
return header.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
}
/** Quotes a single CSV field for output if it contains a comma, quote, or
* newline -- doubling any embedded quotes, per RFC4180. */
export function csvField(value: string): string {
if (/[",\n]/.test(value)) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}