inline-schema/src/csv-loader/module-gen.ts

253 lines
8.2 KiB
TypeScript
Raw Normal View History

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<string, string>();
// Reverse lookup maps: grouped by (tableName, foreignKey)
const reverseLookupInits: string[] = [];
const reverseLookupVarMap = new Map<string, string>();
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: { name: string; code: 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({
name: config.name,
code: `(${revLookup}.get(String(row.${defaultPrimaryKey})) || null)`,
});
} else {
rowResolvers.push({
name: config.name,
code: `(${revLookup}.get(String(row.${defaultPrimaryKey})) || [])`,
});
}
}
} else if (hasNestedReferences(config.schema)) {
const resolveCode = generateSchemaResolutionCode(
config.schema,
`row.${config.name}`,
lookupVar,
defaultPrimaryKey,
reverseLookupVar,
);
rowResolvers.push({ name: config.name, code: 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.name} = ${r.code};`),
" return row;",
" });",
]
: []),
" return _resolved;",
"}",
].join("\n");
return {
js,
dts: result.typeDefinition,
};
}