inline-schema/src/csv-loader/loader.test.ts

968 lines
29 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { parseCsv, csvToModule } from './loader';
import * as path from 'path';
import * as 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', () => {
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 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');
});
});
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);
}
});
});
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' });
});
});
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);
});
});
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');
});
});
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');
});
});
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',
});
});
});
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');
});
});
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');
});
});