Compare commits

..

No commits in common. "5a1627c6f111e24f9a831498cc6ea1b5b71097dd" and "e76ae79b2d850c064bf01aa946b4598143ee0d0a" have entirely different histories.

2 changed files with 26 additions and 75 deletions

View File

@ -1164,52 +1164,6 @@ describe("parseCsv - reverse reference resolution", () => {
expect(result.reverseReferences).toHaveLength(1); expect(result.reverseReferences).toHaveLength(1);
expect(result.data[0]).toHaveProperty("orders"); expect(result.data[0]).toHaveProperty("orders");
}); });
it("should handle multiple comment lines including plain comments and reverse references", () => {
const orderCsvPath = path.join(fixturesDir, "order.csv");
const orderContent = [
"id,user,total",
"string,string,number",
"o01,u01,100",
"o02,u01,50",
"o03,u02,75",
].join("\n");
fs.writeFileSync(orderCsvPath, orderContent);
try {
const csv = [
"# id: id of user",
"# orders: list of related orders",
"# orders := ~order(user)",
"id,name",
"string,string",
"u01,Alice",
"u02,Bob",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.reverseReferences).toHaveLength(1);
expect(result.reverseReferences[0].fieldName).toBe("orders");
expect(result.data).toHaveLength(2);
expect(result.data[0].id).toBe("u01");
expect(result.data[1].id).toBe("u02");
// User u01 (Alice) should have 2 orders
const aliceOrders = result.data[0].orders as Record<string, unknown>[];
expect(aliceOrders).toHaveLength(2);
expect(aliceOrders[0]).toEqual({ id: "o01", user: "u01", total: 100 });
expect(aliceOrders[1]).toEqual({ id: "o02", user: "u01", total: 50 });
// User u02 (Bob) should have 1 order
const bobOrders = result.data[1].orders as Record<string, unknown>[];
expect(bobOrders).toHaveLength(1);
expect(bobOrders[0].id).toBe("o03");
} finally {
fs.unlinkSync(orderCsvPath);
}
});
}); });
describe("parseCsv - reverse reference with resolveReferences: false", () => { describe("parseCsv - reverse reference with resolveReferences: false", () => {

View File

@ -538,13 +538,12 @@ export interface ReverseReferenceDeclaration {
*/ */
function parseReverseReferenceDeclaration( function parseReverseReferenceDeclaration(
line: string, line: string,
commentChar: string = "#",
): ReverseReferenceDeclaration | null { ): ReverseReferenceDeclaration | null {
const trimmed = line.trim(); const trimmed = line.trim();
// Must start with the comment character // Must start with # (comment)
if (!trimmed.startsWith(commentChar)) return null; if (!trimmed.startsWith("#")) return null;
const content = trimmed.slice(commentChar.length).trim(); const content = trimmed.slice(1).trim();
// Match pattern: fieldName := ~tableName(foreignKey) // Match pattern: fieldName := ~tableName(foreignKey)
const match = content.match(/^(\w+)\s*:=\s*~(\w+)\((\w+)\)(\?)?$/); const match = content.match(/^(\w+)\s*:=\s*~(\w+)\((\w+)\)(\?)?$/);
@ -794,36 +793,18 @@ export function parseCsv(
quote, quote,
escape, escape,
bom, bom,
// Don't let csv-parse skip comments; we need to parse them for reverse references. comment: undefined, // Don't let csv-parse skip comments; we need to parse them for reverse references
// Comment lines are filtered out manually below using the configured comment character.
comment: undefined,
trim, trim,
skip_empty_lines: true, skip_empty_lines: true,
relax_column_count: true, relax_column_count: true,
}); });
// Filter out comment lines from all records, collecting reverse reference declarations if (records.length < 2) {
const reverseReferences: ReverseReferenceDeclaration[] = [];
const filteredRecords: string[][] = [];
for (const row of records) {
const firstCell = (row[0] ?? "").trim();
if (comment && firstCell.startsWith(comment)) {
const decl = parseReverseReferenceDeclaration(firstCell, comment);
if (decl) {
reverseReferences.push(decl);
}
// Skip comment lines (whether or not they're reverse ref declarations)
continue;
}
filteredRecords.push(row);
}
if (filteredRecords.length < 2) {
throw new Error("CSV must have at least 2 rows: headers and schemas"); throw new Error("CSV must have at least 2 rows: headers and schemas");
} }
const headers = filteredRecords[0]; const headers = records[0];
const schemas = filteredRecords[1]; const schemas = records[1];
if (headers.length !== schemas.length) { if (headers.length !== schemas.length) {
throw new Error( throw new Error(
@ -831,14 +812,30 @@ export function parseCsv(
); );
} }
const dataRows = filteredRecords.slice(2); // Parse reverse reference declarations from comment lines between schema row and data rows
const reverseReferences: ReverseReferenceDeclaration[] = [];
const dataRows: string[][] = [];
for (let i = 2; i < records.length; i++) {
const row = records[i];
// Check if this is a single-column row starting with # (comment with reverse ref declaration)
const firstCell = (row[0] ?? "").trim();
if (firstCell.startsWith("#")) {
const decl = parseReverseReferenceDeclaration(firstCell);
if (decl) {
reverseReferences.push(decl);
}
// Skip comment lines (whether or not they're reverse ref declarations)
continue;
}
dataRows.push(row);
}
// Also check schema row cells for comment-prefixed reverse reference declarations // Also check schema row cells for comment-prefixed reverse reference declarations
// (in case they appear as schema cells rather than separate rows) // (in case they appear as schema cells rather than separate rows)
for (let col = 0; col < schemas.length; col++) { for (let col = 0; col < schemas.length; col++) {
const cell = (schemas[col] ?? "").trim(); const cell = (schemas[col] ?? "").trim();
if (comment && cell.startsWith(comment)) { if (cell.startsWith("#")) {
const decl = parseReverseReferenceDeclaration(cell, comment); const decl = parseReverseReferenceDeclaration(cell);
if (decl) { if (decl) {
reverseReferences.push(decl); reverseReferences.push(decl);
} }