Compare commits
No commits in common. "3e768f5c833a138ca4a83b5b737d49b028dcb25b" and "f66f60aa0e5c25d8783126c477f4da2d7c8197ed" have entirely different histories.
3e768f5c83
...
f66f60aa0e
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,16 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { parseValue } from "../index.js";
|
||||
import {
|
||||
parseValue,
|
||||
} from "../index.js";
|
||||
import type {
|
||||
Schema,
|
||||
ReferenceSchema,
|
||||
ReverseReferenceSchema,
|
||||
} from "../types.js";
|
||||
import type { ReferenceFieldInfo } from "./types.js";
|
||||
import type {
|
||||
ReferenceFieldInfo,
|
||||
} from "./types.js";
|
||||
import { parseCsv } from "./loader.js";
|
||||
|
||||
/** Cache for loaded referenced tables */
|
||||
|
|
@ -106,7 +110,12 @@ export function parseReferenceIds(
|
|||
if (schema.isOptional && trimmed === "") {
|
||||
return null;
|
||||
}
|
||||
return parseValue(schema, trimmed);
|
||||
const valueParser = new ReferenceValueParser(trimmed);
|
||||
const ids = valueParser.parseIds(schema.isArray);
|
||||
if (schema.isArray) {
|
||||
return ids;
|
||||
}
|
||||
return ids[0];
|
||||
}
|
||||
|
||||
export function parseValueWithReferenceIds(
|
||||
|
|
@ -155,10 +164,7 @@ export function parseValueWithReferenceIds(
|
|||
}
|
||||
}
|
||||
|
||||
export function extractNestedReferenceIds(
|
||||
value: unknown,
|
||||
schema: Schema,
|
||||
): unknown {
|
||||
export function extractNestedReferenceIds(value: unknown, schema: Schema): unknown {
|
||||
switch (schema.type) {
|
||||
case "reference":
|
||||
if (value === null || value === undefined) return value;
|
||||
|
|
@ -471,12 +477,101 @@ export function parseReferenceValue(
|
|||
currentFilePath,
|
||||
);
|
||||
|
||||
const ids = parseValue(schema, trimmed) as string | string[] | null;
|
||||
if (ids === null) return null;
|
||||
const valueParser = new ReferenceValueParser(trimmed);
|
||||
const ids = valueParser.parseIds(schema.isArray);
|
||||
|
||||
if (schema.isArray && Array.isArray(ids)) {
|
||||
if (schema.isArray) {
|
||||
return ids.map((id) => resolveReferenceId(id, lookup, schema.tableName));
|
||||
}
|
||||
|
||||
return resolveReferenceId(ids as string, lookup, schema.tableName);
|
||||
return resolveReferenceId(ids[0], 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()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
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