refactor: accessor based imports
This commit is contained in:
+298
-21
@@ -81,6 +81,120 @@ function resolveReferenceId(
|
||||
return obj;
|
||||
}
|
||||
|
||||
function parseReferenceIds(schema: ReferenceSchema, valueString: string): unknown {
|
||||
const valueParser = new ReferenceValueParser(valueString.trim());
|
||||
const ids = valueParser.parseIds(schema.isArray);
|
||||
if (schema.isArray) {
|
||||
return ids;
|
||||
}
|
||||
return ids[0];
|
||||
}
|
||||
|
||||
function parseValueWithReferenceIds(
|
||||
valueString: string,
|
||||
schema: Schema
|
||||
): unknown {
|
||||
if (!hasNestedReferences(schema)) {
|
||||
return parseValue(schema, valueString);
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case 'reference':
|
||||
return parseReferenceIds(schema, valueString);
|
||||
case 'tuple': {
|
||||
const parsed = parseValue(schema, valueString) as unknown[];
|
||||
return schema.elements.map((el, i) =>
|
||||
hasNestedReferences(el.schema)
|
||||
? extractNestedReferenceIds(parsed[i], el.schema)
|
||||
: parsed[i]
|
||||
);
|
||||
}
|
||||
case 'array': {
|
||||
const parsed = parseValue(schema, valueString) as unknown[];
|
||||
return parsed.map(item =>
|
||||
hasNestedReferences(schema.element)
|
||||
? extractNestedReferenceIds(item, schema.element)
|
||||
: item
|
||||
);
|
||||
}
|
||||
case 'union': {
|
||||
for (const member of schema.members) {
|
||||
if (hasNestedReferences(member)) {
|
||||
try {
|
||||
const parsed = parseValue(member, valueString);
|
||||
return extractNestedReferenceIds(parsed, member);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return parseValue(schema, valueString);
|
||||
}
|
||||
default:
|
||||
return parseValue(schema, valueString);
|
||||
}
|
||||
}
|
||||
|
||||
function extractNestedReferenceIds(value: unknown, schema: Schema): unknown {
|
||||
switch (schema.type) {
|
||||
case 'reference':
|
||||
if (schema.isArray) {
|
||||
const ids = Array.isArray(value) ? value : [value];
|
||||
return ids.map(id => String(id));
|
||||
}
|
||||
return String(value);
|
||||
case 'tuple': {
|
||||
if (!Array.isArray(value)) return value;
|
||||
return schema.elements.map((el, i) =>
|
||||
hasNestedReferences(el.schema)
|
||||
? extractNestedReferenceIds(value[i], el.schema)
|
||||
: value[i]
|
||||
);
|
||||
}
|
||||
case 'array': {
|
||||
if (!Array.isArray(value)) return value;
|
||||
return value.map(item =>
|
||||
hasNestedReferences(schema.element)
|
||||
? extractNestedReferenceIds(item, schema.element)
|
||||
: item
|
||||
);
|
||||
}
|
||||
case 'union': {
|
||||
for (const member of schema.members) {
|
||||
if (hasNestedReferences(member)) {
|
||||
try {
|
||||
return extractNestedReferenceIds(value, member);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function collectReferenceFields(schema: Schema, name: string): ReferenceFieldInfo[] {
|
||||
const fields: ReferenceFieldInfo[] = [];
|
||||
switch (schema.type) {
|
||||
case 'reference':
|
||||
fields.push({ name, tableName: schema.tableName, isArray: schema.isArray, schema });
|
||||
break;
|
||||
case 'tuple':
|
||||
for (const el of schema.elements) {
|
||||
fields.push(...collectReferenceFields(el.schema, name));
|
||||
}
|
||||
break;
|
||||
case 'array':
|
||||
fields.push(...collectReferenceFields(schema.element, name));
|
||||
break;
|
||||
case 'union':
|
||||
for (const member of schema.members) {
|
||||
fields.push(...collectReferenceFields(member, name));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function parseValueWithReferences(
|
||||
valueString: string,
|
||||
schema: Schema,
|
||||
@@ -203,6 +317,23 @@ export interface CsvLoaderOptions {
|
||||
defaultPrimaryKey?: string;
|
||||
/** Current file path (used to resolve relative references) */
|
||||
currentFilePath?: string;
|
||||
/**
|
||||
* When false, reference fields store parsed IDs instead of resolved objects.
|
||||
* Used by csvToModule to emit accessor-based code with lazy resolution.
|
||||
* Default: true (resolves references eagerly by loading referenced CSV files).
|
||||
*/
|
||||
resolveReferences?: boolean;
|
||||
}
|
||||
|
||||
export interface ReferenceFieldInfo {
|
||||
/** Column name in the CSV */
|
||||
name: string;
|
||||
/** Referenced table name */
|
||||
tableName: string;
|
||||
/** Whether it's an array reference */
|
||||
isArray: boolean;
|
||||
/** The schema of this field (for nested references) */
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export interface CsvParseResult {
|
||||
@@ -214,6 +345,8 @@ export interface CsvParseResult {
|
||||
propertyConfigs: PropertyConfig[];
|
||||
/** Referenced table names */
|
||||
references: Set<string>;
|
||||
/** Reference field metadata (populated when resolveReferences is false) */
|
||||
referenceFields: ReferenceFieldInfo[];
|
||||
}
|
||||
|
||||
interface PropertyConfig {
|
||||
@@ -401,7 +534,8 @@ function generateTypeDefinition(
|
||||
resourceName: string,
|
||||
propertyConfigs: PropertyConfig[],
|
||||
references: Set<string>,
|
||||
currentFilePath?: string
|
||||
currentFilePath?: string,
|
||||
hasRefs?: boolean
|
||||
): string {
|
||||
const typeName = resourceName ? `${resourceName}Table` : 'Table';
|
||||
|
||||
@@ -432,17 +566,23 @@ function generateTypeDefinition(
|
||||
.map((config) => ` readonly ${config.name}: ${schemaToTypeString(config.schema, resourceNames)};`)
|
||||
.join('\n');
|
||||
|
||||
// Generate both the table type and export a singular type alias for references
|
||||
// e.g., for "parts" table, export both "partsTable" and "Parts" (as alias)
|
||||
let exportAlias = '';
|
||||
if (resourceName) {
|
||||
// Capitalize resource name for the singular type
|
||||
const singularType = resourceName.charAt(0).toUpperCase() + resourceName.slice(1);
|
||||
// Remove trailing 's' if it looks like a plural (simple heuristic)
|
||||
// Actually, let's just use the table name capitalized - users can adjust if needed
|
||||
exportAlias = `\nexport type ${singularType} = ${typeName}[number];`;
|
||||
}
|
||||
|
||||
if (hasRefs) {
|
||||
return `${importSection}type ${typeName} = readonly {
|
||||
${properties}
|
||||
}[];
|
||||
${exportAlias}
|
||||
|
||||
declare function getData(): ${typeName};
|
||||
export default getData;
|
||||
`;
|
||||
}
|
||||
|
||||
return `${importSection}type ${typeName} = readonly {
|
||||
${properties}
|
||||
}[];
|
||||
@@ -499,6 +639,8 @@ export function parseCsv(
|
||||
);
|
||||
}
|
||||
|
||||
const resolveReferences = options.resolveReferences ?? true;
|
||||
|
||||
const propertyConfigs: PropertyConfig[] = headers.map((header: string, index: number) => {
|
||||
const schemaString = schemas[index];
|
||||
const schema = parseSchema(schemaString);
|
||||
@@ -514,14 +656,26 @@ export function parseCsv(
|
||||
config.isReference = true;
|
||||
config.referenceTableName = schema.tableName;
|
||||
config.referenceIsArray = schema.isArray;
|
||||
config.parser = (valueString: string) => {
|
||||
return parseReferenceValue(schema, valueString, refBaseDir, defaultPrimaryKey, options.currentFilePath);
|
||||
};
|
||||
if (resolveReferences) {
|
||||
config.parser = (valueString: string) => {
|
||||
return parseReferenceValue(schema, valueString, refBaseDir, defaultPrimaryKey, options.currentFilePath);
|
||||
};
|
||||
} else {
|
||||
config.parser = (valueString: string) => {
|
||||
return parseReferenceIds(schema, valueString);
|
||||
};
|
||||
}
|
||||
} else if (hasNestedReferences(schema)) {
|
||||
config.isReference = true;
|
||||
config.parser = (valueString: string) => {
|
||||
return parseValueWithReferences(valueString, schema, refBaseDir, defaultPrimaryKey, options.currentFilePath);
|
||||
};
|
||||
if (resolveReferences) {
|
||||
config.parser = (valueString: string) => {
|
||||
return parseValueWithReferences(valueString, schema, refBaseDir, defaultPrimaryKey, options.currentFilePath);
|
||||
};
|
||||
} else {
|
||||
config.parser = (valueString: string) => {
|
||||
return parseValueWithReferenceIds(valueString, schema);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -573,10 +727,20 @@ export function parseCsv(
|
||||
return obj;
|
||||
});
|
||||
|
||||
const referenceFields: ReferenceFieldInfo[] = [];
|
||||
if (!resolveReferences) {
|
||||
for (const config of propertyConfigs) {
|
||||
if (hasNestedReferences(config.schema)) {
|
||||
referenceFields.push(...collectReferenceFields(config.schema, config.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: CsvParseResult = {
|
||||
data: objects,
|
||||
propertyConfigs,
|
||||
references,
|
||||
referenceFields,
|
||||
};
|
||||
|
||||
if (emitTypes) {
|
||||
@@ -584,29 +748,142 @@ export function parseCsv(
|
||||
options.resourceName || '',
|
||||
propertyConfigs,
|
||||
references,
|
||||
options.currentFilePath
|
||||
options.currentFilePath,
|
||||
referenceFields.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate runtime reference resolution code for a schema.
|
||||
* Returns a JS expression string that resolves references using lookup maps.
|
||||
*/
|
||||
function generateSchemaResolutionCode(
|
||||
schema: Schema,
|
||||
valueExpr: string,
|
||||
lookupVar: (tableName: string) => string,
|
||||
pkField: string
|
||||
): string {
|
||||
switch (schema.type) {
|
||||
case 'reference': {
|
||||
const lookup = lookupVar(schema.tableName);
|
||||
if (schema.isArray) {
|
||||
return `(Array.isArray(${valueExpr}) ? ${valueExpr}.map(id => ${lookup}.get(String(id))) : ${valueExpr})`;
|
||||
}
|
||||
return `${lookup}.get(String(${valueExpr}))`;
|
||||
}
|
||||
case 'tuple': {
|
||||
const elementResolvers = schema.elements.map((el, i) => {
|
||||
if (hasNestedReferences(el.schema)) {
|
||||
return generateSchemaResolutionCode(el.schema, `${valueExpr}[${i}]`, lookupVar, pkField);
|
||||
}
|
||||
return `${valueExpr}[${i}]`;
|
||||
});
|
||||
return `[${elementResolvers.join(', ')}]`;
|
||||
}
|
||||
case 'array': {
|
||||
if (hasNestedReferences(schema.element)) {
|
||||
const itemResolve = generateSchemaResolutionCode(schema.element, 'item', lookupVar, pkField);
|
||||
return `(${valueExpr}).map(item => ${itemResolve})`;
|
||||
}
|
||||
return valueExpr;
|
||||
}
|
||||
case 'union': {
|
||||
const refMembers = schema.members.filter(m => hasNestedReferences(m));
|
||||
const nonRefMembers = schema.members.filter(m => !hasNestedReferences(m));
|
||||
const parts: string[] = [];
|
||||
for (const member of refMembers) {
|
||||
const resolveCode = generateSchemaResolutionCode(member, valueExpr, lookupVar, pkField);
|
||||
parts.push(resolveCode);
|
||||
}
|
||||
if (nonRefMembers.length > 0) {
|
||||
parts.push(valueExpr);
|
||||
}
|
||||
return parts[0] || valueExpr;
|
||||
}
|
||||
default:
|
||||
return valueExpr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JavaScript module code from CSV content.
|
||||
* Returns a string that can be used as a module export.
|
||||
*
|
||||
* @param content - CSV content string
|
||||
* @param options - Parsing options
|
||||
* @returns JavaScript module code string
|
||||
* Emits an accessor function for tables with references (lazy resolution),
|
||||
* or static JSON for tables without references.
|
||||
*/
|
||||
export function csvToModule(
|
||||
content: string,
|
||||
options: CsvLoaderOptions & { resourceName?: string } = {}
|
||||
): { js: string; dts?: string } {
|
||||
const result = parseCsv(content, options);
|
||||
const result = parseCsv(content, { ...options, resolveReferences: false });
|
||||
|
||||
const json = JSON.stringify(result.data, null, 2);
|
||||
const js = `export default ${json};`;
|
||||
const hasRefs = result.referenceFields.length > 0;
|
||||
const defaultPrimaryKey = options.defaultPrimaryKey ?? 'id';
|
||||
|
||||
if (!hasRefs) {
|
||||
const json = JSON.stringify(result.data, null, 2);
|
||||
return {
|
||||
js: `export default ${json};`,
|
||||
dts: result.typeDefinition,
|
||||
};
|
||||
}
|
||||
|
||||
const imports: string[] = [];
|
||||
const lookupInits: string[] = [];
|
||||
const lookupVarMap = new Map<string, string>();
|
||||
|
||||
const uniqueTables = new Set(result.referenceFields.map(f => f.tableName));
|
||||
uniqueTables.forEach(tableName => {
|
||||
const varName = `_${tableName}`;
|
||||
lookupVarMap.set(tableName, `_${tableName}Lookup`);
|
||||
imports.push(`import ${varName} from './${tableName}.csv';`);
|
||||
lookupInits.push(
|
||||
`const _${tableName}Lookup = new Map(${varName}().map(p => [String(p.${defaultPrimaryKey}), p]));`
|
||||
);
|
||||
});
|
||||
|
||||
const lookupVar = (tableName: string) => lookupVarMap.get(tableName)!;
|
||||
|
||||
const rowResolvers: string[] = [];
|
||||
for (const config of result.propertyConfigs) {
|
||||
if (hasNestedReferences(config.schema)) {
|
||||
const resolveCode = generateSchemaResolutionCode(
|
||||
config.schema,
|
||||
`row.${config.name}`,
|
||||
lookupVar,
|
||||
defaultPrimaryKey
|
||||
);
|
||||
rowResolvers.push(` ${config.name}: ${resolveCode},`);
|
||||
}
|
||||
}
|
||||
|
||||
const rawJson = JSON.stringify(result.data, null, 2);
|
||||
|
||||
let js: string;
|
||||
if (rowResolvers.length > 0) {
|
||||
js = [
|
||||
...imports,
|
||||
'',
|
||||
`const _raw = ${rawJson};`,
|
||||
'',
|
||||
'let _resolved = null;',
|
||||
'',
|
||||
'export default function getData() {',
|
||||
' if (_resolved) return _resolved;',
|
||||
' _resolved = _raw;',
|
||||
...lookupInits.map(l => ` ${l}`),
|
||||
' _resolved = _raw.map(row => ({',
|
||||
' ...row,',
|
||||
...rowResolvers,
|
||||
' }));',
|
||||
' return _resolved;',
|
||||
'}',
|
||||
].join('\n');
|
||||
} else {
|
||||
js = `export default ${rawJson};`;
|
||||
}
|
||||
|
||||
return {
|
||||
js,
|
||||
|
||||
Reference in New Issue
Block a user