UNPKG

@velopays/prisma-crud-generator

Version:

Generate CRUD operations and tests from Prisma models for NestJS

545 lines 26.5 kB
"use strict"; 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