@velopays/prisma-crud-generator
Version:
Generate CRUD operations and tests from Prisma models for NestJS
545 lines • 26.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrismaCrudGenerator = void 0;
const getDmmf_js_1 = require("@prisma/internals/dist/engine-commands/getDmmf.js");
const internals_1 = require("@prisma/internals");
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const Handlebars = __importStar(require("handlebars"));
const utils_1 = require("./utils");
const pluralize_1 = __importDefault(require("pluralize"));
class PrismaCrudGenerator {
constructor(config) {
this.modelSchemaMap = new Map();
this.config = config;
this.schemaName = this.extractSchemaName();
}
async init() {
// 尝试多种方式获取 DMMF
try {
// 方法1: 尝试从生成的 Prisma 客户端获取 DMMF
await this.loadDmmfFromClient();
}
catch (clientError) {
console.warn('Failed to load DMMF from Prisma client:', clientError.message);
try {
// 方法2: 使用 getSchemaWithPath 来支持多文件结构
const schemaResult = await (0, internals_1.getSchemaWithPath)(this.config.prismaSchemaPath);
if (schemaResult.schemas && schemaResult.schemas.length > 0) {
// 合并所有文件内容
const combinedSchema = schemaResult.schemas
.map(([filePath, content]) => content)
.join('\n');
this.dmmf = await (0, getDmmf_js_1.getDMMF)({ datamodel: combinedSchema });
console.log(`Loaded ${schemaResult.schemas.length} schema files from ${schemaResult.schemaPath}`);
}
else {
throw new Error('No schema files found');
}
}
catch (schemaError) {
// 方法3: 回退到直接读取文件
console.warn('Failed to load schema with getSchemaWithPath, falling back to direct file read');
console.warn('Error:', schemaError.message);
const schema = fs.readFileSync(this.config.prismaSchemaPath, 'utf-8');
this.dmmf = await (0, getDmmf_js_1.getDMMF)({ datamodel: schema });
}
}
}
async loadDmmfFromClient() {
// 尝试找到生成的 Prisma 客户端
const possibleClientPaths = [];
// 如果配置中指定了客户端路径,优先使用
if (this.config.clientPath) {
possibleClientPaths.push(path.resolve(this.config.clientPath));
}
// 默认路径
possibleClientPaths.push(path.join(process.cwd(), 'generated/prisma'), path.join(process.cwd(), 'node_modules/.prisma/client'), path.join(process.cwd(), 'prisma/generated/client'), path.join(process.cwd(), 'src/generated/prisma'));
for (const clientPath of possibleClientPaths) {
try {
const indexPath = path.join(clientPath, 'index.js');
if (fs.existsSync(indexPath)) {
console.log(`Attempting to load DMMF from: ${clientPath}`);
try {
// 删除 require 缓存以确保重新加载
delete require.cache[require.resolve(indexPath)];
// 使用 require 而不是 import 来加载 CommonJS 模块
const prismaModule = require(indexPath);
if (prismaModule.Prisma && prismaModule.Prisma.dmmf) {
this.dmmf = prismaModule.Prisma.dmmf;
console.log(`Successfully loaded DMMF from Prisma client at: ${clientPath}`);
console.log(`Found ${this.dmmf.datamodel.models.length} models in DMMF`);
return;
}
console.log(`Found client at ${clientPath} but no DMMF available`);
console.log('Available keys in Prisma object:', Object.keys(prismaModule.Prisma || {}));
}
catch (requireError) {
console.log(`Failed to require client from ${clientPath}:`, requireError.message);
}
}
}
catch (error) {
console.warn(`Failed to load client from ${clientPath}:`, error.message);
continue;
}
}
throw new Error('Could not find generated Prisma client with DMMF');
}
extractSchemaName() {
// 如果配置中提供了 schema 名称,使用它
if (this.config.schemaName) {
return this.config.schemaName;
}
// 否则从文件路径提取
const filename = path.basename(this.config.prismaSchemaPath, '.prisma');
return filename === 'schema' ? 'main' : filename;
}
async generate() {
await this.init();
const models = this.getModels();
// 首先建立完整的 model-schema 映射
for (const model of models) {
const originalModel = this.dmmf.datamodel.models.find(m => m.name === model.name);
const modelSchemaName = this.config.schemaMap?.[model.name] ||
this.getModelSchemaName(originalModel) ||
this.schemaName;
this.modelSchemaMap.set(model.name, modelSchemaName);
}
// 如果配置启用,生成通用文件(默认启用)
if (this.config.generateCommonFiles !== false) {
await this.generateCommonFiles(models);
}
// 按 schema 分组模型
const modelsBySchema = new Map();
for (const model of models) {
const modelSchemaName = this.modelSchemaMap.get(model.name) || this.schemaName;
if (!modelsBySchema.has(modelSchemaName)) {
modelsBySchema.set(modelSchemaName, []);
}
modelsBySchema.get(modelSchemaName).push(model);
await this.generateModelFiles(model, modelSchemaName);
}
// 为每个 schema 生成 root module
if (this.config.generateRootModules !== false) {
await this.generateRootModules(modelsBySchema);
}
}
getModels() {
if (!this.dmmf || !this.dmmf.datamodel || !this.dmmf.datamodel.models) {
console.error('No models found in schema');
if (this.dmmf) {
console.error('DMMF structure:', JSON.stringify(this.dmmf, null, 2).substring(0, 500));
}
return [];
}
return this.dmmf.datamodel.models.map(model => this.transformModel(model));
}
getSchemaName() {
return this.schemaName;
}
transformModel(model) {
const name = model.name;
const fields = model.fields.map(field => this.transformField(field));
const relations = model.fields
.filter(field => field.kind === 'object')
.map(field => this.transformRelation(field));
// Handle composite IDs
const idFields = fields.filter(f => f.isId);
const hasCompositeId = idFields.length > 1;
const compositeIdFields = hasCompositeId ? idFields.map(f => f.name) : undefined;
// Extract unique constraints
const uniqueFields = this.extractUniqueFields(model);
// Extract indexes
const indexes = this.extractIndexes(model);
return {
name,
pluralName: (0, pluralize_1.default)(name),
kebabName: (0, utils_1.kebabCase)(name),
pascalName: (0, utils_1.pascalCase)(name),
camelName: (0, utils_1.camelCase)(name),
fields,
relations,
hasCompositeId,
compositeIdFields,
uniqueFields,
indexes
};
}
getModelSchemaName(model) {
// 方法1: 检查 DMMF 中的 schema 字段
if (model.schema) {
return model.schema;
}
// 方法2: 检查是否有 primaryKey 或 uniqueIndexes 中包含schema信息
if (model.primaryKey && model.primaryKey.name) {
// 有时schema信息可能在primaryKey名称中
const schemaMatch = model.primaryKey.name.match(/^([^_]+)_/);
if (schemaMatch && schemaMatch[1] !== model.name.toLowerCase()) {
return schemaMatch[1];
}
}
// 方法3: 从模型的 documentation 中解析 @@schema 指令
if (model.documentation) {
const schemaMatch = model.documentation.match(/@@schema\s*\(\s*"([^"]+)"\s*\)/);
if (schemaMatch) {
return schemaMatch[1];
}
}
// 方法4: 检查字段中是否有schema信息
if (model.fields && model.fields.length > 0) {
for (const field of model.fields) {
if (field.documentation) {
const schemaMatch = field.documentation.match(/@@schema\s*\(\s*"([^"]+)"\s*\)/);
if (schemaMatch) {
return schemaMatch[1];
}
}
}
}
// 方法5: 通过检查 dbName 来推断schema
if (model.dbName && model.dbName !== model.name) {
// 如果dbName与模型名不同,可能包含schema前缀
const parts = model.dbName.split('.');
if (parts.length > 1) {
return parts[0];
}
}
// 最后回退到配置中的 schema 名称
return this.schemaName;
}
transformField(field) {
const validations = this.extractValidations(field);
return {
name: field.name,
type: this.mapPrismaTypeToTS(field.type),
kind: field.kind,
isList: field.isList,
isRequired: field.isRequired,
isUnique: field.isUnique || false,
isId: field.isId,
isUpdatedAt: field.isUpdatedAt,
isCreatedAt: field.isCreatedAt || field.name === 'createdAt',
default: field.default,
documentation: field.documentation,
validations
};
}
transformRelation(field) {
return {
name: field.name,
type: field.type,
isList: field.isList,
isRequired: field.isRequired,
relatedModel: field.type,
foreignKeys: field.relationFromFields,
references: field.relationToFields
};
}
extractValidations(field) {
const validations = [];
// Required validation
if (field.isRequired && !field.isId && !field.isUpdatedAt && !field.isCreatedAt) {
validations.push({ type: 'IsNotEmpty' });
}
// Type validations
switch (field.type) {
case 'String':
validations.push({ type: 'IsString' });
if (field.documentation?.includes('@email')) {
validations.push({ type: 'IsEmail' });
}
if (field.documentation?.includes('@uuid')) {
validations.push({ type: 'IsUUID' });
}
break;
case 'Int':
case 'Float':
case 'Decimal':
validations.push({ type: 'IsNumber' });
break;
case 'Boolean':
validations.push({ type: 'IsBoolean' });
break;
case 'DateTime':
validations.push({ type: 'IsDateString' });
break;
case 'Json':
validations.push({ type: 'IsObject' });
break;
}
// Array validation
if (field.isList) {
validations.push({ type: 'IsArray' });
}
// Optional validation
if (!field.isRequired) {
validations.push({ type: 'IsOptional' });
}
return validations;
}
extractUniqueFields(model) {
const uniqueFields = [];
// Extract single unique fields from field definitions
model.fields.forEach(field => {
if (field.isUnique && !field.isId) {
uniqueFields.push({
name: field.name,
fields: [field.name],
isComposite: false
});
}
});
// Extract from uniqueFields (composite uniques)
if (model.uniqueFields && model.uniqueFields.length > 0) {
model.uniqueFields.forEach((unique, index) => {
const fields = Array.isArray(unique) ? unique : (unique.fields || []);
if (fields.length > 0) {
uniqueFields.push({
name: unique.name || `unique_${fields.join('_')}`,
fields: fields,
isComposite: fields.length > 1
});
}
});
}
// Extract from uniqueIndexes
if (model.uniqueIndexes && model.uniqueIndexes.length > 0) {
model.uniqueIndexes.forEach(index => {
uniqueFields.push({
name: index.name || `unique_${index.fields.join('_')}`,
fields: index.fields,
isComposite: index.fields.length > 1
});
});
}
return uniqueFields;
}
extractIndexes(model) {
const indexes = [];
// Extract from @@index
if (model.dbName || model.documentation) {
// Parse from model attributes if available
// This would need to be extracted from the schema parser
}
// For now, we'll check common index patterns
// In a real implementation, this would come from DMMF
model.fields.forEach(field => {
// Check for indexed fields (this is a simplified approach)
if (field.documentation?.includes('@index') ||
field.name.endsWith('Id') ||
field.name === 'email' ||
field.name === 'username') {
indexes.push({
name: `idx_${field.name}`,
fields: [field.name],
type: 'NORMAL',
isComposite: false
});
}
});
return indexes;
}
mapPrismaTypeToTS(prismaType) {
const typeMap = {
'String': 'string',
'Int': 'number',
'Float': 'number',
'Decimal': 'number',
'Boolean': 'boolean',
'DateTime': 'Date',
'Json': 'any',
'BigInt': 'bigint',
'Bytes': 'Buffer'
};
return typeMap[prismaType] || prismaType;
}
async generateModelFiles(model, modelSchemaName) {
const { CrudGenerator } = await Promise.resolve().then(() => __importStar(require('./crud-generator')));
// 将 modelSchemaMap 转换为普通对象传递给配置
const schemaMap = Object.fromEntries(this.modelSchemaMap);
const configWithSchemaMap = { ...this.config, schemaMap };
const generator = new CrudGenerator(configWithSchemaMap, modelSchemaName);
await generator.generateFiles(model);
}
async generateRootModules(modelsBySchema) {
console.log('📁 Generating root modules...');
// Register helpers
Handlebars.registerHelper('kebabCase', (str) => {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase();
});
Handlebars.registerHelper('pascalCase', (str) => {
return str
.split(/[-_\s]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
});
// 注册 uniqueEnums helper - 收集唯一的枚举类型
Handlebars.registerHelper('uniqueEnums', (fields) => {
const enums = new Set();
const basicTypes = ['string', 'number', 'boolean', 'Date', 'any', 'bigint', 'Buffer'];
fields.forEach(field => {
if (field.kind !== 'object' && !basicTypes.includes(field.type)) {
enums.add(field.type);
}
});
return Array.from(enums).join(', ');
});
const templatesPath = this.config.customTemplatesPath || path.join(__dirname, '../templates');
const templateContent = fs.readFileSync(path.join(templatesPath, 'root-module.hbs'), 'utf-8');
const template = Handlebars.compile(templateContent);
for (const [schemaName, models] of modelsBySchema) {
const outputPath = path.join(this.config.outputPath, schemaName, 'index.ts');
const data = {
schemaName,
modules: models.map(model => ({
name: model.name,
pascalName: model.pascalName,
kebabName: model.kebabName,
})),
};
const content = template(data);
await fs.writeFile(outputPath, content);
console.log(`✅ Generated: ${schemaName}/index.ts`);
}
}
async generateCommonFiles(models) {
console.log('📁 Generating common files...');
const commonPath = path.join(this.config.outputPath, 'common');
const templatesPath = this.config.customTemplatesPath || path.join(__dirname, '../templates');
// 创建目录结构
await fs.ensureDir(path.join(commonPath, 'dto'));
await fs.ensureDir(path.join(commonPath, 'guards'));
await fs.ensureDir(path.join(commonPath, 'decorators'));
await fs.ensureDir(path.join(commonPath, 'types'));
await fs.ensureDir(path.join(commonPath, 'prisma'));
await fs.ensureDir(path.join(commonPath, 'interceptors'));
await fs.ensureDir(path.join(commonPath, 'pipes'));
await fs.ensureDir(path.join(commonPath, 'filters'));
await fs.ensureDir(path.join(commonPath, 'services'));
await fs.ensureDir(path.join(commonPath, 'utils'));
// 生成 ApiResponseDto
const apiResponseTemplate = await fs.readFile(path.join(templatesPath, 'common/dto/api-response.dto.hbs'), 'utf-8');
const apiResponseContent = apiResponseTemplate; // 不需要处理,直接使用
await fs.writeFile(path.join(commonPath, 'dto/api-response.dto.ts'), apiResponseContent);
console.log('✅ Generated: common/dto/api-response.dto.ts');
// 生成 BaseQueryDto
const baseQueryTemplate = await fs.readFile(path.join(templatesPath, 'common/dto/base-query.dto.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'dto/base-query.dto.ts'), baseQueryTemplate);
console.log('✅ Generated: common/dto/base-query.dto.ts');
// 生成 JWT Auth Guard
const jwtAuthGuardTemplate = await fs.readFile(path.join(templatesPath, 'common/guards/jwt-auth.guard.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'guards/jwt-auth.guard.ts'), jwtAuthGuardTemplate);
console.log('✅ Generated: common/guards/jwt-auth.guard.ts');
// 生成 Permission Guard
const permissionGuardTemplate = await fs.readFile(path.join(templatesPath, 'common/guards/permission.guard.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'guards/permission.guard.ts'), permissionGuardTemplate);
console.log('✅ Generated: common/guards/permission.guard.ts');
// 生成 Require Permission Decorator
const requirePermissionTemplate = await fs.readFile(path.join(templatesPath, 'common/decorators/require-permission.decorator.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'decorators/require-permission.decorator.ts'), requirePermissionTemplate);
console.log('✅ Generated: common/decorators/require-permission.decorator.ts');
// 生成 Permission Types
const permissionTypesTemplate = await fs.readFile(path.join(templatesPath, 'common/types/permission.types.hbs'), 'utf-8');
// 编译模板
const Handlebars = await Promise.resolve().then(() => __importStar(require('handlebars')));
const template = Handlebars.compile(permissionTypesTemplate);
// 注册 snakeCase helper
Handlebars.registerHelper('snakeCase', (str) => {
return str
.replace(/([a-z])([A-Z])/g, '$1_$2')
.replace(/[\s-]+/g, '_')
.toUpperCase();
});
// 生成资源类型列表
const resourceTypes = models.map(model => model.name);
const permissionTypesContent = template({ resourceTypes });
await fs.writeFile(path.join(commonPath, 'types/permission.types.ts'), permissionTypesContent);
console.log('✅ Generated: common/types/permission.types.ts');
// 生成 Prisma Service
const prismaServiceTemplate = await fs.readFile(path.join(templatesPath, 'common/prisma/prisma.service.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'prisma/prisma.service.ts'), prismaServiceTemplate);
console.log('✅ Generated: common/prisma/prisma.service.ts');
// 生成 Prisma Module
const prismaModuleTemplate = await fs.readFile(path.join(templatesPath, 'common/prisma/prisma.module.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'prisma/prisma.module.ts'), prismaModuleTemplate);
console.log('✅ Generated: common/prisma/prisma.module.ts');
// 生成 base-response.dto
const baseResponseTemplate = await fs.readFile(path.join(templatesPath, 'common/dto/base-response.dto.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'dto/base-response.dto.ts'), baseResponseTemplate);
console.log('✅ Generated: common/dto/base-response.dto.ts');
// 生成 base.dto
const baseDtoTemplate = await fs.readFile(path.join(templatesPath, 'common/dto/base.dto.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'dto/base.dto.ts'), baseDtoTemplate);
console.log('✅ Generated: common/dto/base.dto.ts');
// 生成 Response Interceptor
const responseInterceptorTemplate = await fs.readFile(path.join(templatesPath, 'common/interceptors/response.interceptor.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'interceptors/response.interceptor.ts'), responseInterceptorTemplate);
console.log('✅ Generated: common/interceptors/response.interceptor.ts');
// 生成 Logging Interceptor
const loggingInterceptorTemplate = await fs.readFile(path.join(templatesPath, 'common/interceptors/logging.interceptor.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'interceptors/logging.interceptor.ts'), loggingInterceptorTemplate);
console.log('✅ Generated: common/interceptors/logging.interceptor.ts');
// 生成 Validation Pipe
const validationPipeTemplate = await fs.readFile(path.join(templatesPath, 'common/pipes/validation.pipe.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'pipes/validation.pipe.ts'), validationPipeTemplate);
console.log('✅ Generated: common/pipes/validation.pipe.ts');
// 生成 HTTP Exception Filter
const httpExceptionFilterTemplate = await fs.readFile(path.join(templatesPath, 'common/filters/http-exception.filter.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'filters/http-exception.filter.ts'), httpExceptionFilterTemplate);
console.log('✅ Generated: common/filters/http-exception.filter.ts');
// 生成 Prisma Error Util
const prismaErrorUtilTemplate = await fs.readFile(path.join(templatesPath, 'common/utils/prisma-error.util.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'utils/prisma-error.util.ts'), prismaErrorUtilTemplate);
console.log('✅ Generated: common/utils/prisma-error.util.ts');
// 生成 Query Builder Service
const queryBuilderServiceTemplate = await fs.readFile(path.join(templatesPath, 'common/services/query-builder.service.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'services/query-builder.service.ts'), queryBuilderServiceTemplate);
console.log('✅ Generated: common/services/query-builder.service.ts');
// 生成 Common Module
const commonModuleTemplate = await fs.readFile(path.join(templatesPath, 'common/common.module.hbs'), 'utf-8');
await fs.writeFile(path.join(commonPath, 'common.module.ts'), commonModuleTemplate);
console.log('✅ Generated: common/common.module.ts');
}
}
exports.PrismaCrudGenerator = PrismaCrudGenerator;
//# sourceMappingURL=generator.js.map