- 支持一对多/多对一关系定义并生成到 Prisma schema - 简化流程:查询关联配置根据关系自动预填 - 修复 Handlebars 模板 HTML 转义导致的乱码问题 - 修复 controller 模板缺少 Prisma 导入的问题 - 新增页面模板 (page.hbs) 生成前端页面 - 添加 FindAllParams/PaginationQueryDto 索引签名修复类型兼容 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
402 lines
10 KiB
TypeScript
402 lines
10 KiB
TypeScript
/**
|
||
* 字段 DSL 解析器
|
||
*
|
||
* DSL 语法:
|
||
* 字段名:类型[修饰符] 标签 "示例值" [验证规则...]
|
||
*
|
||
* 示例:
|
||
* title:string 标题 "示例标题" min:2 max:100
|
||
* email:string! 邮箱 "test@example.com" email
|
||
* description:string? 描述 "描述内容" max:500
|
||
* price:number 价格 "99.99" min:0
|
||
* status:enum(draft,published) 状态 "draft"
|
||
*/
|
||
|
||
export type FieldType =
|
||
| 'string'
|
||
| 'number'
|
||
| 'boolean'
|
||
| 'date'
|
||
| 'datetime'
|
||
| 'enum';
|
||
|
||
export interface Validation {
|
||
type: 'min' | 'max' | 'email' | 'url' | 'pattern';
|
||
value?: string | number;
|
||
}
|
||
|
||
export interface FieldDefinition {
|
||
name: string; // 字段名
|
||
type: FieldType; // 字段类型
|
||
label: string; // 中文标签
|
||
example: string; // 示例值
|
||
nullable: boolean; // 是否可空
|
||
unique: boolean; // 是否唯一
|
||
validations: Validation[]; // 验证规则
|
||
options?: string[]; // 枚举选项
|
||
flags: {
|
||
noCreate: boolean; // 不在创建时使用
|
||
noUpdate: boolean; // 不在更新时使用
|
||
noTable: boolean; // 不在表格中显示
|
||
};
|
||
}
|
||
|
||
// 匹配: 字段名:类型[修饰符] 标签 "示例值" [验证规则...]
|
||
const FIELD_REGEX =
|
||
/^(\w+):(\w+(?:\([^)]+\))?)([\?!]*)\s+(.+?)\s+"([^"]+)"(?:\s+(.+))?$/;
|
||
|
||
/**
|
||
* 解析字段 DSL 字符串
|
||
*/
|
||
export function parseFields(dsl: string): FieldDefinition[] {
|
||
const lines = dsl
|
||
.split('\n')
|
||
.map((line) => line.trim())
|
||
.filter((line) => line && !line.startsWith('#'));
|
||
|
||
return lines.map((line) => parseFieldLine(line));
|
||
}
|
||
|
||
/**
|
||
* 解析单行字段定义
|
||
*/
|
||
function parseFieldLine(line: string): FieldDefinition {
|
||
const match = line.match(FIELD_REGEX);
|
||
if (!match) {
|
||
throw new Error(`无效的字段定义: ${line}`);
|
||
}
|
||
|
||
const [, name, typeStr, modifiers, label, example, validationsStr] = match;
|
||
|
||
// 解析类型
|
||
let type: FieldType;
|
||
let options: string[] | undefined;
|
||
|
||
if (typeStr.startsWith('enum(')) {
|
||
type = 'enum';
|
||
options = typeStr
|
||
.slice(5, -1)
|
||
.split(',')
|
||
.map((s) => s.trim());
|
||
} else {
|
||
type = typeStr as FieldType;
|
||
}
|
||
|
||
// 解析修饰符
|
||
const nullable = modifiers.includes('?');
|
||
const unique = modifiers.includes('!');
|
||
|
||
// 解析验证规则和标志
|
||
const validations: Validation[] = [];
|
||
const flags = { noCreate: false, noUpdate: false, noTable: false };
|
||
|
||
if (validationsStr) {
|
||
const parts = validationsStr.split(/\s+/);
|
||
for (const part of parts) {
|
||
if (part === 'noCreate') {
|
||
flags.noCreate = true;
|
||
} else if (part === 'noUpdate') {
|
||
flags.noUpdate = true;
|
||
} else if (part === 'noTable') {
|
||
flags.noTable = true;
|
||
} else if (part.startsWith('min:')) {
|
||
validations.push({ type: 'min', value: parseFloat(part.slice(4)) });
|
||
} else if (part.startsWith('max:')) {
|
||
validations.push({ type: 'max', value: parseFloat(part.slice(4)) });
|
||
} else if (part === 'email') {
|
||
validations.push({ type: 'email' });
|
||
} else if (part === 'url') {
|
||
validations.push({ type: 'url' });
|
||
} else if (part.startsWith('pattern:')) {
|
||
validations.push({ type: 'pattern', value: part.slice(8) });
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
name,
|
||
type,
|
||
label,
|
||
example,
|
||
nullable,
|
||
unique,
|
||
validations,
|
||
options,
|
||
flags,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取 TypeScript 类型
|
||
*/
|
||
export function getTsType(field: FieldDefinition): string {
|
||
const typeMap: Record<FieldType, string> = {
|
||
string: 'string',
|
||
number: 'number',
|
||
boolean: 'boolean',
|
||
date: 'Date',
|
||
datetime: 'Date',
|
||
enum: field.options?.map((o) => `'${o}'`).join(' | ') || 'string',
|
||
};
|
||
return typeMap[field.type] || 'string';
|
||
}
|
||
|
||
/**
|
||
* 获取 TypeScript 响应类型(Date 转 string)
|
||
*/
|
||
export function getTsResponseType(field: FieldDefinition): string {
|
||
if (field.type === 'date' || field.type === 'datetime') {
|
||
return 'string';
|
||
}
|
||
return getTsType(field);
|
||
}
|
||
|
||
/**
|
||
* 获取 Prisma 类型
|
||
*/
|
||
export function getPrismaType(field: FieldDefinition): string {
|
||
const typeMap: Record<FieldType, string> = {
|
||
string: 'String',
|
||
number: 'Float',
|
||
boolean: 'Boolean',
|
||
date: 'DateTime',
|
||
datetime: 'DateTime',
|
||
enum: 'String',
|
||
};
|
||
const baseType = typeMap[field.type] || 'String';
|
||
return field.nullable ? `${baseType}?` : baseType;
|
||
}
|
||
|
||
/**
|
||
* 获取验证装饰器列表
|
||
*/
|
||
export function getValidationDecorators(field: FieldDefinition): string[] {
|
||
const decorators: string[] = [];
|
||
|
||
// 类型验证
|
||
switch (field.type) {
|
||
case 'string':
|
||
decorators.push('@IsString()');
|
||
break;
|
||
case 'number':
|
||
decorators.push('@IsNumber()');
|
||
break;
|
||
case 'boolean':
|
||
decorators.push('@IsBoolean()');
|
||
break;
|
||
case 'date':
|
||
case 'datetime':
|
||
decorators.push('@IsDate()');
|
||
decorators.push('@Type(() => Date)');
|
||
break;
|
||
case 'enum':
|
||
decorators.push(
|
||
`@IsIn([${field.options?.map((o) => `'${o}'`).join(', ')}])`,
|
||
);
|
||
break;
|
||
}
|
||
|
||
// 验证规则
|
||
for (const v of field.validations) {
|
||
switch (v.type) {
|
||
case 'min':
|
||
if (field.type === 'string') {
|
||
decorators.push(`@MinLength(${v.value})`);
|
||
} else {
|
||
decorators.push(`@Min(${v.value})`);
|
||
}
|
||
break;
|
||
case 'max':
|
||
if (field.type === 'string') {
|
||
decorators.push(`@MaxLength(${v.value})`);
|
||
} else {
|
||
decorators.push(`@Max(${v.value})`);
|
||
}
|
||
break;
|
||
case 'email':
|
||
decorators.push('@IsEmail()');
|
||
break;
|
||
case 'url':
|
||
decorators.push('@IsUrl()');
|
||
break;
|
||
case 'pattern':
|
||
decorators.push(`@Matches(${v.value})`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return decorators;
|
||
}
|
||
|
||
/**
|
||
* 获取验证器导入列表
|
||
*/
|
||
export function getValidationImports(fields: FieldDefinition[]): string[] {
|
||
const imports = new Set<string>();
|
||
|
||
for (const field of fields) {
|
||
switch (field.type) {
|
||
case 'string':
|
||
imports.add('IsString');
|
||
break;
|
||
case 'number':
|
||
imports.add('IsNumber');
|
||
break;
|
||
case 'boolean':
|
||
imports.add('IsBoolean');
|
||
break;
|
||
case 'date':
|
||
case 'datetime':
|
||
imports.add('IsDate');
|
||
break;
|
||
case 'enum':
|
||
imports.add('IsIn');
|
||
break;
|
||
}
|
||
|
||
for (const v of field.validations) {
|
||
switch (v.type) {
|
||
case 'min':
|
||
imports.add(field.type === 'string' ? 'MinLength' : 'Min');
|
||
break;
|
||
case 'max':
|
||
imports.add(field.type === 'string' ? 'MaxLength' : 'Max');
|
||
break;
|
||
case 'email':
|
||
imports.add('IsEmail');
|
||
break;
|
||
case 'url':
|
||
imports.add('IsUrl');
|
||
break;
|
||
case 'pattern':
|
||
imports.add('Matches');
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (field.nullable) {
|
||
imports.add('IsOptional');
|
||
}
|
||
}
|
||
|
||
return Array.from(imports);
|
||
}
|
||
|
||
/**
|
||
* 获取 Zod 验证字符串
|
||
*/
|
||
export function getZodValidation(field: FieldDefinition): string {
|
||
let zod = '';
|
||
|
||
switch (field.type) {
|
||
case 'string':
|
||
zod = 'z.string()';
|
||
for (const v of field.validations) {
|
||
if (v.type === 'min')
|
||
zod += `.min(${v.value}, '最少 ${v.value} 个字符')`;
|
||
if (v.type === 'max')
|
||
zod += `.max(${v.value}, '最多 ${v.value} 个字符')`;
|
||
if (v.type === 'email') zod += `.email('请输入有效的邮箱地址')`;
|
||
if (v.type === 'url') zod += `.url('请输入有效的 URL')`;
|
||
}
|
||
break;
|
||
case 'number':
|
||
zod = 'z.coerce.number()';
|
||
for (const v of field.validations) {
|
||
if (v.type === 'min') zod += `.min(${v.value}, '最小值为 ${v.value}')`;
|
||
if (v.type === 'max') zod += `.max(${v.value}, '最大值为 ${v.value}')`;
|
||
}
|
||
break;
|
||
case 'boolean':
|
||
zod = 'z.boolean()';
|
||
break;
|
||
case 'enum':
|
||
zod = `z.enum([${field.options?.map((o) => `'${o}'`).join(', ')}])`;
|
||
break;
|
||
default:
|
||
zod = 'z.string()';
|
||
}
|
||
|
||
if (field.nullable) {
|
||
zod += '.optional()';
|
||
}
|
||
|
||
return zod;
|
||
}
|
||
|
||
/**
|
||
* 生成表单控件代码
|
||
*/
|
||
export function getFormControl(field: FieldDefinition): string {
|
||
switch (field.type) {
|
||
case 'string': {
|
||
const maxLen = field.validations.find((v) => v.type === 'max')?.value;
|
||
if (maxLen && Number(maxLen) > 100) {
|
||
return `<Textarea placeholder="请输入${field.label}" {...field} />`;
|
||
}
|
||
return `<Input placeholder="请输入${field.label}" {...field} />`;
|
||
}
|
||
case 'number':
|
||
return `<Input type="number" placeholder="请输入${field.label}" {...field} onChange={e => field.onChange(e.target.valueAsNumber)} />`;
|
||
case 'boolean':
|
||
return `<Switch checked={field.value} onCheckedChange={field.onChange} />`;
|
||
case 'enum':
|
||
return `<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="请选择${field.label}" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
${field.options?.map((o) => ` <SelectItem value="${o}">${o}</SelectItem>`).join('\n')}
|
||
</SelectContent>
|
||
</Select>`;
|
||
default:
|
||
return `<Input placeholder="请输入${field.label}" {...field} />`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成表格单元格渲染器
|
||
*/
|
||
export function getCellRenderer(field: FieldDefinition): string | null {
|
||
switch (field.type) {
|
||
case 'boolean':
|
||
return `row.original.${field.name} ? '是' : '否'`;
|
||
case 'date':
|
||
return `formatDate(new Date(row.original.${field.name}), 'YYYY-MM-DD')`;
|
||
case 'datetime':
|
||
return `formatDate(new Date(row.original.${field.name}), 'YYYY-MM-DD HH:mm')`;
|
||
default:
|
||
return field.nullable ? `row.original.${field.name} || '-'` : null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成 where 条件
|
||
*/
|
||
export function getWhereCondition(field: FieldDefinition): string {
|
||
if (field.type === 'string') {
|
||
return `{ contains: query.${field.name}, mode: 'insensitive' }`;
|
||
}
|
||
return `query.${field.name}`;
|
||
}
|
||
|
||
/**
|
||
* 获取格式化后的 example 值(用于代码生成)
|
||
* 字符串类型加引号,数字/布尔值直接输出
|
||
*/
|
||
export function getFormattedExample(field: FieldDefinition): string {
|
||
switch (field.type) {
|
||
case 'string':
|
||
case 'enum':
|
||
case 'date':
|
||
case 'datetime':
|
||
return `'${field.example}'`;
|
||
case 'number':
|
||
return field.example;
|
||
case 'boolean':
|
||
return field.example.toLowerCase() === 'true' ? 'true' : 'false';
|
||
default:
|
||
return `'${field.example}'`;
|
||
}
|
||
}
|