fix: bug fixes and new tests
This commit is contained in:
+193
-61
@@ -4,6 +4,186 @@ import type { Schema, ReferenceSchema } from '../types.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
function hasNestedReferences(schema: Schema): boolean {
|
||||
switch (schema.type) {
|
||||
case 'reference':
|
||||
return true;
|
||||
case 'tuple':
|
||||
return schema.elements.some(el => hasNestedReferences(el.schema));
|
||||
case 'array':
|
||||
return hasNestedReferences(schema.element);
|
||||
case 'union':
|
||||
return schema.members.some(m => hasNestedReferences(m));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadReferenceTable(
|
||||
schema: ReferenceSchema,
|
||||
refBaseDir: string | undefined,
|
||||
defaultPrimaryKey: string,
|
||||
currentFilePath: string | undefined
|
||||
): { lookup: Map<string, Record<string, unknown>>; refTable: Record<string, unknown>[] } {
|
||||
const baseDir = refBaseDir || (currentFilePath ? path.dirname(currentFilePath) : process.cwd());
|
||||
const fileName = `${schema.tableName}.csv`;
|
||||
const refFilePath = path.isAbsolute(fileName)
|
||||
? fileName
|
||||
: path.join(baseDir, fileName);
|
||||
|
||||
let refTable: Record<string, unknown>[];
|
||||
if (referenceTableCache.has(refFilePath)) {
|
||||
refTable = referenceTableCache.get(refFilePath)!;
|
||||
} else {
|
||||
if (loadingFiles.has(refFilePath)) {
|
||||
throw new Error(
|
||||
`Circular reference detected: table "${schema.tableName}" (${refFilePath}) is already being loaded`
|
||||
);
|
||||
}
|
||||
loadingFiles.add(refFilePath);
|
||||
try {
|
||||
const refContent = fs.readFileSync(refFilePath, 'utf-8');
|
||||
const refResult = parseCsv(refContent, {
|
||||
currentFilePath: refFilePath,
|
||||
emitTypes: false,
|
||||
});
|
||||
refTable = refResult.data;
|
||||
referenceTableCache.set(refFilePath, refTable);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load referenced table "${schema.tableName}" from ${refFilePath}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
} finally {
|
||||
loadingFiles.delete(refFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
const lookup = new Map<string, Record<string, unknown>>();
|
||||
refTable.forEach(row => {
|
||||
const pkValue = row[defaultPrimaryKey];
|
||||
if (pkValue !== undefined) {
|
||||
lookup.set(String(pkValue), row);
|
||||
}
|
||||
});
|
||||
|
||||
return { lookup, refTable };
|
||||
}
|
||||
|
||||
function resolveReferenceId(
|
||||
id: string,
|
||||
lookup: Map<string, Record<string, unknown>>,
|
||||
tableName: string
|
||||
): Record<string, unknown> {
|
||||
const obj = lookup.get(id);
|
||||
if (!obj) {
|
||||
throw new Error(`Reference to "${tableName}" with id="${id}" not found`);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function parseValueWithReferences(
|
||||
valueString: string,
|
||||
schema: Schema,
|
||||
refBaseDir: string | undefined,
|
||||
defaultPrimaryKey: string,
|
||||
currentFilePath: string | undefined
|
||||
): unknown {
|
||||
if (!hasNestedReferences(schema)) {
|
||||
return parseValue(schema, valueString);
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case 'reference':
|
||||
return parseReferenceValue(schema, valueString, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
case 'tuple': {
|
||||
const parsed = parseValue(schema, valueString) as unknown[];
|
||||
return schema.elements.map((el, i) =>
|
||||
resolveNestedReferences(parsed[i], el.schema, refBaseDir, defaultPrimaryKey, currentFilePath)
|
||||
);
|
||||
}
|
||||
case 'array': {
|
||||
const parsed = parseValue(schema, valueString) as unknown[];
|
||||
return parsed.map(item =>
|
||||
resolveNestedReferences(item, schema.element, refBaseDir, defaultPrimaryKey, currentFilePath)
|
||||
);
|
||||
}
|
||||
case 'union': {
|
||||
const errors: Error[] = [];
|
||||
for (const member of schema.members) {
|
||||
if (hasNestedReferences(member)) {
|
||||
try {
|
||||
const parsed = parseValue(member, valueString);
|
||||
return resolveNestedReferences(parsed, member, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
} catch (e) {
|
||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0 && errors.every(e => /not found|Circular reference|Failed to load/.test(e.message))) {
|
||||
for (const member of schema.members) {
|
||||
if (!hasNestedReferences(member)) {
|
||||
try {
|
||||
return parseValue(member, valueString);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseValue(schema, valueString);
|
||||
}
|
||||
default:
|
||||
return parseValue(schema, valueString);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveNestedReferences(
|
||||
value: unknown,
|
||||
schema: Schema,
|
||||
refBaseDir: string | undefined,
|
||||
defaultPrimaryKey: string,
|
||||
currentFilePath: string | undefined
|
||||
): unknown {
|
||||
switch (schema.type) {
|
||||
case 'reference': {
|
||||
const { lookup } = loadReferenceTable(schema, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
if (schema.isArray) {
|
||||
const ids = Array.isArray(value) ? value : [value];
|
||||
return ids.map(id => resolveReferenceId(String(id), lookup, schema.tableName));
|
||||
}
|
||||
return resolveReferenceId(String(value), lookup, schema.tableName);
|
||||
}
|
||||
case 'tuple': {
|
||||
if (!Array.isArray(value)) return value;
|
||||
return schema.elements.map((el, i) =>
|
||||
resolveNestedReferences(value[i], el.schema, refBaseDir, defaultPrimaryKey, currentFilePath)
|
||||
);
|
||||
}
|
||||
case 'array': {
|
||||
if (!Array.isArray(value)) return value;
|
||||
return value.map(item =>
|
||||
resolveNestedReferences(item, schema.element, refBaseDir, defaultPrimaryKey, currentFilePath)
|
||||
);
|
||||
}
|
||||
case 'union': {
|
||||
const errors: Error[] = [];
|
||||
for (const member of schema.members) {
|
||||
if (hasNestedReferences(member)) {
|
||||
try {
|
||||
return resolveNestedReferences(value, member, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
} catch (e) {
|
||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw errors[0];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CsvLoaderOptions {
|
||||
delimiter?: string;
|
||||
quote?: string;
|
||||
@@ -50,7 +230,10 @@ interface PropertyConfig {
|
||||
}
|
||||
|
||||
/** Cache for loaded referenced tables */
|
||||
const referenceTableCache = new Map<string, Record<string, unknown>[]>();
|
||||
const referenceTableCache = new Map<string, Record<string,unknown>[]>();
|
||||
|
||||
/** Set of file paths currently being loaded (to detect circular references) */
|
||||
const loadingFiles = new Set<string>();
|
||||
|
||||
/**
|
||||
* Parse and resolve a reference value.
|
||||
@@ -63,70 +246,16 @@ function parseReferenceValue(
|
||||
defaultPrimaryKey: string,
|
||||
currentFilePath: string | undefined
|
||||
): unknown {
|
||||
// Determine the directory to search for referenced files
|
||||
const baseDir = refBaseDir || (currentFilePath ? path.dirname(currentFilePath) : process.cwd());
|
||||
const { lookup } = loadReferenceTable(schema, refBaseDir, defaultPrimaryKey, currentFilePath);
|
||||
|
||||
// Build the referenced file path
|
||||
const fileName = `${schema.tableName}.csv`;
|
||||
const refFilePath = path.isAbsolute(fileName)
|
||||
? fileName
|
||||
: path.join(baseDir, fileName);
|
||||
|
||||
// Load the referenced table (use cache if already loaded)
|
||||
let refTable: Record<string, unknown>[];
|
||||
if (referenceTableCache.has(refFilePath)) {
|
||||
refTable = referenceTableCache.get(refFilePath)!;
|
||||
} else {
|
||||
try {
|
||||
const refContent = fs.readFileSync(refFilePath, 'utf-8');
|
||||
const refResult = parseCsv(refContent, {
|
||||
currentFilePath: refFilePath,
|
||||
emitTypes: false,
|
||||
});
|
||||
refTable = refResult.data;
|
||||
referenceTableCache.set(refFilePath, refTable);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load referenced table "${schema.tableName}" from ${refFilePath}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a lookup map by primary key
|
||||
const primaryKeyMap = new Map<string, Record<string, unknown>>();
|
||||
refTable.forEach(row => {
|
||||
const pkValue = row[defaultPrimaryKey];
|
||||
if (pkValue !== undefined) {
|
||||
primaryKeyMap.set(String(pkValue), row);
|
||||
}
|
||||
});
|
||||
|
||||
// Parse the value string to extract IDs
|
||||
const valueParser = new ReferenceValueParser(valueString.trim());
|
||||
const ids = valueParser.parseIds(schema.isArray);
|
||||
|
||||
// Resolve IDs to actual objects
|
||||
if (schema.isArray) {
|
||||
return ids.map(id => {
|
||||
const obj = primaryKeyMap.get(id);
|
||||
if (!obj) {
|
||||
throw new Error(
|
||||
`Reference to "${schema.tableName}" with ${defaultPrimaryKey}="${id}" not found`
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
} else {
|
||||
// Single reference (first ID if array provided)
|
||||
const id = ids[0];
|
||||
const obj = primaryKeyMap.get(id);
|
||||
if (!obj) {
|
||||
throw new Error(
|
||||
`Reference to "${schema.tableName}" with ${defaultPrimaryKey}="${id}" not found`
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
return ids.map(id => resolveReferenceId(id, lookup, schema.tableName));
|
||||
}
|
||||
|
||||
return resolveReferenceId(ids[0], lookup, schema.tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,15 +510,18 @@ export function parseCsv(
|
||||
parser: (valueString: string) => parseValue(schema, valueString),
|
||||
};
|
||||
|
||||
// Check if it's a reference type
|
||||
if (schema.type === 'reference') {
|
||||
config.isReference = true;
|
||||
config.referenceTableName = schema.tableName;
|
||||
config.referenceIsArray = schema.isArray;
|
||||
// Override parser for reference fields
|
||||
config.parser = (valueString: string) => {
|
||||
return parseReferenceValue(schema, valueString, refBaseDir, defaultPrimaryKey, options.currentFilePath);
|
||||
};
|
||||
} else if (hasNestedReferences(schema)) {
|
||||
config.isReference = true;
|
||||
config.parser = (valueString: string) => {
|
||||
return parseValueWithReferences(valueString, schema, refBaseDir, defaultPrimaryKey, options.currentFilePath);
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
Reference in New Issue
Block a user