- 新增 schema-parser.ts 解析 Prisma schema 文件 - 关联关系先选择模型,再编辑预填的配置 - 多对多关系自动推断中间表配置 - 可用字段作为注释显示,方便用户参考 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
189 lines
4.8 KiB
TypeScript
189 lines
4.8 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|