Files
seclusion/plop/utils/field-parser.ts
charilezhou 3119460f13 feat(plop): 优化生成器支持 Prisma 关联关系
- 支持一对多/多对一关系定义并生成到 Prisma schema
- 简化流程:查询关联配置根据关系自动预填
- 修复 Handlebars 模板 HTML 转义导致的乱码问题
- 修复 controller 模板缺少 Prisma 导入的问题
- 新增页面模板 (page.hbs) 生成前端页面
- 添加 FindAllParams/PaginationQueryDto 索引签名修复类型兼容

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:30:18 +08:00

402 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 字段 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}'`;
}
}