Files
seclusion/plop/utils/schema-parser.ts
charilezhou 8c904c419a feat(plop): 关联配置支持从 Schema 选择模型和字段
- 新增 schema-parser.ts 解析 Prisma schema 文件
- 关联关系先选择模型,再编辑预填的配置
- 多对多关系自动推断中间表配置
- 可用字段作为注释显示,方便用户参考

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 16:14:48 +08:00

189 lines
4.8 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.

/**
* Prisma Schema 解析器
* 解析 schema.prisma 文件,提取模型和字段信息
*/
import fs from 'fs';
import path from 'path';
/** 字段信息 */
export interface SchemaField {
name: string;
type: string;
isOptional: boolean;
isArray: boolean;
isRelation: boolean;
relationModel?: string;
}
/** 模型信息 */
export interface SchemaModel {
name: string;
fields: SchemaField[];
relations: SchemaField[];
}
/**
* 解析 Prisma schema 文件
*/
export function parseSchema(schemaPath?: string): SchemaModel[] {
const defaultPath = path.resolve(process.cwd(), 'apps/api/prisma/schema.prisma');
const filePath = schemaPath || defaultPath;
if (!fs.existsSync(filePath)) {
console.warn(`Schema file not found: ${filePath}`);
return [];
}
const content = fs.readFileSync(filePath, 'utf-8');
const models: SchemaModel[] = [];
// 匹配 model 定义
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
let match;
while ((match = modelRegex.exec(content)) !== null) {
const modelName = match[1];
const modelBody = match[2];
const fields: SchemaField[] = [];
const relations: SchemaField[] = [];
// 解析字段
const lines = modelBody.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// 跳过空行、注释、@@指令
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {
continue;
}
// 匹配字段定义: fieldName Type? @...
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\??/);
if (fieldMatch) {
const [, fieldName, fieldType, isArray] = fieldMatch;
const isOptional = trimmed.includes('?');
// 判断是否是关系字段(类型是其他模型)
const isRelation = /^[A-Z]/.test(fieldType) && fieldType !== 'String' &&
fieldType !== 'Int' && fieldType !== 'Float' && fieldType !== 'Boolean' &&
fieldType !== 'DateTime' && fieldType !== 'Json' && fieldType !== 'BigInt' &&
fieldType !== 'Decimal' && fieldType !== 'Bytes';
const field: SchemaField = {
name: fieldName,
type: fieldType,
isOptional,
isArray: !!isArray,
isRelation,
relationModel: isRelation ? fieldType : undefined,
};
fields.push(field);
if (isRelation) {
relations.push(field);
}
}
}
models.push({
name: modelName,
fields,
relations,
});
}
return models;
}
/**
* 获取所有可用的目标模型(排除中间表)
*/
export function getAvailableModels(models: SchemaModel[]): string[] {
// 排除常见的中间表模式
const excludePatterns = [
/^User\w*Role$/i,
/^Role\w*Permission$/i,
/^Role\w*Menu$/i,
/^\w+Teacher$/i,
/^\w+Student$/i,
];
return models
.filter((m) => {
// 排除中间表(通常只有外键字段和 createdAt
const nonIdFields = m.fields.filter((f) =>
!['id', 'createdAt', 'updatedAt', 'deletedAt'].includes(f.name) && !f.name.endsWith('Id')
);
// 如果非 ID 字段少于 2 个,可能是中间表
if (nonIdFields.length < 2) {
return false;
}
// 排除匹配的模式
return !excludePatterns.some((p) => p.test(m.name));
})
.map((m) => m.name);
}
/**
* 获取模型的可选字段(用于 select
*/
export function getSelectableFields(model: SchemaModel): string[] {
return model.fields
.filter((f) => !f.isRelation && !f.isArray)
.map((f) => f.name);
}
/**
* 获取模型的关系字段(用于统计关系)
*/
export function getRelationFields(model: SchemaModel): string[] {
return model.relations
.filter((r) => r.isArray) // 只返回一对多/多对多关系
.map((r) => r.name);
}
/**
* 根据模型名获取模型
*/
export function getModelByName(models: SchemaModel[], name: string): SchemaModel | undefined {
return models.find((m) => m.name === name);
}
/**
* 推断多对多中间表配置
* 根据当前模型和目标模型,找到中间表
*/
export function inferManyToManyConfig(
models: SchemaModel[],
currentModel: string,
targetModel: string
): { through: string; foreignKey: string; targetKey: string } | null {
// 查找包含两个模型外键的中间表
for (const model of models) {
const fields = model.fields;
const currentFk = fields.find((f) =>
f.name.toLowerCase() === `${currentModel.toLowerCase()}id`
);
const targetFk = fields.find((f) =>
f.name.toLowerCase() === `${targetModel.toLowerCase()}id`
);
if (currentFk && targetFk) {
// 转换为 camelCase
const through = model.name.charAt(0).toLowerCase() + model.name.slice(1);
return {
through,
foreignKey: currentFk.name,
targetKey: targetFk.name,
};
}
}
return null;
}