Compare commits
2 Commits
f66f60aa0e
...
3e768f5c83
| Author | SHA1 | Date |
|---|---|---|
|
|
3e768f5c83 | |
|
|
d0ed1dc92d |
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +1,12 @@
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import {
|
import { parseValue } from "../index.js";
|
||||||
parseValue,
|
|
||||||
} from "../index.js";
|
|
||||||
import type {
|
import type {
|
||||||
Schema,
|
Schema,
|
||||||
ReferenceSchema,
|
ReferenceSchema,
|
||||||
ReverseReferenceSchema,
|
ReverseReferenceSchema,
|
||||||
} from "../types.js";
|
} from "../types.js";
|
||||||
import type {
|
import type { ReferenceFieldInfo } from "./types.js";
|
||||||
ReferenceFieldInfo,
|
|
||||||
} from "./types.js";
|
|
||||||
import { parseCsv } from "./loader.js";
|
import { parseCsv } from "./loader.js";
|
||||||
|
|
||||||
/** Cache for loaded referenced tables */
|
/** Cache for loaded referenced tables */
|
||||||
|
|
@ -110,12 +106,7 @@ export function parseReferenceIds(
|
||||||
if (schema.isOptional && trimmed === "") {
|
if (schema.isOptional && trimmed === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const valueParser = new ReferenceValueParser(trimmed);
|
return parseValue(schema, trimmed);
|
||||||
const ids = valueParser.parseIds(schema.isArray);
|
|
||||||
if (schema.isArray) {
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
return ids[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseValueWithReferenceIds(
|
export function parseValueWithReferenceIds(
|
||||||
|
|
@ -164,7 +155,10 @@ export function parseValueWithReferenceIds(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractNestedReferenceIds(value: unknown, schema: Schema): unknown {
|
export function extractNestedReferenceIds(
|
||||||
|
value: unknown,
|
||||||
|
schema: Schema,
|
||||||
|
): unknown {
|
||||||
switch (schema.type) {
|
switch (schema.type) {
|
||||||
case "reference":
|
case "reference":
|
||||||
if (value === null || value === undefined) return value;
|
if (value === null || value === undefined) return value;
|
||||||
|
|
@ -477,101 +471,12 @@ export function parseReferenceValue(
|
||||||
currentFilePath,
|
currentFilePath,
|
||||||
);
|
);
|
||||||
|
|
||||||
const valueParser = new ReferenceValueParser(trimmed);
|
const ids = parseValue(schema, trimmed) as string | string[] | null;
|
||||||
const ids = valueParser.parseIds(schema.isArray);
|
if (ids === null) return null;
|
||||||
|
|
||||||
if (schema.isArray) {
|
if (schema.isArray && Array.isArray(ids)) {
|
||||||
return ids.map((id) => resolveReferenceId(id, lookup, schema.tableName));
|
return ids.map((id) => resolveReferenceId(id, lookup, schema.tableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveReferenceId(ids[0], lookup, schema.tableName);
|
return resolveReferenceId(ids as string, lookup, schema.tableName);
|
||||||
}
|
|
||||||
|
|
||||||
class ReferenceValueParser {
|
|
||||||
private input: string;
|
|
||||||
private pos: number = 0;
|
|
||||||
|
|
||||||
constructor(input: string) {
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
private peek(): string {
|
|
||||||
return this.input[this.pos] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private consume(): string {
|
|
||||||
return this.input[this.pos++] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private skipWhitespace(): void {
|
|
||||||
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
|
|
||||||
this.pos++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private consumeStr(str: string): boolean {
|
|
||||||
if (this.input.slice(this.pos, this.pos + str.length) === str) {
|
|
||||||
this.pos += str.length;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseIds(isArray: boolean): string[] {
|
|
||||||
this.skipWhitespace();
|
|
||||||
|
|
||||||
if (isArray) {
|
|
||||||
// Parse array format: [id1; id2; id3]
|
|
||||||
if (this.peek() === "[") {
|
|
||||||
this.consume();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skipWhitespace();
|
|
||||||
|
|
||||||
if (this.peek() === "]") {
|
|
||||||
this.consume();
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids: string[] = [];
|
|
||||||
while (true) {
|
|
||||||
this.skipWhitespace();
|
|
||||||
let id = "";
|
|
||||||
while (
|
|
||||||
this.pos < this.input.length &&
|
|
||||||
this.peek() !== ";" &&
|
|
||||||
this.peek() !== "]"
|
|
||||||
) {
|
|
||||||
id += this.consume();
|
|
||||||
}
|
|
||||||
const trimmedId = id.trim();
|
|
||||||
if (trimmedId) {
|
|
||||||
ids.push(trimmedId);
|
|
||||||
}
|
|
||||||
this.skipWhitespace();
|
|
||||||
|
|
||||||
if (!this.consumeStr(";")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skipWhitespace();
|
|
||||||
if (this.peek() === "]") {
|
|
||||||
this.consume();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
} else {
|
|
||||||
// Parse single ID
|
|
||||||
let id = "";
|
|
||||||
while (this.pos < this.input.length) {
|
|
||||||
const char = this.peek();
|
|
||||||
if (char === ";" || char === "]" || char === ",") {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
id += this.consume();
|
|
||||||
}
|
|
||||||
return [id.trim()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue