test: refactor csv-loader tests into separate files

Split the monolithic `src/csv-loader/loader.test.ts` into multiple
specialized test files to improve maintainability and readability:

- `parseCsv-basic.test.ts`: Primitive types, arrays, and tuples
- `parseCsv-caching.test.ts`: Table caching logic
- `parseCsv-circular.test.ts`: Circular reference detection
- `parseCsv-combinators.test.ts`: References in unions and tuples
- `parseCsv-noResolveRefs.
This commit is contained in:
hypercross 2026-04-20 11:37:56 +08:00
parent d0ed1dc92d
commit 3e768f5c83
10 changed files with 1059 additions and 1038 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,112 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
describe("parseCsv - basic parsing", () => {
it("should parse a simple CSV with primitive types", () => {
const csv = [
"name,age,active",
"string,number,boolean",
"Alice,30,true",
"Bob,25,false",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(2);
expect(result.data[0]).toEqual({ name: "Alice", age: 30, active: true });
expect(result.data[1]).toEqual({ name: "Bob", age: 25, active: false });
});
it("should parse CSV with int and float columns", () => {
const csv = [
"id,count,price",
"int,int,float",
"1,5,9.99",
"2,3,4.50",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(2);
expect(result.data[0]).toEqual({ id: 1, count: 5, price: 9.99 });
expect(result.data[1]).toEqual({ id: 2, count: 3, price: 4.5 });
});
it("should parse CSV with non-ASCII characters and comments", () => {
const csv = [
'# id: unique intent state ID (e.g. "仙人掌怪-boost")',
"id",
"string",
"仙人掌怪-boost",
"仙人掌怪-defend",
"仙人掌怪-attack",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(3);
expect(result.data[0]).toEqual({ id: "仙人掌怪-boost" });
expect(result.data[1]).toEqual({ id: "仙人掌怪-defend" });
expect(result.data[2]).toEqual({ id: "仙人掌怪-attack" });
});
it("should parse CSV with string literal columns (unquoted in CSV)", () => {
const csv = [
"name,status",
"string,'on' | 'off'",
"Alice,on",
"Bob,off",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(2);
expect(result.data[0]).toEqual({ name: "Alice", status: "on" });
expect(result.data[1]).toEqual({ name: "Bob", status: "off" });
});
it("should parse CSV with array columns", () => {
const csv = [
"name,tags",
"string,string[]",
"Alice,[dev; admin]",
"Bob,[user]",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(2);
expect(result.data[0]).toEqual({ name: "Alice", tags: ["dev", "admin"] });
expect(result.data[1]).toEqual({ name: "Bob", tags: ["user"] });
});
it("should parse CSV with tuple columns", () => {
const csv = [
"name,coords",
"string,[number; number]",
"Alice,[1; 2]",
"Bob,[3; 4]",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(2);
expect(result.data[0]).toEqual({ name: "Alice", coords: [1, 2] });
expect(result.data[1]).toEqual({ name: "Bob", coords: [3, 4] });
});
it("should require at least 2 rows (header + schema)", () => {
const csv = "name,age\nstring,number";
expect(() => parseCsv(csv, { emitTypes: false })).not.toThrow();
const csv1Row = "name,age";
expect(() => parseCsv(csv1Row, { emitTypes: false })).toThrow(
"at least 2 rows",
);
});
it("should throw if header and schema count mismatch", () => {
const csv = "name,age\nstring";
expect(() => parseCsv(csv, { emitTypes: false })).toThrow("does not match");
});
});

View File

@ -0,0 +1,25 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir, readFixture } from "../test-utils";
describe("parseCsv - caching", () => {
it("should cache referenced table and not re-read on subsequent references", () => {
const usersCsv = readFixture("users.csv");
const csv = ["id,creator,reviewer", "string,@users,@users", "1,1,2"].join(
"\n",
);
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
const creator = result.data[0].creator as Record<string, unknown>;
const reviewer = result.data[0].reviewer as Record<string, unknown>;
expect(creator).not.toEqual(reviewer);
expect(creator.id).toBe("1");
expect(reviewer.id).toBe("2");
});
});

View File

@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir, readFixture } from "../test-utils";
describe("parseCsv - circular reference detection", () => {
it("should detect self-referencing circular reference", () => {
const csv = ["id,name,parent", "string,string,@self_ref", "1,Root,2"].join(
"\n",
);
expect(() =>
parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "self_ref.csv"),
}),
).toThrow(/Circular reference detected/);
});
it("should detect mutual circular reference (A -> B -> A)", () => {
const csv = readFixture("circular_a.csv");
expect(() =>
parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "circular_a.csv"),
}),
).toThrow(/Circular reference detected/);
});
it("should allow same table referenced from multiple columns without circular reference", () => {
const usersCsv = readFixture("users.csv");
const csv = ["id,creator,reviewer", "string,@users,@users", "1,1,2"].join(
"\n",
);
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data[0].creator).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(result.data[0].reviewer).toEqual({
id: "2",
name: "Bob",
email: "bob@example.com",
});
});
});

View File

@ -0,0 +1,164 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir } from "../test-utils";
describe("parseCsv - references in combinatory schemas", () => {
it("should resolve reference inside a tuple", () => {
const csv = [
"id,info",
"string,[ref: @users; note: string]",
"1,[ref: 1; note: urgent]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(1);
const info = result.data[0].info as unknown[];
expect(info).toHaveLength(2);
expect(info[0]).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(info[1]).toBe("urgent");
});
it("should resolve reference array inside a tuple", () => {
const csv = [
"id,info",
"string,[refs: @users[]; note: string]",
"1,[refs: [1; 2]; note: test]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(1);
const info = result.data[0].info as unknown[];
expect(info).toHaveLength(2);
const refs = info[0] as Record<string, unknown>[];
expect(refs).toHaveLength(2);
expect(refs[0]).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(refs[1]).toEqual({ id: "2", name: "Bob", email: "bob@example.com" });
expect(info[1]).toBe("test");
});
it("should resolve array of tuples containing references", () => {
const csv = [
"id,pairs",
"string,[@users; number][]",
"1,[[1; 10]; [2; 20]]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(1);
const pairs = result.data[0].pairs as unknown[][];
expect(pairs).toHaveLength(2);
expect(pairs[0]).toHaveLength(2);
expect(pairs[0][0]).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(pairs[0][1]).toBe(10);
expect(pairs[1][0]).toEqual({
id: "2",
name: "Bob",
email: "bob@example.com",
});
expect(pairs[1][1]).toBe(20);
});
it("should resolve reference in union (@users | string)", () => {
const csv = ["id,value", "string,@users | string", "1,1", "2,unknown"].join(
"\n",
);
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(2);
expect(result.data[0].value).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(result.data[1].value).toBe("unknown");
});
it("should resolve reference in union (@users[] | string)", () => {
const csv = [
"id,value",
"string,@users[] | string",
"1,[1; 2]",
"2,none",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(2);
const arr = result.data[0].value as Record<string, unknown>[];
expect(arr).toHaveLength(2);
expect(arr[0]).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(result.data[1].value).toBe("none");
});
it("should resolve array of reference unions (@users | @parts)[]", () => {
const csv = ["id,items", "string,(@users | @parts)[]", "1,[1; 2]"].join(
"\n",
);
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(1);
});
it("should resolve named tuple with reference and other fields", () => {
const csv = [
"id,details",
"string,[owner: @users; count: number]",
"1,[owner: 1; count: 5]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(1);
const details = result.data[0].details as unknown[];
expect(details).toHaveLength(2);
expect(details[0]).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(details[1]).toBe(5);
});
});

View File

@ -0,0 +1,190 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir, readFixture } from "../test-utils";
describe("parseCsv - resolveReferences: false", () => {
it("should store IDs instead of resolved objects for reference fields", () => {
const csv = [
"id,customer,items",
"string,@users,@parts[]",
"1,1,[1; 2]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
});
expect(result.data[0].customer).toBe("1");
expect(result.data[0].items).toEqual(["1", "2"]);
});
it("should populate referenceFields with metadata", () => {
const csv = [
"id,customer,items",
"string,@users,@parts[]",
"1,1,[1; 2]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
});
expect(result.referenceFields).toHaveLength(2);
expect(result.referenceFields[0]).toEqual({
name: "customer",
tableName: "users",
isArray: false,
schema: expect.objectContaining({
type: "reference",
tableName: "users",
isArray: false,
}),
});
expect(result.referenceFields[1]).toEqual({
name: "items",
tableName: "parts",
isArray: true,
schema: expect.objectContaining({
type: "reference",
tableName: "parts",
isArray: true,
}),
});
});
it("should not load referenced CSV files", () => {
const csv = ["id,customer", "string,@nonexistent", "1,someid"].join("\n");
expect(() =>
parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
}),
).not.toThrow();
});
it("should store IDs for nested references in tuples", () => {
const csv = [
"id,info",
"string,[ref: @users; note: string]",
"1,[ref: 1; note: urgent]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
});
expect((result.data[0].info as unknown[])[0]).toBe("1");
expect((result.data[0].info as unknown[])[1]).toBe("urgent");
expect(result.referenceFields).toHaveLength(1);
expect(result.referenceFields[0].tableName).toBe("users");
});
it("should store IDs for references in unions", () => {
const csv = ["id,value", "string,@users | string", "1,1", "2,unknown"].join(
"\n",
);
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
});
expect(result.data[0].value).toBe("1");
expect(result.data[1].value).toBe("unknown");
});
it("should not throw for self-referencing table when resolveReferences is false", () => {
const csv = readFixture("self_ref.csv");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
currentFilePath: path.join(fixturesDir, "self_ref.csv"),
});
expect(result.data).toHaveLength(2);
expect(result.data[0].parent).toBe("2");
expect(result.data[1].parent).toBe("1");
expect(result.referenceFields).toHaveLength(1);
expect(result.referenceFields[0].tableName).toBe("self_ref");
});
it("should not throw for cross-referencing tables when resolveReferences is false", () => {
const csv = readFixture("circular_a.csv");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
currentFilePath: path.join(fixturesDir, "circular_a.csv"),
});
expect(result.data).toHaveLength(1);
expect(result.data[0].related).toEqual(["1"]);
expect(result.referenceFields).toHaveLength(1);
expect(result.referenceFields[0].tableName).toBe("circular_b");
});
it("should store IDs for nested self-reference in tuple", () => {
const csv = [
"id,name,parent_info",
"string,string,[parent: @self_ref; role: string]",
"1,Root,[parent: 2; role: admin]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
currentFilePath: path.join(fixturesDir, "self_ref.csv"),
});
const parentInfo = result.data[0].parent_info as unknown[];
expect(parentInfo[0]).toBe("2");
expect(parentInfo[1]).toBe("admin");
expect(result.referenceFields).toHaveLength(1);
expect(result.referenceFields[0].tableName).toBe("self_ref");
});
it("should store IDs for self-reference in union", () => {
const csv = [
"id,name,ref_or_val",
"string,string,@self_ref | string",
"1,Root,2",
"2,Child,none",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
currentFilePath: path.join(fixturesDir, "self_ref.csv"),
});
expect(result.data[0].ref_or_val).toBe("2");
expect(result.data[1].ref_or_val).toBe("none");
expect(result.referenceFields).toHaveLength(1);
expect(result.referenceFields[0].tableName).toBe("self_ref");
});
it("should store IDs for self-reference array in tuple", () => {
const csv = [
"id,name,children",
"string,string,[@self_ref[]]",
"1,Root,[[2]]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
currentFilePath: path.join(fixturesDir, "self_ref.csv"),
});
const children = result.data[0].children as unknown[];
expect(children[0]).toEqual(["2"]);
expect(result.referenceFields).toHaveLength(1);
expect(result.referenceFields[0].tableName).toBe("self_ref");
});
});

View File

@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import { fixturesDir } from "../test-utils";
describe("parseCsv - refBaseDir option", () => {
it("should use refBaseDir to resolve reference paths", () => {
const csv = ["id,customer", "string,@users", "1,1"].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
refBaseDir: fixturesDir,
});
expect(result.data[0].customer).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
});
});

View File

@ -0,0 +1,148 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir, readFixture } from "../test-utils";
import fs from "fs";
describe("parseCsv - reference resolution", () => {
it("should resolve single reference to another CSV table", () => {
const usersCsv = readFixture("users.csv");
const result = parseCsv(usersCsv, { emitTypes: false });
expect(result.data).toHaveLength(3);
expect(result.data[0]).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
});
it("should resolve reference values using parseCsv with referenced tables", () => {
const ordersCsv = [
"id,customer,total",
"string,@users,number",
"1,1,100",
].join("\n");
const result = parseCsv(ordersCsv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "orders.csv"),
});
expect(result.data).toHaveLength(1);
expect(result.data[0].customer).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
expect(result.data[0].total).toBe(100);
});
it("should resolve array reference values", () => {
const ordersCsv = [
"id,items,total",
"string,@parts[],number",
"1,[1; 2],35.5",
].join("\n");
const result = parseCsv(ordersCsv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "orders.csv"),
});
expect(result.data).toHaveLength(1);
const items = result.data[0].items as Record<string, unknown>[];
expect(items).toHaveLength(2);
expect(items[0]).toEqual({ id: "1", name: "Widget", price: 10.5 });
expect(items[1]).toEqual({ id: "2", name: "Gadget", price: 25 });
expect(result.data[0].total).toBe(35.5);
});
it("should resolve mixed single and array references", () => {
const ordersCsv = [
"id,customer,items,total",
"string,@users,@parts[],number",
"1,1,[1; 2],35.5",
].join("\n");
const result = parseCsv(ordersCsv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "orders.csv"),
});
expect(result.data).toHaveLength(1);
expect(result.data[0].customer).toEqual({
id: "1",
name: "Alice",
email: "alice@example.com",
});
const items = result.data[0].items as Record<string, unknown>[];
expect(items).toHaveLength(2);
expect(items[0]).toEqual({ id: "1", name: "Widget", price: 10.5 });
});
it("should throw error for reference to non-existent ID", () => {
const ordersCsv = ["id,customer", "string,@users", "1,999"].join("\n");
expect(() =>
parseCsv(ordersCsv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "orders.csv"),
}),
).toThrow(/not found/);
});
it("should throw error for reference to non-existent table", () => {
const csv = ["id,ref", "string,@nonexistent", "1,someid"].join("\n");
expect(() =>
parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
}),
).toThrow(/Failed to load referenced table/);
});
it("should collect reference table names", () => {
const csv = ["id,customer,items", "string,@users,@parts[]", "1,1,[1]"].join(
"\n",
);
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.references.has("users")).toBe(true);
expect(result.references.has("parts")).toBe(true);
});
it("should use custom primary key", () => {
const nameCsv = [
"code,name",
"string,string",
"US,United States",
"UK,United Kingdom",
].join("\n");
const nameCsvPath = path.join(fixturesDir, "countries.csv");
fs.writeFileSync(nameCsvPath, nameCsv);
try {
const refCsv = ["id,country", "string,@countries", "1,US"].join("\n");
const result = parseCsv(refCsv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "ref.csv"),
defaultPrimaryKey: "code",
});
expect(result.data[0].country).toEqual({
code: "US",
name: "United States",
});
} finally {
fs.unlinkSync(nameCsvPath);
}
});
});

View File

@ -0,0 +1,269 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir } from "../test-utils";
import fs from "fs";
describe("parseCsv - reverse reference resolution", () => {
it("should resolve reverse reference from comment declaration", () => {
const ordersCsvPath = path.join(fixturesDir, "rev_orders.csv");
const ordersContent = [
"id,customer,total",
"string,string,number",
"1,1,100",
"2,1,50",
"3,2,75",
].join("\n");
fs.writeFileSync(ordersCsvPath, ordersContent);
try {
const csv = [
"id,name",
"string,string",
"# orders := ~rev_orders(customer)",
"1,Alice",
"2,Bob",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(2);
const aliceOrders = result.data[0].orders as Record<string, unknown>[];
expect(aliceOrders).toHaveLength(2);
expect(aliceOrders[0]).toEqual({ id: "1", customer: "1", total: 100 });
expect(aliceOrders[1]).toEqual({ id: "2", customer: "1", total: 50 });
const bobOrders = result.data[1].orders as Record<string, unknown>[];
expect(bobOrders).toHaveLength(1);
expect(bobOrders[0].id).toBe("3");
} finally {
fs.unlinkSync(ordersCsvPath);
}
});
it("should return empty array for reverse reference with no matches", () => {
const ordersCsvPath = path.join(fixturesDir, "rev_orders.csv");
const ordersContent = [
"id,customer,total",
"string,string,number",
"1,1,100",
"2,1,50",
].join("\n");
fs.writeFileSync(ordersCsvPath, ordersContent);
try {
const csv = [
"id,name",
"string,string",
"# orders := ~rev_orders(customer)",
"1,Alice",
"99,Nobody",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(2);
expect(result.data[0].orders).toHaveLength(2);
expect(result.data[1].orders).toEqual([]);
} finally {
fs.unlinkSync(ordersCsvPath);
}
});
it("should return null for optional reverse reference with no matches", () => {
const csv = [
"id,name",
"string,string",
"# orders := ~orders(customer)?",
"99,Nobody",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.data).toHaveLength(1);
expect(result.data[0].orders).toBeNull();
});
it("should populate reverseReferences in result", () => {
const csv = [
"id,name",
"string,string",
"# orders := ~orders(customer)",
"1,Alice",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.reverseReferences).toHaveLength(1);
expect(result.reverseReferences[0]).toEqual({
fieldName: "orders",
tableName: "orders",
foreignKey: "customer",
isOptional: false,
schema: {
type: "reverseReference",
tableName: "orders",
foreignKey: "customer",
isOptional: false,
},
});
});
it("should include reverse reference tables in references set", () => {
const csv = [
"id,name",
"string,string",
"# orders := ~orders(customer)",
"1,Alice",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.references.has("orders")).toBe(true);
});
it("should support multiple reverse reference declarations", () => {
const csv = [
"id,name",
"string,string",
"# orders := ~orders(customer)",
"# parts := ~parts(user)",
"1,Alice",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.reverseReferences).toHaveLength(2);
expect(result.reverseReferences[0].fieldName).toBe("orders");
expect(result.reverseReferences[1].fieldName).toBe("parts");
expect(result.data[0]).toHaveProperty("orders");
expect(result.data[0]).toHaveProperty("parts");
});
it("should ignore comment lines that are not reverse reference declarations", () => {
const csv = [
"id,name",
"string,string",
"# This is just a comment",
"# orders := ~orders(customer)",
"1,Alice",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.reverseReferences).toHaveLength(1);
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");
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 });
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", () => {
it("should populate referenceFields for reverse references", () => {
const csv = [
"id,name",
"string,string",
"# orders := ~orders(customer)",
"1,Alice",
].join("\n");
const result = parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
});
const withForeignKey = result.referenceFields.filter(
(f) => f.foreignKey === "customer",
);
expect(withForeignKey).toHaveLength(1);
expect(withForeignKey[0]).toEqual({
name: "orders",
tableName: "orders",
isArray: true,
foreignKey: "customer",
schema: expect.objectContaining({
type: "reverseReference",
tableName: "orders",
foreignKey: "customer",
}),
});
});
it("should not load referenced CSV files for reverse references", () => {
const csv = [
"id,name",
"string,string",
"# nonexistent := ~nonexistent(some_key)",
"1,Alice",
].join("\n");
expect(() =>
parseCsv(csv, {
emitTypes: false,
resolveReferences: false,
}),
).not.toThrow();
});
});

View File

@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { parseCsv } from "../loader";
import * as path from "path";
import { fixturesDir } from "../test-utils";
describe("parseCsv - type generation", () => {
it("should generate type definition with emitTypes enabled", () => {
const csv = ["name,age", "string,number", "Alice,30"].join("\n");
const result = parseCsv(csv, { emitTypes: true, resourceName: "people" });
expect(result.typeDefinition).toBeDefined();
expect(result.typeDefinition).toContain("peopleTable");
expect(result.typeDefinition).toContain("readonly name: string");
expect(result.typeDefinition).toContain("readonly age: number");
});
it("should include reference imports in type definition", () => {
const csv = ["id,customer", "string,@users", "1,1"].join("\n");
const result = parseCsv(csv, {
emitTypes: true,
resourceName: "orders",
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.typeDefinition).toBeDefined();
expect(result.typeDefinition).toContain("Users");
expect(result.typeDefinition).toContain("users.csv");
});
it("should not generate type definition when emitTypes is false", () => {
const csv = ["name,age", "string,number", "Alice,30"].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.typeDefinition).toBeUndefined();
});
it("should generate correct type for reference column", () => {
const csv = ["id,customer", "string,@users", "1,1"].join("\n");
const result = parseCsv(csv, {
emitTypes: true,
resourceName: "orders",
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.typeDefinition).toContain("readonly customer: Users");
});
it("should generate correct type for array reference column", () => {
const csv = ["id,items", "string,@parts[]", "1,[1]"].join("\n");
const result = parseCsv(csv, {
emitTypes: true,
resourceName: "orders",
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.typeDefinition).toContain("readonly items: Parts[]");
});
it("should generate correct type for reference in tuple", () => {
const csv = [
"id,info",
"string,[ref: @users; note: string]",
"1,[ref: 1; note: test]",
].join("\n");
const result = parseCsv(csv, {
emitTypes: true,
resourceName: "data",
currentFilePath: path.join(fixturesDir, "test.csv"),
});
expect(result.typeDefinition).toContain("Users");
});
});