import { describe, it, expect } from "vitest"; import { csvToModule } from "./module-gen"; import * as path from "path"; import { fixturesDir, readFixture } from "./test-utils"; describe("csvToModule - accessor-based output", () => { it("should emit accessor function for tables without references", () => { const csv = ["name,age", "string,number", "Alice,30"].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("export default function getData()"); expect(result.js).not.toContain("import "); expect(result.js).not.toContain("Lookup"); }); it("should emit accessor function for tables with references", () => { const csv = ["id,customer", "string,@users", "1,1"].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("import _users from './users.csv'"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("_usersLookup"); expect(result.js).toContain("_resolved = _raw;"); }); it("should emit accessor function for tables with array references", () => { const csv = ["id,items", "string,@parts[]", "1,[1; 2]"].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("import _parts from './parts.csv'"); expect(result.js).toContain("_partsLookup"); expect(result.js).toContain(".map(id =>"); }); it("should emit multiple imports for multiple reference tables", () => { const csv = [ "id,customer,items", "string,@users,@parts[]", "1,1,[1; 2]", ].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("import _users from './users.csv'"); expect(result.js).toContain("import _parts from './parts.csv'"); expect(result.js).toContain("_usersLookup"); expect(result.js).toContain("_partsLookup"); }); it("should generate function type in dts for tables with references", () => { const csv = ["id,customer", "string,@users", "1,1"].join("\n"); const result = csvToModule(csv, { emitTypes: true, resourceName: "orders", }); expect(result.dts).toContain("declare function getData(): ordersTable"); expect(result.dts).toContain("export default getData"); expect(result.dts).not.toContain("declare const data"); }); it("should generate function type in dts for tables without references", () => { const csv = ["name,age", "string,number", "Alice,30"].join("\n"); const result = csvToModule(csv, { emitTypes: true, resourceName: "people", }); expect(result.dts).toContain("declare function getData(): peopleTable"); expect(result.dts).toContain("export default getData"); expect(result.dts).not.toContain("declare const data"); }); it("should handle nested references in tuples", () => { const csv = [ "id,info", "string,[ref: @users; note: string]", "1,[ref: 1; note: urgent]", ].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("import _users from './users.csv'"); expect(result.js).toContain("_usersLookup"); }); }); describe("csvToModule - circular reference support", () => { it("should emit accessor for self-referencing table without self-import", () => { const csv = readFixture("self_ref.csv"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "self_ref.csv"), }); expect(result.js).not.toContain("import _self_ref from './self_ref.csv'"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("_self_refLookup"); expect(result.js).toContain("_self_refLookup = new Map(_raw.map"); expect(result.js).toContain( "parent: _self_refLookup.get(String(row.parent))", ); }); it("should emit accessor for cross-referencing tables", () => { const csv = readFixture("circular_a.csv"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "circular_a.csv"), }); expect(result.js).toContain("import _circular_b from './circular_b.csv'"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("_circular_bLookup"); expect(result.js).toContain("related:"); }); it("should emit accessor for self-referencing table with nested 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 = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "self_ref.csv"), }); expect(result.js).not.toContain("import _self_ref from './self_ref.csv'"); expect(result.js).toContain("_self_refLookup"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("parent_info:"); expect(result.js).toContain( "_self_refLookup.get(String(row.parent_info[0]))", ); }); it("should emit accessor for self-referencing table with nested reference in union with fallback", () => { const csv = [ "id,name,ref_or_val", "string,string,@self_ref | string", "1,Root,2", "2,Child,none", ].join("\n"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "self_ref.csv"), }); expect(result.js).not.toContain("import _self_ref from './self_ref.csv'"); expect(result.js).toContain("_self_refLookup"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("ref_or_val:"); expect(result.js).toContain( "_self_refLookup.get(String(row.ref_or_val)) ?? row.ref_or_val", ); }); it("should emit accessor for self-referencing table with nested reference array in tuple", () => { const csv = [ "id,name,children", "string,string,[kids: @self_ref[]]", "1,Root,[[2]]", ].join("\n"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "self_ref.csv"), }); expect(result.js).not.toContain("import _self_ref from './self_ref.csv'"); expect(result.js).toContain("_self_refLookup"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("children:"); }); it("should generate correct type definition for self-referencing table using local singular type", () => { const csv = readFixture("self_ref.csv"); const result = csvToModule(csv, { emitTypes: true, resourceName: "nodes", currentFilePath: path.join(fixturesDir, "self_ref.csv"), }); expect(result.dts).toContain("declare function getData(): nodesTable"); expect(result.dts).toContain("readonly parent: Nodes"); expect(result.dts).not.toContain( "import type { Self_ref } from './self_ref.csv'", ); expect(result.dts).not.toContain("Self_ref"); }); it("should emit accessor for cross-referencing tables with array references", () => { const csv = readFixture("circular_a.csv"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "circular_a.csv"), }); expect(result.js).toContain("import _circular_b from './circular_b.csv'"); expect(result.js).toContain("export default function getData()"); expect(result.js).toContain("_circular_bLookup"); expect(result.js).toContain(".map(id =>"); }); it("should emit accessor for nested cross-reference in tuple", () => { const csv = [ "id,name,info", "string,string,[ref: @circular_b; note: string]", "1,A,[ref: 1; note: linked]", ].join("\n"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "circular_a.csv"), }); expect(result.js).toContain("import _circular_b from './circular_b.csv'"); expect(result.js).toContain("_circular_bLookup"); expect(result.js).toContain("export default function getData()"); }); it("should generate union fallback with ?? for non-reference union members", () => { const csv = ["id,value", "string,@users | string", "1,1", "2,unknown"].join( "\n", ); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("?? row.value"); }); }); describe("csvToModule - reverse reference output", () => { it("should emit reverse lookup code for reverse references", () => { const csv = [ "id,name", "string,string", "# orders := ~orders(customer)", "1,Alice", ].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("import _orders from './orders.csv'"); expect(result.js).toContain("_ordersBy_customer"); expect(result.js).toContain("orders:"); expect(result.js).toContain("_ordersBy_customer.get(String(row.id))"); }); it("should emit null fallback for optional reverse references", () => { const csv = [ "id,name", "string,string", "# orders := ~orders(customer)?", "1,Alice", ].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain( "_ordersBy_customer.get(String(row.id)) || null", ); }); it("should emit empty array fallback for non-optional reverse references", () => { const csv = [ "id,name", "string,string", "# orders := ~orders(customer)", "1,Alice", ].join("\n"); const result = csvToModule(csv, { emitTypes: false }); expect(result.js).toContain("_ordersBy_customer.get(String(row.id)) || []"); }); it("should handle self-referencing reverse reference without self-import", () => { const csv = [ "id,name", "string,string", "# children := ~self_ref(parent)", "1,Root", "2,Child", ].join("\n"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "self_ref.csv"), }); expect(result.js).not.toContain("import _self_ref from './self_ref.csv'"); expect(result.js).toContain("_self_refBy_parent"); expect(result.js).toContain("for (const r of _raw)"); expect(result.js).toContain("children:"); }); it("should generate correct type definition for reverse references", () => { const csv = [ "id,name", "string,string", "# orders := ~orders(customer)", "1,Alice", ].join("\n"); const result = csvToModule(csv, { emitTypes: true, resourceName: "users", currentFilePath: path.join(fixturesDir, "test.csv"), }); expect(result.dts).toContain("readonly orders: Orders[]"); expect(result.dts).toContain("import type { Orders }"); }); it("should generate nullable type for optional reverse references", () => { const csv = [ "id,name", "string,string", "# orders := ~orders(customer)?", "1,Alice", ].join("\n"); const result = csvToModule(csv, { emitTypes: true, resourceName: "users", currentFilePath: path.join(fixturesDir, "test.csv"), }); expect(result.dts).toContain("readonly orders: Orders[] | null"); }); it("should combine forward and reverse references to the same table", () => { const csv = [ "id,name,manager", "string,string,@users", "# reports := ~users(manager)", "1,Alice,2", "2,Bob,", ].join("\n"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "users.csv"), }); expect(result.js).toContain("_usersLookup"); expect(result.js).toContain("_usersBy_manager"); expect(result.js).not.toContain("import _users from './users.csv'"); }); it("should import referenced table when forward and reverse reference a different table", () => { const csv = [ "id,name,creator", "string,string,@users", "# reviews := ~users(reviewer)", "1,Doc,1", ].join("\n"); const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, "test.csv"), }); expect(result.js).toContain("_usersLookup"); expect(result.js).toContain("_usersBy_reviewer"); expect(result.js).toContain("import _users from './users.csv'"); }); });