CI / test (push) Failing after 8s
/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.
61 lines
1.9 KiB
TypeScript
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;
|
|
}
|