test: add comprehensive tests for csvToModule
- Add unit tests for accessor-based output, circular references, and reverse reference resolution in `csvToModule`. - Extract fixture loading logic into `test-utils.ts`. - Refactor `loader.test.ts` to use the new test utilities.
This commit is contained in:
parent
f94e9b68e4
commit
f66f60aa0e
|
|
@ -1,14 +1,8 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { parseCsv } from "./loader";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { csvToModule } from "./module-gen";
|
||||
|
||||
const fixturesDir = path.join(__dirname, "fixtures");
|
||||
|
||||
function readFixture(name: string): string {
|
||||
return fs.readFileSync(path.join(fixturesDir, name), "utf-8");
|
||||
}
|
||||
import { fixturesDir, readFixture } from "./test-utils";
|
||||
import fs from "fs";
|
||||
|
||||
describe("parseCsv - basic parsing", () => {
|
||||
it("should parse a simple CSV with primitive types", () => {
|
||||
|
|
@ -770,245 +764,6 @@ describe("parseCsv - resolveReferences: false", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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("parseCsv - reverse reference resolution", () => {
|
||||
it("should resolve reverse reference from comment declaration", () => {
|
||||
// Create a temporary orders CSV with a plain string foreign key
|
||||
|
|
@ -1281,149 +1036,3 @@ describe("parseCsv - reverse reference with resolveReferences: false", () => {
|
|||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
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", () => {
|
||||
// When the current table references itself via both @ and ~,
|
||||
// no self-import is needed
|
||||
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"),
|
||||
});
|
||||
|
||||
// Forward reference uses lookup built from _raw (self)
|
||||
expect(result.js).toContain("_usersLookup");
|
||||
// Reverse reference uses reverse lookup built from _raw (self)
|
||||
expect(result.js).toContain("_usersBy_manager");
|
||||
// No self-import since the current file IS users.csv
|
||||
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"),
|
||||
});
|
||||
|
||||
// Forward reference uses lookup
|
||||
expect(result.js).toContain("_usersLookup");
|
||||
// Reverse reference uses reverse lookup
|
||||
expect(result.js).toContain("_usersBy_reviewer");
|
||||
// users is a different table, so it should be imported
|
||||
expect(result.js).toContain("import _users from './users.csv'");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,381 @@
|
|||
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'");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
export const fixturesDir = path.join(__dirname, "fixtures");
|
||||
|
||||
export function readFixture(name: string): string {
|
||||
return fs.readFileSync(path.join(fixturesDir, name), "utf-8");
|
||||
}
|
||||
Loading…
Reference in New Issue