- 支持一对多/多对一关系定义并生成到 Prisma schema - 简化流程:查询关联配置根据关系自动预填 - 修复 Handlebars 模板 HTML 转义导致的乱码问题 - 修复 controller 模板缺少 Prisma 导入的问题 - 新增页面模板 (page.hbs) 生成前端页面 - 添加 FindAllParams/PaginationQueryDto 索引签名修复类型兼容 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
341 lines
9.4 KiB
TypeScript
341 lines
9.4 KiB
TypeScript
/**
|
||
* 关联关系 DSL 解析器
|
||
*
|
||
* DSL 格式:
|
||
* - 关联关系: 关联名:目标模型 字段1,字段2,... [noList]
|
||
* - 多对多: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,...
|
||
* - 一对多: 关系名:目标模型 [optional]
|
||
*/
|
||
|
||
/**
|
||
* 关联关系配置(用于查询时 include)
|
||
*/
|
||
export interface RelationConfig {
|
||
/** 关联名,如 'class' */
|
||
name: string;
|
||
/** 目标模型 PascalCase,如 'Class' */
|
||
model: string;
|
||
/** 目标模型 camelCase,如 'class' */
|
||
modelCamel: string;
|
||
/** select 字段列表 */
|
||
selectFields: string[];
|
||
/** 是否在列表中包含(默认 true) */
|
||
includeInList: boolean;
|
||
}
|
||
|
||
/**
|
||
* 一对多关系配置(新模型包含多个目标模型)
|
||
* 例如:Dormitory 包含多个 Student
|
||
* - 新模型:students Student[]
|
||
* - 目标模型:dormitoryId String?, dormitory Dormitory?
|
||
*/
|
||
export interface OneToManyConfig {
|
||
/** 关系名(复数),如 'students' */
|
||
name: string;
|
||
/** 目标模型 PascalCase,如 'Student' */
|
||
targetModel: string;
|
||
/** 目标模型 camelCase,如 'student' */
|
||
target: string;
|
||
/** 外键字段名(在目标模型中),如 'dormitoryId' */
|
||
foreignKey: string;
|
||
/** 反向关联名(在目标模型中),如 'dormitory' */
|
||
backRelation: string;
|
||
/** 是否可选(外键是否可为 null),默认 true */
|
||
optional: boolean;
|
||
}
|
||
|
||
/**
|
||
* 多对一关系配置(新模型属于一个目标模型)
|
||
* 例如:Grade(成绩) 属于 Student
|
||
* - 新模型:studentId String, student Student
|
||
* - 目标模型:grades Grade[]
|
||
*/
|
||
export interface ManyToOneConfig {
|
||
/** 外键字段名,如 'studentId' */
|
||
foreignKey: string;
|
||
/** 关联名,如 'student' */
|
||
name: string;
|
||
/** 目标模型 PascalCase,如 'Student' */
|
||
targetModel: string;
|
||
/** 目标模型 camelCase,如 'student' */
|
||
target: string;
|
||
/** 反向关联名(在目标模型中,复数),如 'grades' */
|
||
backRelation: string;
|
||
/** 是否可选,默认 false */
|
||
optional: boolean;
|
||
}
|
||
|
||
/**
|
||
* 多对多关系<E585B3><E7B3BB><EFBFBD>置
|
||
*/
|
||
export interface ManyToManyConfig {
|
||
/** 关系名,如 'teachers' */
|
||
name: string;
|
||
/** 中间表名,如 'classTeacher' */
|
||
through: string;
|
||
/** 当前实体外键,如 'classId' */
|
||
foreignKey: string;
|
||
/** 目标实体外键,如 'teacherId' */
|
||
targetKey: string;
|
||
/** 目标模型 camelCase,如 'teacher' */
|
||
target: string;
|
||
/** 目标模型 PascalCase,如 'Teacher' */
|
||
targetModel: string;
|
||
/** 目标实体 select 字段列表 */
|
||
selectFields: string[];
|
||
}
|
||
|
||
/**
|
||
* 解析关联关系 DSL
|
||
*
|
||
* @example
|
||
* 输入:
|
||
* ```
|
||
* class:Class id,code,name
|
||
* headTeacher:Teacher id,teacherNo,name,subject noList
|
||
* ```
|
||
*
|
||
* 输出:
|
||
* [
|
||
* { name: 'class', model: 'Class', selectFields: ['id', 'code', 'name'], includeInList: true },
|
||
* { name: 'headTeacher', model: 'Teacher', selectFields: ['id', 'teacherNo', 'name', 'subject'], includeInList: false },
|
||
* ]
|
||
*/
|
||
export function parseRelations(dsl: string): RelationConfig[] {
|
||
const lines = dsl
|
||
.split('\n')
|
||
.map((line) => line.trim())
|
||
.filter((line) => line && !line.startsWith('#'));
|
||
|
||
const relations: RelationConfig[] = [];
|
||
|
||
for (const line of lines) {
|
||
// 匹配格式: 关联名:目标模型 字段1,字段2,... [noList]
|
||
const match = line.match(/^(\w+):(\w+)\s+([\w,]+)(?:\s+(noList))?$/);
|
||
if (!match) {
|
||
console.warn(`无法解析关联配置行: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
const [, name, model, fieldsStr, noListFlag] = match;
|
||
const selectFields = fieldsStr.split(',').map((f) => f.trim());
|
||
|
||
relations.push({
|
||
name,
|
||
model,
|
||
modelCamel: model.charAt(0).toLowerCase() + model.slice(1),
|
||
selectFields,
|
||
includeInList: !noListFlag,
|
||
});
|
||
}
|
||
|
||
return relations;
|
||
}
|
||
|
||
/**
|
||
* 解析多对多关系 DSL
|
||
*
|
||
* @example
|
||
* 输入:
|
||
* ```
|
||
* teachers:classTeacher:classId:teacherId:Teacher id,teacherNo,name,subject
|
||
* ```
|
||
*
|
||
* 输出:
|
||
* [
|
||
* {
|
||
* name: 'teachers',
|
||
* through: 'classTeacher',
|
||
* foreignKey: 'classId',
|
||
* targetKey: 'teacherId',
|
||
* target: 'teacher',
|
||
* targetModel: 'Teacher',
|
||
* selectFields: ['id', 'teacherNo', 'name', 'subject'],
|
||
* },
|
||
* ]
|
||
*/
|
||
export function parseManyToMany(dsl: string): ManyToManyConfig[] {
|
||
const lines = dsl
|
||
.split('\n')
|
||
.map((line) => line.trim())
|
||
.filter((line) => line && !line.startsWith('#'));
|
||
|
||
const configs: ManyToManyConfig[] = [];
|
||
|
||
for (const line of lines) {
|
||
// 匹配格式: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,...
|
||
const match = line.match(/^(\w+):(\w+):(\w+):(\w+):(\w+)\s+([\w,]+)$/);
|
||
if (!match) {
|
||
console.warn(`无法解析多对多配置行: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
const [, name, through, foreignKey, targetKey, targetModel, fieldsStr] = match;
|
||
const selectFields = fieldsStr.split(',').map((f) => f.trim());
|
||
|
||
configs.push({
|
||
name,
|
||
through,
|
||
foreignKey,
|
||
targetKey,
|
||
target: targetModel.charAt(0).toLowerCase() + targetModel.slice(1),
|
||
targetModel,
|
||
selectFields,
|
||
});
|
||
}
|
||
|
||
return configs;
|
||
}
|
||
|
||
/**
|
||
* 解析统计关系(逗号分隔的字符串)
|
||
*
|
||
* @example
|
||
* 输入: "students, orders"
|
||
* 输出: ['students', 'orders']
|
||
*/
|
||
export function parseCountRelations(input: string): string[] {
|
||
if (!input || !input.trim()) {
|
||
return [];
|
||
}
|
||
return input
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
/**
|
||
* 获取字段的 TypeScript 类型(用于生成关联类型)
|
||
* 简化版本,假设大多数字段是 string
|
||
*/
|
||
export function getFieldType(fieldName: string): string {
|
||
// 常见的非字符串字段
|
||
const numberFields = ['id', 'count', 'amount', 'price', 'quantity', 'sort', 'order'];
|
||
const booleanFields = ['is', 'has', 'can', 'should', 'enabled', 'active', 'visible'];
|
||
|
||
if (numberFields.some((f) => fieldName.toLowerCase().includes(f))) {
|
||
return 'number';
|
||
}
|
||
if (booleanFields.some((f) => fieldName.toLowerCase().startsWith(f))) {
|
||
return 'boolean';
|
||
}
|
||
return 'string';
|
||
}
|
||
|
||
/**
|
||
* 生成关联类型的 select 对象字符串
|
||
*
|
||
* @example
|
||
* 输入: ['id', 'code', 'name']
|
||
* 输出: '{ id: true, code: true, name: true }'
|
||
*/
|
||
export function generateSelectObject(fields: string[]): string {
|
||
const pairs = fields.map((f) => `${f}: true`);
|
||
return `{ ${pairs.join(', ')} }`;
|
||
}
|
||
|
||
/**
|
||
* 生成关联类型定义字符串
|
||
*
|
||
* @example
|
||
* 输入: { name: 'class', selectFields: ['id', 'code', 'name'] }
|
||
* 输出: 'class?: { id: string; code: string; name: string } | null'
|
||
*/
|
||
export function generateRelationTypeField(config: RelationConfig): string {
|
||
const fieldTypes = config.selectFields.map((f) => `${f}: ${getFieldType(f)}`);
|
||
return `${config.name}?: { ${fieldTypes.join('; ')} } | null`;
|
||
}
|
||
|
||
/**
|
||
* 生成多对多类型定义字符串
|
||
*
|
||
* @example
|
||
* 输入: { name: 'teachers', target: 'teacher', selectFields: ['id', 'name'] }
|
||
* 输出: 'teachers?: Array<{ teacher: { id: string; name: string } }>'
|
||
*/
|
||
export function generateManyToManyTypeField(config: ManyToManyConfig): string {
|
||
const fieldTypes = config.selectFields.map((f) => `${f}: ${getFieldType(f)}`);
|
||
return `${config.name}?: Array<{ ${config.target}: { ${fieldTypes.join('; ')} } }>`;
|
||
}
|
||
|
||
/**
|
||
* 解析一对多关系配置
|
||
* 格式: 关系名:目标模型 [optional]
|
||
*
|
||
* @example
|
||
* 输入: "students:Student optional"
|
||
* 输出: { name: 'students', targetModel: 'Student', target: 'student', foreignKey: 'dormitoryId', backRelation: 'dormitory', optional: true }
|
||
*/
|
||
export function parseOneToMany(dsl: string, currentModelName: string): OneToManyConfig[] {
|
||
const lines = dsl
|
||
.split('\n')
|
||
.map((line) => line.trim())
|
||
.filter((line) => line && !line.startsWith('#'));
|
||
|
||
const configs: OneToManyConfig[] = [];
|
||
const currentModelCamel = currentModelName.charAt(0).toLowerCase() + currentModelName.slice(1);
|
||
|
||
for (const line of lines) {
|
||
// 匹配格式: 关系名:目标模型 [optional]
|
||
const match = line.match(/^(\w+):(\w+)(?:\s+(optional))?$/);
|
||
if (!match) {
|
||
console.warn(`无法解析一对多配置行: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
const [, name, targetModel, optionalFlag] = match;
|
||
const target = targetModel.charAt(0).toLowerCase() + targetModel.slice(1);
|
||
|
||
configs.push({
|
||
name,
|
||
targetModel,
|
||
target,
|
||
foreignKey: `${currentModelCamel}Id`,
|
||
backRelation: currentModelCamel,
|
||
optional: !!optionalFlag,
|
||
});
|
||
}
|
||
|
||
return configs;
|
||
}
|
||
|
||
/**
|
||
* 解析多对一关系配置
|
||
* 格式: 关联名:目标模型 [optional]
|
||
*
|
||
* @example
|
||
* 输入: "student:Student"
|
||
* 输出: { foreignKey: 'studentId', name: 'student', targetModel: 'Student', target: 'student', backRelation: 'grades', optional: false }
|
||
*/
|
||
export function parseManyToOne(dsl: string, currentModelPluralName: string): ManyToOneConfig[] {
|
||
const lines = dsl
|
||
.split('\n')
|
||
.map((line) => line.trim())
|
||
.filter((line) => line && !line.startsWith('#'));
|
||
|
||
const configs: ManyToOneConfig[] = [];
|
||
|
||
for (const line of lines) {
|
||
// 匹配格式: 关联名:目标模型 [optional]
|
||
const match = line.match(/^(\w+):(\w+)(?:\s+(optional))?$/);
|
||
if (!match) {
|
||
console.warn(`无法解析多对一配置行: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
const [, name, targetModel, optionalFlag] = match;
|
||
const target = targetModel.charAt(0).toLowerCase() + targetModel.slice(1);
|
||
|
||
configs.push({
|
||
foreignKey: `${target}Id`,
|
||
name,
|
||
targetModel,
|
||
target,
|
||
backRelation: currentModelPluralName,
|
||
optional: !!optionalFlag,
|
||
});
|
||
}
|
||
|
||
return configs;
|
||
}
|