968 lines
29 KiB
TypeScript
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');
|
|
});
|
|
}); |