@velopays/prisma-crud-generator
Version:
Generate CRUD operations and tests from Prisma models for NestJS
348 lines • 16.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;
};
})();
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