UNPKG

@velopays/prisma-crud-generator

Version:

Generate CRUD operations and tests from Prisma models for NestJS

348 lines 16.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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.CrudGenerator = void 0; exports.generateCrudFiles = generateCrudFiles; exports.generateAllCrudFiles = generateAllCrudFiles; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const Handlebars = __importStar(require("handlebars")); const generator_1 = require("./generator"); const utils_1 = require("./utils"); class CrudGenerator { constructor(config, schemaName) { this.templates = new Map(); this.config = config; this.schemaName = schemaName; this.loadTemplates(); this.registerHelpers(); } loadTemplates() { const templatesPath = this.config.customTemplatesPath || path.join(__dirname, '../templates'); const templateFiles = [ 'controller.hbs', 'controller.spec.hbs', 'service.hbs', 'service.spec.hbs', 'dto/create.dto.hbs', 'dto/update.dto.hbs', 'dto/query.dto.hbs', 'dto/response.dto.hbs', 'module.hbs' ]; templateFiles.forEach(file => { const templatePath = path.join(templatesPath, file); if (fs.existsSync(templatePath)) { const template = fs.readFileSync(templatePath, 'utf-8'); this.templates.set(file, Handlebars.compile(template)); } }); } registerHelpers() { // Helper to check if field should be in create DTO Handlebars.registerHelper('isCreateField', (field, options) => { // 首先检查是否是ID或时间戳字段,这些不应该在create DTO中 if (field.isId || field.isCreatedAt || field.isUpdatedAt) { return false; } // 检查是否是关系字段 const isRelationField = field.kind === 'object'; // 如果是关系字段且配置为不包含关系字段,则返回false if (isRelationField && !options.data.root.includeRelationsInCreateDto) { return false; } // 其他字段都应该包含在create DTO中 return true; }); // Helper to check if field should be in update DTO Handlebars.registerHelper('isUpdateField', (field, options) => { // 首先检查是否是ID或时间戳字段,这些不应该在update DTO中 if (field.isId || field.isCreatedAt || field.isUpdatedAt) { return false; } // 检查是否是关系字段 const isRelationField = field.kind === 'object'; // 如果是关系字段且配置为不包含关系字段,则返回false if (isRelationField && !options.data.root.includeRelationsInCreateDto) { return false; } // 其他字段都应该包含在update DTO中 return true; }); // Helper to check if field should be in query DTO Handlebars.registerHelper('isQueryField', (field, options) => { // 检查是否是关系字段 const isRelationField = field.kind === 'object'; // 如果是关系字段且配置为不包含关系字段,则返回false if (isRelationField && !options.data.root.includeRelationsInQueryDto) { return false; } // 所有非关系字段都应该包含在query DTO中 return true; }); // Helper to generate validation decorators Handlebars.registerHelper('validationDecorators', (field) => { const decorators = []; field.validations?.forEach((validation) => { if (validation.value) { decorators.push(`@${validation.type}(${JSON.stringify(validation.value)})`); } else { decorators.push(`@${validation.type}()`); } }); return decorators.join('\n '); }); // Helper for swagger type Handlebars.registerHelper('swaggerType', (type, isList) => { const swaggerTypes = { 'string': 'String', 'number': 'Number', 'boolean': 'Boolean', 'Date': 'Date', 'any': 'Object' }; const swaggerType = swaggerTypes[type] || 'String'; return isList ? `[${swaggerType}]` : swaggerType; }); // Helper for composite ID where clause Handlebars.registerHelper('compositeIdWhere', (compositeIdFields, prefix = '') => { if (!compositeIdFields || compositeIdFields.length === 0) return ''; const fields = compositeIdFields.map(field => `${field}: ${prefix}${field}`).join(', '); return `{ ${fields} }`; }); // Equals helper Handlebars.registerHelper('eq', (a, b) => a === b); // Not equals helper Handlebars.registerHelper('neq', (a, b) => a !== b); // Or helper - 支持多个参数 Handlebars.registerHelper('or', function (...args) { // 移除最后一个参数(Handlebars options对象) args.pop(); return args.some(arg => arg); }); // And helper Handlebars.registerHelper('and', (a, b) => a && b); // Snake case helper Handlebars.registerHelper('snakeCase', (str) => (0, utils_1.snakeCase)(str).toUpperCase()); // Kebab case helper Handlebars.registerHelper('kebabCase', (str) => { return str .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .toLowerCase(); }); // Pascal case helper Handlebars.registerHelper('pascalCase', (str) => { return str .split(/[-_\s]+/) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); }); // Helper to deduplicate imports Handlebars.registerHelper('uniqueImports', (imports) => { return [...new Set(imports)].join(', '); }); // Helper to check if field needs Type import Handlebars.registerHelper('needsTypeImport', (fields) => { return fields.some(field => (field.type === 'Date' || field.isList) && field.kind !== 'object' // 排除关系字段 ); }); // 注册 uniqueEnums helper - 收集唯一的枚举类型 Handlebars.registerHelper('uniqueEnums', (fields) => { if (!fields || !Array.isArray(fields)) { return ''; } 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); } }); const result = Array.from(enums).join(', '); return result; }); // 注册 uniqueRelationImports helper - 收集需要导入的关系实体DTO Handlebars.registerHelper('uniqueRelationImports', (fields, options) => { if (!fields || !Array.isArray(fields)) { return ''; } // 检查是否配置了包含关系字段 if (!options.data.root.includeRelationsInCreateDto) { return ''; } const imports = new Set(); const schemaName = options.data.root.schemaName; const currentModelName = options.data.root.name; fields.forEach(field => { // 只处理关系字段(object类型) if (field.kind === 'object' && field.type) { // 跳过自引用 if (field.type === currentModelName) { return; } // 将实体名转换为kebab-case格式 const kebabName = field.type .replace(/([a-z])([A-Z])/g, '$1-$2') .toLowerCase(); // 构建导入语句 const importStatement = `import { Create${field.type}Dto } from '../${kebabName}/dto/create-${kebabName}.dto';`; imports.add(importStatement); } }); return Array.from(imports).join('\n'); }); // 注册 uniqueRelationResponseImports helper - 收集需要导入的关系实体ResponseDTO Handlebars.registerHelper('uniqueRelationResponseImports', function (relations, options) { if (!relations || !Array.isArray(relations)) { return ''; } const imports = new Set(); const currentSchemaName = options.data.root.schemaName; relations.forEach(relation => { if (relation.relatedModel) { // 将实体名转换为kebab-case格式 const kebabName = relation.relatedModel .replace(/([a-z])([A-Z])/g, '$1-$2') .toLowerCase(); // 为了确定关联模型的 schema,我们需要一个映射或者约定 // 这里我们假设如果没有特殊配置,关联模型属于同一个 schema // 但是可以通过配置或命名约定来覆盖 let targetSchemaName = currentSchemaName; // 如果有 schemaMap 配置,使用它来确定目标 schema const schemaMap = options.data.root.schemaMap; if (schemaMap && schemaMap[relation.relatedModel]) { targetSchemaName = schemaMap[relation.relatedModel]; } // 构建导入语句,使用绝对路径 const importStatement = `import { ${relation.relatedModel}ResponseDto } from '@modules/${targetSchemaName}/${kebabName}/dto/${kebabName}-response.dto';`; imports.add(importStatement); } }); return Array.from(imports).join('\n'); }); } async generateFiles(model) { const templateData = this.prepareTemplateData(model); // 使用 schema 名称构建路径 const modulePath = path.join(this.config.outputPath, this.schemaName, model.kebabName); // Create directory structure await fs.ensureDir(path.join(modulePath, 'controllers')); await fs.ensureDir(path.join(modulePath, 'services')); await fs.ensureDir(path.join(modulePath, 'dto')); // Generate files await this.generateFile('controller.hbs', templateData, path.join(modulePath, 'controllers', `${model.kebabName}.controller.ts`)); await this.generateFile('controller.spec.hbs', templateData, path.join(modulePath, 'controllers', `${model.kebabName}.controller.spec.ts`)); await this.generateFile('service.hbs', templateData, path.join(modulePath, 'services', `${model.kebabName}.service.ts`)); await this.generateFile('service.spec.hbs', templateData, path.join(modulePath, 'services', `${model.kebabName}.service.spec.ts`)); await this.generateFile('dto/create.dto.hbs', templateData, path.join(modulePath, 'dto', `create-${model.kebabName}.dto.ts`)); await this.generateFile('dto/update.dto.hbs', templateData, path.join(modulePath, 'dto', `update-${model.kebabName}.dto.ts`)); await this.generateFile('dto/query.dto.hbs', templateData, path.join(modulePath, 'dto', `query-${model.kebabName}.dto.ts`)); await this.generateFile('dto/response.dto.hbs', templateData, path.join(modulePath, 'dto', `${model.kebabName}-response.dto.ts`)); await this.generateFile('module.hbs', templateData, path.join(modulePath, `${model.kebabName}.module.ts`)); } async generateFile(templateName, data, outputPath) { const template = this.templates.get(templateName); if (!template) { console.warn(`Template ${templateName} not found`); return; } const content = template(data); await fs.writeFile(outputPath, content); console.log(`Generated: ${outputPath}`); } prepareTemplateData(model) { const imports = this.generateImports(model); const defaultImports = this.config.imports || {}; return { ...model, imports, apiPrefix: this.config.apiPrefix || 'api', swaggerEnabled: this.config.swaggerEnabled ?? true, schemaName: this.schemaName, // 导入路径配置 apiResponseDtoPath: defaultImports.apiResponseDto || '@/common/dto/api-response.dto', jwtAuthGuardPath: defaultImports.jwtAuthGuard || '@/modules/auth/guards/jwt-auth.guard', permissionGuardPath: defaultImports.permissionGuard || '@/modules/auth/guards/permission.guard', requirePermissionDecoratorPath: defaultImports.requirePermission || '@/modules/auth/decorators/require-permission.decorator', permissionTypesPath: defaultImports.permissionTypes || '@/modules/auth/types/permission.types', baseQueryDtoPath: defaultImports.baseQueryDto || '@/common/dto/base-query.dto', prismaServicePath: defaultImports.prismaService || '@/shared/prisma/prisma.service', // 功能配置 includeRelationsInCreateDto: this.config.includeRelationsInCreateDto ?? false, includeRelationsInQueryDto: this.config.includeRelationsInQueryDto ?? false, // Schema 映射 schemaMap: this.config.schemaMap, }; } generateImports(model) { const imports = []; // Collect validation decorators const validationTypes = new Set(); model.fields.forEach(field => { field.validations?.forEach(validation => { validationTypes.add(validation.type); }); }); if (validationTypes.size > 0) { imports.push({ name: Array.from(validationTypes).join(', '), from: 'class-validator' }); } // Add Type decorator if needed if (model.fields.some(f => f.type === 'Date')) { imports.push({ name: 'Type', from: 'class-transformer' }); } return imports; } } exports.CrudGenerator = CrudGenerator; // Export convenience functions async function generateCrudFiles(model, config, schemaName) { const generator = new CrudGenerator(config, schemaName); await generator.generateFiles(model); } async function generateAllCrudFiles(config) { const prismaGenerator = new generator_1.PrismaCrudGenerator(config); await prismaGenerator.generate(); } //# sourceMappingURL=crud-generator.js.map