2026-04-20 00:48:01 +08:00
|
|
|
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: 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
|
|
|
|
|
? [
|
2026-04-20 15:49:41 +08:00
|
|
|
" _resolved = _raw.map(row => (",
|
|
|
|
|
...rowResolvers.map((r) => ` row${r.slice(1)}`),
|
|
|
|
|
" row));",
|
2026-04-20 00:48:01 +08:00
|
|
|
]
|
|
|
|
|
: []),
|
|
|
|
|
" return _resolved;",
|
|
|
|
|
"}",
|
|
|
|
|
].join("\n");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
js,
|
|
|
|
|
dts: result.typeDefinition,
|
|
|
|
|
};
|
|
|
|
|
}
|