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[]; 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[]; 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[]; 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[]; 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: readonly 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; const reviewer = result.data[0].reviewer as Record; 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 static JSON 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'); expect(result.js).not.toContain('import '); expect(result.js).not.toContain('getData'); }); 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 const 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 const data: peopleTable'); expect(result.dts).toContain('export default data'); expect(result.dts).not.toContain('declare function getData'); }); 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'); }); });