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 { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
import { parseCsv } from "./loader";
|
import { parseCsv } from "./loader";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs";
|
import { fixturesDir, readFixture } from "./test-utils";
|
||||||
import { csvToModule } from "./module-gen";
|
import fs from "fs";
|
||||||
|
|
||||||
const fixturesDir = path.join(__dirname, "fixtures");
|
|
||||||
|
|
||||||
function readFixture(name: string): string {
|
|
||||||
return fs.readFileSync(path.join(fixturesDir, name), "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("parseCsv - basic parsing", () => {
|
describe("parseCsv - basic parsing", () => {
|
||||||
it("should parse a simple CSV with primitive types", () => {
|
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", () => {
|
describe("parseCsv - reverse reference resolution", () => {
|
||||||
it("should resolve reverse reference from comment declaration", () => {
|
it("should resolve reverse reference from comment declaration", () => {
|
||||||
// Create a temporary orders CSV with a plain string foreign key
|
// Create a temporary orders CSV with a plain string foreign key
|
||||||
|
|
@ -1281,149 +1036,3 @@ describe("parseCsv - reverse reference with resolveReferences: false", () => {
|
||||||
).not.toThrow();
|
).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