import * as path from "path"; import { parseCsv } from "./loader.js"; import { hasNestedReferences } from "./reference-resolver.js"; import type { Schema } from "../types.js"; import type { CsvLoaderOptions, PropertyConfig } from "./types.js"; /** * 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, reverseLookupVar?: (tableName: string, foreignKey: string) => string, ): string { switch (schema.type) { case "reference": { const lookup = lookupVar(schema.tableName); if (schema.isOptional) { if (schema.isArray) { return `(${valueExpr} === null || ${valueExpr} === undefined ? ${valueExpr} : (Array.isArray(${valueExpr}) ? ${valueExpr}.map(id => ${lookup}.get(String(id))) : ${lookup}.get(String(${valueExpr}))))`; } return `(${valueExpr} === null || ${valueExpr} === undefined ? ${valueExpr} : ${lookup}.get(String(${valueExpr})))`; } if (schema.isArray) { return `(Array.isArray(${valueExpr}) ? ${valueExpr}.map(id => ${lookup}.get(String(id))) : ${valueExpr})`; } return `${lookup}.get(String(${valueExpr}))`; } case "reverseReference": { if (!reverseLookupVar) return valueExpr; const reverseLookup = reverseLookupVar( schema.tableName, schema.foreignKey, ); if (schema.isOptional) { return `(${reverseLookup}.get(String(row.${pkField})) || null)`; } return `(${reverseLookup}.get(String(row.${pkField})) || [])`; } case "tuple": { const elementResolvers = schema.elements.map((el, i) => { if (hasNestedReferences(el.schema)) { return generateSchemaResolutionCode( el.schema, `${valueExpr}[${i}]`, lookupVar, pkField, reverseLookupVar, ); } return `${valueExpr}[${i}]`; }); return `[${elementResolvers.join(", ")}]`; } case "array": { if (hasNestedReferences(schema.element)) { const itemResolve = generateSchemaResolutionCode( schema.element, "item", lookupVar, pkField, reverseLookupVar, ); 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 resolveParts: string[] = []; for (const member of refMembers) { const resolveCode = generateSchemaResolutionCode( member, valueExpr, lookupVar, pkField, reverseLookupVar, ); resolveParts.push(resolveCode); } if (nonRefMembers.length > 0) { resolveParts.push(valueExpr); } if (resolveParts.length === 0) return valueExpr; if (resolveParts.length === 1) return resolveParts[0]; return `(${resolveParts.join(" ?? ")})`; } default: return valueExpr; } } /** * Generate JavaScript module code from CSV content. * 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, resolveReferences: false }); const hasRefs = result.referenceFields.length > 0 || result.reverseReferences.length > 0; const defaultPrimaryKey = options.defaultPrimaryKey ?? "id"; const imports: string[] = []; const lookupInits: string[] = []; const lookupVarMap = new Map(); // Reverse lookup maps: grouped by (tableName, foreignKey) const reverseLookupInits: string[] = []; const reverseLookupVarMap = new Map(); const currentTableName = options.currentFilePath ? path.basename( options.currentFilePath, path.extname(options.currentFilePath), ) : undefined; // Build forward lookup maps for referenced tables const uniqueTables = new Set(result.referenceFields.map((f) => f.tableName)); // Also include tables from reverse references for (const decl of result.reverseReferences) { uniqueTables.add(decl.tableName); } uniqueTables.forEach((tableName) => { const lookupVar = `_${tableName}Lookup`; lookupVarMap.set(tableName, lookupVar); if (tableName === currentTableName) { lookupInits.push( `const ${lookupVar} = new Map(_raw.map(p => [String(p.${defaultPrimaryKey}), p]));`, ); } else { const varName = `_${tableName}`; imports.push(`import ${varName} from './${tableName}.csv';`); lookupInits.push( `const ${lookupVar} = new Map(${varName}().map(p => [String(p.${defaultPrimaryKey}), p]));`, ); } }); // Build reverse lookup maps for reverse references for (const decl of result.reverseReferences) { const key = `${decl.tableName}:${decl.foreignKey}`; if (reverseLookupVarMap.has(key)) continue; const revLookupVar = `_${decl.tableName}By_${decl.foreignKey}`; reverseLookupVarMap.set(key, revLookupVar); if (decl.tableName === currentTableName) { reverseLookupInits.push( `const ${revLookupVar} = new Map();`, `for (const r of _raw) {`, ` const kv = r.${decl.foreignKey};`, ` const k = String(typeof kv === "object" && "${defaultPrimaryKey}" in kv ? kv.${defaultPrimaryKey} : kv);`, ` if (!${revLookupVar}.has(k)) ${revLookupVar}.set(k, []);`, ` ${revLookupVar}.get(k).push(r);`, `}`, ); } else { const varName = `_${decl.tableName}`; reverseLookupInits.push( `const ${revLookupVar} = new Map();`, `for (const r of ${varName}()) {`, ` const kv = r.${decl.foreignKey};`, ` const k = String(typeof kv === "object" && "${defaultPrimaryKey}" in kv ? kv.${defaultPrimaryKey} : kv);`, ` if (!${revLookupVar}.has(k)) ${revLookupVar}.set(k, []);`, ` ${revLookupVar}.get(k).push(r);`, `}`, ); } } const lookupVar = (tableName: string) => lookupVarMap.get(tableName)!; const reverseLookupVar = (tableName: string, foreignKey: string) => reverseLookupVarMap.get(`${tableName}:${foreignKey}`)!; const rowResolvers: string[] = []; for (const config of result.propertyConfigs) { if (config.isReverseReference) { // Reverse reference resolution const decl = result.reverseReferences.find( (d) => d.fieldName === config.name, ); if (decl) { const revLookup = reverseLookupVar(decl.tableName, decl.foreignKey); if (decl.isOptional) { rowResolvers.push( ` ${config.name}: (${revLookup}.get(String(row.${defaultPrimaryKey})) || null),`, ); } else { rowResolvers.push( ` ${config.name}: (${revLookup}.get(String(row.${defaultPrimaryKey})) || []),`, ); } } } else if (hasNestedReferences(config.schema)) { const resolveCode = generateSchemaResolutionCode( config.schema, `row.${config.name}`, lookupVar, defaultPrimaryKey, reverseLookupVar, ); rowResolvers.push(` ${config.name}: ${resolveCode},`); } } const rawJson = JSON.stringify(result.data, null, 2); const js = [ ...imports, "", `const _raw = ${rawJson};`, "", "let _resolved = null;", "", "export default function getData() {", " if (_resolved) return _resolved;", " _resolved = _raw;", ...lookupInits.map((l) => ` ${l}`), ...reverseLookupInits.map((l) => ` ${l}`), ...(rowResolvers.length > 0 ? [ " _resolved = _raw.map(row => (", ...rowResolvers.map((r) => ` row${r.slice(1)}`), " row));", ] : []), " return _resolved;", "}", ].join("\n"); return { js, dts: result.typeDefinition, }; }