UNPKG

@diagramers/cli

Version:

Diagramers CLI - Command-line tools for managing Diagramers projects

1,092 lines (1,067 loc) 49.1 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.generateModule = generateModule; exports.generateEndpoint = generateEndpoint; const fs = __importStar(require("fs")); const path = __importStar(require("path")); async function generateModule(moduleName, options = {}) { // Validate module name if (!moduleName || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(moduleName)) { throw new Error('Invalid module name. Use only letters, numbers, hyphens, and underscores. Must start with a letter.'); } // Convert module name to valid TypeScript identifiers const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); const moduleNameCapitalized = moduleNameCamelCase.charAt(0).toUpperCase() + moduleNameCamelCase.slice(1); console.log(`🔍 Debug: moduleName = "${moduleName}"`); console.log(`🔍 Debug: moduleNameCamelCase = "${moduleNameCamelCase}"`); console.log(`🔍 Debug: moduleNameCapitalized = "${moduleNameCapitalized}"`); const currentDir = process.cwd(); // Check for required API project structure const requiredDirs = ['src/modules']; const missingDirs = requiredDirs.filter(dir => !fs.existsSync(path.join(currentDir, dir))); if (missingDirs.length > 0) { throw new Error(`This command should be run from a diagramers API project. Missing directories: ${missingDirs.join(', ')}`); } // Create module directory structure const modulePath = path.join(currentDir, 'src/modules', moduleName); if (!fs.existsSync(modulePath)) { fs.mkdirSync(modulePath, { recursive: true }); fs.mkdirSync(path.join(modulePath, 'entities'), { recursive: true }); fs.mkdirSync(path.join(modulePath, 'schemas'), { recursive: true }); fs.mkdirSync(path.join(modulePath, 'services'), { recursive: true }); fs.mkdirSync(path.join(modulePath, 'controllers'), { recursive: true }); fs.mkdirSync(path.join(modulePath, 'routes'), { recursive: true }); } // Parse options const fields = options.fields || ['name', 'description', 'status']; const generateRoutes = options.generateRoutes !== false; const generateDatabase = options.generateDatabase !== false; const crud = options.crud || false; console.log('📝 Creating entity...'); const entityContent = generateEntityContent(moduleNameCamelCase, moduleNameCapitalized, fields); fs.writeFileSync(path.join(modulePath, 'entities', `${moduleNameCamelCase}.entity.ts`), entityContent); console.log('📋 Creating schema...'); const schemaContent = generateSchemaContent(moduleNameCamelCase, moduleNameCapitalized, fields); fs.writeFileSync(path.join(modulePath, 'schemas', `${moduleNameCamelCase}.schema.ts`), schemaContent); console.log('🔧 Creating service...'); const serviceContent = generateServiceContent(moduleNameCamelCase, moduleNameCapitalized, fields, crud); fs.writeFileSync(path.join(modulePath, 'services', `${moduleNameCamelCase}.service.ts`), serviceContent); console.log('🎮 Creating controller...'); const controllerContent = generateControllerContent(moduleNameCamelCase, moduleNameCapitalized, fields, crud); fs.writeFileSync(path.join(modulePath, 'controllers', `${moduleNameCamelCase}.controller.ts`), controllerContent); if (generateRoutes) { console.log('🛣️ Creating routes...'); const routesContent = generateRoutesContent(moduleNameCamelCase, moduleNameCapitalized, crud, fields); fs.writeFileSync(path.join(modulePath, 'routes', `${moduleNameCamelCase}.routes.ts`), routesContent); // Register routes in server manager console.log('🔗 Registering routes...'); await registerModuleRoutes(currentDir, moduleName, moduleNameCapitalized); } if (generateDatabase) { // Create database table/collection console.log('🗄️ Creating database table...'); await createDatabaseTable(currentDir, moduleName, moduleNameCapitalized); } console.log('✅ Module generation completed!'); console.log(`📁 Module: src/modules/${moduleName}/`); if (generateRoutes) { console.log(`🔗 Routes: /api/${moduleName}s`); } if (generateDatabase) { console.log(`🗄️ Database: ${moduleName} collection`); } } function generateEntityContent(moduleName, moduleNameCapitalized, fields) { // Convert moduleName to camelCase for internal identifiers const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); let entityContent = `import { BaseEntity } from '../../../shared/types/base-entity'; export interface ${moduleNameCapitalized} extends BaseEntity { _id?: string; name: string; description?: string; } export interface Create${moduleNameCapitalized}Data { name: string; description?: string; } export interface Update${moduleNameCapitalized}Data { name?: string; description?: string; }`; // Custom fields can be added here if needed // For now, we use the default entity structure return entityContent; } function generateSchemaContent(moduleName, moduleNameCapitalized, fields) { // Convert moduleName to camelCase for file paths const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); let schemaContent = `import mongoose, { Document, Schema } from 'mongoose'; import { ${moduleNameCapitalized} } from '../entities/${moduleNameCamelCase}.entity'; export interface I${moduleNameCapitalized}Document extends Omit<${moduleNameCapitalized}, '_id'>, Document {} const ${moduleNameCapitalized}Schema = new Schema<I${moduleNameCapitalized}Document>({ name: { type: String, required: true }, description: { type: String }, status: { type: Number, default: 1 }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); export const ${moduleNameCapitalized}Model = mongoose.model<I${moduleNameCapitalized}Document>('${moduleNameCapitalized}', ${moduleNameCapitalized}Schema);`; // Custom fields can be added here if needed // For now, we use the default schema structure return schemaContent; } function generateServiceContent(moduleName, moduleNameCapitalized, fields, crud) { // Convert moduleName to camelCase for file paths const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); let serviceContent = `import { Result } from '../../../shared/types/result'; import { ${moduleNameCapitalized}Model, I${moduleNameCapitalized}Document } from '../schemas/${moduleNameCamelCase}.schema'; import { ${moduleNameCapitalized}, Create${moduleNameCapitalized}Data, Update${moduleNameCapitalized}Data } from '../entities/${moduleNameCamelCase}.entity'; export class ${moduleNameCapitalized}Service { async getAll(): Promise<Result<${moduleNameCapitalized}[]>> { try { const items = await ${moduleNameCapitalized}Model.find({ status: 1 }).lean(); const transformedItems = items.map(item => ({ ...item, _id: item._id.toString() })) as ${moduleNameCapitalized}[]; return Result.success(transformedItems); } catch (error: any) { return Result.error('Failed to fetch ${moduleName}s', error.message); } } async getById(id: string): Promise<Result<${moduleNameCapitalized} | null>> { try { const item = await ${moduleNameCapitalized}Model.findOne({ _id: id, status: 1 }); if (!item) { return Result.error('${moduleNameCapitalized} not found', '${moduleNameCapitalized} with the specified ID does not exist'); } const itemObj = item.toObject(); itemObj._id = itemObj._id.toString(); return Result.success(itemObj as ${moduleNameCapitalized}); } catch (error: any) { return Result.error('Failed to fetch ${moduleName}', error.message); } } async create(data: Create${moduleNameCapitalized}Data): Promise<Result<${moduleNameCapitalized}>> { try { const entity = new ${moduleNameCapitalized}Model({ ...data, status: 1 }); const savedItem = await entity.save(); const itemObj = savedItem.toObject(); itemObj._id = itemObj._id.toString(); return Result.success(itemObj as ${moduleNameCapitalized}); } catch (error: any) { return Result.error('Failed to create ${moduleName}', error.message); } } async update(id: string, data: Update${moduleNameCapitalized}Data): Promise<Result<${moduleNameCapitalized} | null>> { try { const updatedItem = await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, { ...data }, { new: true }); if (!updatedItem) { return Result.error('${moduleNameCapitalized} not found', '${moduleNameCapitalized} with the specified ID does not exist'); } const itemObj = updatedItem.toObject(); itemObj._id = itemObj._id.toString(); return Result.success(itemObj as ${moduleNameCapitalized}); } catch (error: any) { return Result.error('Failed to update ${moduleName}', error.message); } } async delete(id: string): Promise<Result<boolean>> { try { const deletedItem = await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, { status: 0 }, { new: true }); if (!deletedItem) { return Result.error('${moduleNameCapitalized} not found', '${moduleNameCapitalized} with the specified ID does not exist'); } return Result.success(true); } catch (error: any) { return Result.error('Failed to delete ${moduleName}', error.message); } } }`; if (crud) { serviceContent += ` export class ${moduleNameCapitalized}Service { async getAll(): Promise<Result<${moduleNameCapitalized}[]>> { return await ${moduleNameCapitalized}Model.find({ status: 1 }).lean(); } async getById(id: string): Promise<Result<${moduleNameCapitalized} | null>> { return await ${moduleNameCapitalized}Model.findById(id); } async create(data: Create${moduleNameCapitalized}Data): Promise<Result<${moduleNameCapitalized}>> { const entity = new ${moduleNameCapitalized}Model({ ...data, status: 1 }); return await entity.save(); } async update(id: string, data: Update${moduleNameCapitalized}Data): Promise<Result<${moduleNameCapitalized} | null>> { return await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, { ...data }, { new: true }); } async delete(id: string): Promise<Result<boolean>> { return await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, { status: 0 }, { new: true }); } `; fields.forEach(field => { serviceContent += ` async ${field}(id: string, data: any): Promise<Result<${moduleNameCapitalized} | null>> { return await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, { ${field}: data }, { new: true }); } `; }); } return serviceContent; } function generateControllerContent(moduleName, moduleNameCapitalized, fields, crud) { // Convert moduleName to camelCase for file paths const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); let controllerContent = `import { ${moduleNameCapitalized}Service } from '../services/${moduleNameCamelCase}.service'; import { Result } from '../../../shared/types/result'; import { ResponseCode } from '../../../shared/constants/enums'; import { handleResponse } from '../../../shared/utils/handle-response'; import { v4 as uuidv4 } from 'uuid'; /** * @openapi * components: * schemas: * ${moduleNameCapitalized}: * type: object * properties: * _id: * type: string * description: ${moduleNameCapitalized} ID * name: * type: string * description: Name of the ${moduleName} * description: * type: string * description: Description of the ${moduleName} * status: * type: integer * description: Status of the ${moduleName} (1 - active, 0 - deleted) * createdAt: * type: string * format: date-time * description: Creation timestamp * updatedAt: * type: string * format: date-time * description: Last update timestamp * required: * - _id * - name * - status * - createdAt * - updatedAt */ export class ${moduleNameCapitalized}Controller { private service = new ${moduleNameCapitalized}Service(); /** * @openapi * /api/${moduleNameCamelCase}: * get: * summary: Get all ${moduleNameCamelCase}s * description: Retrieve a list of all active ${moduleNameCamelCase}s * tags: [${moduleNameCapitalized}s] * responses: * 200: * description: List of ${moduleNameCamelCase}s retrieved successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: array * items: * $ref: '#/components/schemas/${moduleNameCapitalized}' * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request */ async getAll(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const result = await this.service.getAll(); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to fetch ${moduleNameCamelCase}s' }], null, requestIdentifier as string)); } } /** * @openapi * /api/${moduleNameCamelCase}/{id}: * get: * summary: Get ${moduleNameCamelCase} by ID * description: Retrieve a specific ${moduleNameCamelCase} by its ID * tags: [${moduleNameCapitalized}s] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: ${moduleNameCapitalized} ID * responses: * 200: * description: ${moduleNameCapitalized} retrieved successfully * content: * application/json: * schema: * type: object * properties: * Data: * $ref: '#/components/schemas/${moduleNameCapitalized}' * StatusCode: * type: integer * example: 1000 * 404: * description: ${moduleNameCapitalized} not found */ async getById(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const result = await this.service.getById(req.params.id); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to fetch ${moduleNameCamelCase}' }], null, requestIdentifier as string)); } } /** * @openapi * /api/${moduleName}: * post: * summary: Create new ${moduleName} * description: Create a new ${moduleName} record * tags: [${moduleNameCapitalized}s] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * properties: * name: * type: string * description: Name of the ${moduleName} * description: * type: string * description: Description of the ${moduleName} * status: * type: integer * default: 1 * description: Status of the ${moduleName} * responses: * 201: * description: ${moduleNameCapitalized} created successfully * content: * application/json: * schema: * type: object * properties: * Data: * $ref: '#/components/schemas/${moduleNameCapitalized}' * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request or missing required fields */ async create(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const result = await this.service.create(req.body); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to create ${moduleNameCamelCase}' }], null, requestIdentifier as string)); } } /** * @openapi * /api/${moduleName}/{id}: * put: * summary: Update ${moduleName} * description: Update an existing ${moduleName} record * tags: [${moduleNameCapitalized}s] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: ${moduleNameCapitalized} ID * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * description: Name of the ${moduleName} * description: * type: string * description: Description of the ${moduleName} * status: * type: integer * description: Status of the ${moduleName} * responses: * 200: * description: ${moduleNameCapitalized} updated successfully * content: * application/json: * schema: * type: object * properties: * Data: * $ref: '#/components/schemas/${moduleNameCapitalized}' * StatusCode: * type: integer * example: 1000 * 404: * description: ${moduleNameCapitalized} not found */ async update(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const result = await this.service.update(req.params.id, req.body); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to update ${moduleNameCamelCase}' }], null, requestIdentifier as string)); } } /** * @openapi * /api/${moduleName}/{id}: * delete: * summary: Delete ${moduleName} * description: Soft delete a ${moduleName} (sets status to 0) * tags: [${moduleNameCapitalized}s] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: ${moduleNameCapitalized} ID * responses: * 200: * description: ${moduleNameCapitalized} deleted successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: boolean * example: true * StatusCode: * type: integer * example: 1000 * 404: * description: ${moduleNameCapitalized} not found */ async delete(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const result = await this.service.delete(req.params.id); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to delete ${moduleNameCamelCase}' }], null, requestIdentifier as string)); } } } `; // Ensure class is properly closed and do not append duplicate methods return controllerContent; } function generateRoutesContent(moduleName, moduleNameCapitalized, crud, fields = []) { // Convert moduleName to camelCase for file paths const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); let routesContent = `import { Router } from 'express'; import { ${moduleNameCapitalized}Controller } from '../controllers/${moduleNameCamelCase}.controller'; const router = Router(); const controller = new ${moduleNameCapitalized}Controller(); // Health check endpoint router.get('/health', (req, res) => res.send('${moduleNameCapitalized} API is up.')); // CRUD endpoints router.get('/', (req, res) => controller.getAll(req, res)); router.get('/:id', (req, res) => controller.getById(req, res)); router.post('/', (req, res) => controller.create(req, res)); router.put('/:id', (req, res) => controller.update(req, res)); router.delete('/:id', (req, res) => controller.delete(req, res)); export default router;`; if (crud && fields.length > 0) { routesContent += ` `; fields.forEach((field) => { routesContent += ` router.put('/${field}/:id', (req, res) => controller.update(req, res)); `; }); } return routesContent; } function updateRoutesIndex(currentDir, moduleName) { const routesIndexPath = path.join(currentDir, 'src/routes/index.ts'); if (!fs.existsSync(routesIndexPath)) { console.log('⚠️ routes/index.ts not found, skipping routes index update'); return; } // Convert moduleName to camelCase for file paths const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); let routesContent = fs.readFileSync(routesIndexPath, 'utf8'); // Add import - use camelCase for file path const importStatement = `import ${moduleNameCamelCase}Routes from '../modules/${moduleName}/routes/${moduleNameCamelCase}.routes';`; if (!routesContent.includes(importStatement)) { // Add import after the userRoutes import routesContent = routesContent.replace(/import userRoutes from '\.\.\/modules\/user\/routes\/user\.routes';/, `import userRoutes from '../modules/user/routes/user.routes';\n${importStatement}`); } // Add route registration - use original moduleName for route path const routeRegistration = `router.use('/${moduleName}', ${moduleNameCamelCase}Routes);`; if (!routesContent.includes(routeRegistration)) { // Add route after the users route routesContent = routesContent.replace(/router\.use\('\/users', userRoutes\);/, `router.use('/users', userRoutes);\n${routeRegistration}`); } fs.writeFileSync(routesIndexPath, routesContent); console.log('🛣️ Updated routes/index.ts'); } async function registerModuleRoutes(currentDir, moduleName, moduleNameCapitalized) { // Update routes/index.ts instead of server manager updateRoutesIndex(currentDir, moduleName); } async function createDatabaseTable(currentDir, moduleName, moduleNameCapitalized) { // Update the main seeder using the same logic as the bash script const mainSeederPath = path.join(currentDir, 'src/core/database/seeder.ts'); if (fs.existsSync(mainSeederPath)) { let seederContent = fs.readFileSync(mainSeederPath, 'utf8'); // Add import for the model const moduleNameCamelCase = moduleName.replace(/[-_]([a-z0-9])/g, (_, char) => char.toUpperCase()); const importStatement = `import { ${moduleNameCapitalized}Model } from '../../modules/${moduleName}/schemas/${moduleNameCamelCase}.schema';`; if (!seederContent.includes(importStatement)) { seederContent = seederContent.replace(/import { UserModel } from '\.\.\/\.\.\/modules\/user\/schemas\/user\.schema';/, `import { UserModel } from '../../modules/user/schemas/user.schema';\n${importStatement}`); } // Add seeder function - use exact module name provided by developer const seederFunction = ` private async seed${moduleNameCapitalized}(data: any[]): Promise<void> { if (data.length === 0) return; logger.info(\`[DatabaseSeeder] Seeding \${data.length} ${moduleName} records...\`); for (const ${moduleName}Data of data) { try { const existing = await ${moduleNameCapitalized}Model.findOne({ name: ${moduleName}Data.name }); if (!existing) { const ${moduleName} = new ${moduleNameCapitalized}Model({ ...${moduleName}Data, status: 1 }); await ${moduleName}.save(); logger.info(\`[DatabaseSeeder] Created ${moduleName}: \${${moduleName}Data.name}\`); } else { logger.info(\`[DatabaseSeeder] ${moduleNameCapitalized} \${${moduleName}Data.name} already exists, skipping\`); } } catch (error) { logger.error(\`[DatabaseSeeder] Failed to create ${moduleName} \${${moduleName}Data.name}:\`, error); } } }`; if (!seederContent.includes(`seed${moduleNameCapitalized}(`)) { seederContent = seederContent.replace(/\/\/ TEMPLATE: Add new seeder functions here \(the CLI will add new functions automatically\)/, `// TEMPLATE: Add new seeder functions here (the CLI will add new functions automatically)\n${seederFunction}`); } // Add default seed data - use exact module name const seedDataLine = ` ${moduleName}: [ { name: '${moduleNameCapitalized} Example', description: 'Example ${moduleName}', status: 1 } ],`; if (!seederContent.includes(seedDataLine)) { seederContent = seederContent.replace(/const defaultData: SeedData = \{/, `const defaultData: SeedData = {\n${seedDataLine}`); } fs.writeFileSync(mainSeederPath, seederContent); } console.log('✅ Database table creation script added'); } async function generateEndpoint(moduleName, endpointName, options = {}) { // Validate inputs if (!moduleName || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(moduleName)) { throw new Error('Invalid module name. Use only letters, numbers, hyphens, and underscores. Must start with a letter.'); } if (!endpointName || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(endpointName)) { throw new Error('Invalid endpoint name. Use only letters, numbers, hyphens, and underscores. Must start with a letter.'); } const moduleNameCapitalized = moduleName.charAt(0).toUpperCase() + moduleName.slice(1); const endpointNameCapitalized = endpointName.charAt(0).toUpperCase() + endpointName.slice(1); const currentDir = process.cwd(); const modulePath = path.join(currentDir, 'src/modules', moduleName); // Check if module exists if (!fs.existsSync(modulePath)) { throw new Error(`Module '${moduleName}' does not exist. Please create the module first using 'generate:module ${moduleName}'`); } // Parse options const method = (options.method || 'GET').toUpperCase(); const customPath = options.path || endpointName; const description = options.description || `${endpointNameCapitalized} endpoint for ${moduleName}`; const generateService = options.generateService !== false; const generateController = options.generateController !== false; const generateRoute = options.generateRoute !== false; const force = options.force || false; console.log(`🔧 Adding endpoint '${endpointName}' to module '${moduleName}'...`); // Convert endpoint name to camelCase for method names const camelCaseEndpoint = endpointName.replace(/-/g, '_').replace(/_([a-z])/g, (g) => g[1].toUpperCase()); const camelCaseEndpointCapitalized = camelCaseEndpoint.charAt(0).toUpperCase() + camelCaseEndpoint.slice(1); // Add service method if requested if (generateService) { console.log('📝 Adding service method...'); await addServiceMethod(modulePath, moduleName, moduleNameCapitalized, camelCaseEndpoint, camelCaseEndpointCapitalized, method, description, force); } // Add controller method if requested if (generateController) { console.log('🎮 Adding controller method...'); await addControllerMethod(modulePath, moduleName, moduleNameCapitalized, camelCaseEndpoint, camelCaseEndpointCapitalized, method, customPath, description, force); } // Add route if requested if (generateRoute) { console.log('🛣️ Adding route...'); await addRoute(modulePath, moduleName, moduleNameCapitalized, camelCaseEndpoint, camelCaseEndpointCapitalized, method, customPath); } console.log(`✅ Endpoint '${endpointName}' added to module '${moduleName}' successfully!`); console.log(`🔗 New endpoint available at: ${method} /api/${moduleName}/${customPath}`); if (generateService) { console.log(`📝 Service: Method '${camelCaseEndpoint}' added to service`); } if (generateController) { console.log(`🎮 Controller: Method '${camelCaseEndpoint}' added to controller`); } if (generateRoute) { console.log(`🛣️ Route: ${method} /${customPath} added to routes`); } } async function addServiceMethod(modulePath, moduleName, moduleNameCapitalized, endpointName, endpointNameCapitalized, method, description, force) { // Resolve service file path, supporting both singular and plural camelCase filenames const singularName = moduleName.endsWith('s') && moduleName.length > 1 ? moduleName.slice(0, -1) : moduleName; const toCamel = (s) => s.replace(/[-_]([a-z0-9])/g, (_, c) => c.toUpperCase()); const singularCamel = toCamel(singularName); const moduleCamel = toCamel(moduleName); const candidates = [ path.join(modulePath, 'services', `${singularCamel}.service.ts`), path.join(modulePath, 'services', `${moduleCamel}.service.ts`) ]; const servicePath = candidates.find(p => fs.existsSync(p)); if (!servicePath) { throw new Error(`Service file not found: ${candidates.join(' or ')}`); } let serviceContent = fs.readFileSync(servicePath, 'utf8'); // Check if method already exists if (serviceContent.includes(`async ${endpointName}(`) && !force) { throw new Error(`Service method '${endpointName}' already exists. Use --force to override.`); } // Generate service method based on HTTP method using Result envelope let serviceMethod = ''; switch (method) { case 'GET': serviceMethod = ` async ${endpointName}(params?: any) { try { // TODO: Implement ${endpointName} logic // Example: const items = await ${moduleNameCapitalized}Model.find(params); const data = { message: '${endpointNameCapitalized} endpoint - implement your logic here', params }; return Result.success(data); } catch (error: any) { return Result.error('Failed to ${endpointName} ${moduleName}', error.message); } }`; break; case 'POST': serviceMethod = ` async ${endpointName}(data: any) { try { // TODO: Implement ${endpointName} logic // Example: const item = await ${moduleNameCapitalized}Model.create(data); const result = { message: '${endpointNameCapitalized} endpoint - implement your logic here', data }; return Result.success(result); } catch (error: any) { return Result.error('Failed to ${endpointName} ${moduleName}', error.message); } }`; break; case 'PUT': serviceMethod = ` async ${endpointName}(id: string, data: any) { try { // TODO: Implement ${endpointName} logic // Example: const item = await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, data, { new: true }); const result = { message: '${endpointNameCapitalized} endpoint - implement your logic here', id, data }; return Result.success(result); } catch (error: any) { return Result.error('Failed to ${endpointName} ${moduleName}', error.message); } }`; break; case 'DELETE': serviceMethod = ` async ${endpointName}(id: string) { try { // TODO: Implement ${endpointName} logic // Example: await ${moduleNameCapitalized}Model.findByIdAndUpdate(id, { status: 0 }, { new: true }); return Result.success(true); } catch (error: any) { return Result.error('Failed to ${endpointName} ${moduleName}', error.message); } }`; break; default: serviceMethod = ` async ${endpointName}(params?: any) { try { // TODO: Implement ${endpointName} logic const data = { message: '${endpointNameCapitalized} endpoint - implement your logic here', params }; return Result.success(data); } catch (error: any) { return Result.error('Failed to ${endpointName} ${moduleName}', error.message); } }`; } // If method exists and force is enabled, replace it if (serviceContent.includes(`async ${endpointName}(`) && force) { const methodRegex = new RegExp(`\\s*async ${endpointName}\\([^)]*\\)\\s*\\{[^}]*\\}`, 's'); serviceContent = serviceContent.replace(methodRegex, serviceMethod); } else { // Add method before the closing brace const lastBraceIndex = serviceContent.lastIndexOf('}'); serviceContent = serviceContent.slice(0, lastBraceIndex) + serviceMethod + '\n' + serviceContent.slice(lastBraceIndex); } fs.writeFileSync(servicePath, serviceContent); } async function addControllerMethod(modulePath, moduleName, moduleNameCapitalized, endpointName, endpointNameCapitalized, method, customPath, description, force) { // Resolve controller file path, supporting both singular and plural camelCase filenames const singularName = moduleName.endsWith('s') && moduleName.length > 1 ? moduleName.slice(0, -1) : moduleName; const toCamel = (s) => s.replace(/[-_]([a-z0-9])/g, (_, c) => c.toUpperCase()); const singularCamel = toCamel(singularName); const moduleCamel = toCamel(moduleName); const candidates = [ path.join(modulePath, 'controllers', `${singularCamel}.controller.ts`), path.join(modulePath, 'controllers', `${moduleCamel}.controller.ts`) ]; const controllerPath = candidates.find(p => fs.existsSync(p)); if (!controllerPath) { throw new Error(`Controller file not found: ${candidates.join(' or ')}`); } let controllerContent = fs.readFileSync(controllerPath, 'utf8'); // Ensure required imports exist for Result/ResponseCode/handleResponse/uuid const importsToEnsure = [ "import { Result } from '../../../shared/types/result';", "import { ResponseCode } from '../../../shared/constants/enums';", "import { handleResponse } from '../../../shared/utils/handle-response';", "import { v4 as uuidv4 } from 'uuid';" ]; for (const importLine of importsToEnsure) { if (!controllerContent.includes(importLine)) { controllerContent = importLine + '\n' + controllerContent; } } // Check if method already exists if (controllerContent.includes(`async ${endpointName}(`) && !force) { throw new Error(`Controller method '${endpointName}' already exists. Use --force to override.`); } // Generate controller method based on HTTP method using handleResponse let controllerMethod = ''; switch (method) { case 'GET': controllerMethod = ` /** * @openapi * /api/${moduleName}/${customPath}: * get: * summary: ${endpointNameCapitalized} * description: ${description} * tags: [${moduleNameCapitalized}s] * parameters: * - in: query * name: params * schema: * type: object * description: Query parameters * responses: * 200: * description: ${endpointNameCapitalized} executed successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: object * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request */ async ${endpointName}(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const params = req.query; const result = await this.service.${endpointName}(params); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to ${endpointName} ${moduleName}' }], null, requestIdentifier as string)); } }`; break; case 'POST': controllerMethod = ` /** * @openapi * /api/${moduleName}/${customPath}: * post: * summary: ${endpointNameCapitalized} * description: ${description} * tags: [${moduleNameCapitalized}s] * requestBody: * required: true * content: * application/json: * schema: * type: object * responses: * 200: * description: ${endpointNameCapitalized} executed successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: object * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request */ async ${endpointName}(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const data = req.body; const result = await this.service.${endpointName}(data); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to ${endpointName} ${moduleName}' }], null, requestIdentifier as string)); } }`; break; case 'PUT': controllerMethod = ` /** * @openapi * /api/${moduleName}/${customPath}/{id}: * put: * summary: ${endpointNameCapitalized} * description: ${description} * tags: [${moduleNameCapitalized}s] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: ${moduleNameCapitalized} ID * requestBody: * required: true * content: * application/json: * schema: * type: object * responses: * 200: * description: ${endpointNameCapitalized} executed successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: object * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request */ async ${endpointName}(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const { id } = req.params; const data = req.body; const result = await this.service.${endpointName}(id, data); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to ${endpointName} ${moduleName}' }], null, requestIdentifier as string)); } }`; break; case 'DELETE': controllerMethod = ` /** * @openapi * /api/${moduleName}/${customPath}/{id}: * delete: * summary: ${endpointNameCapitalized} * description: ${description} * tags: [${moduleNameCapitalized}s] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: ${moduleNameCapitalized} ID * responses: * 200: * description: ${endpointNameCapitalized} executed successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: object * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request */ async ${endpointName}(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const { id } = req.params; const result = await this.service.${endpointName}(id); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to ${endpointName} ${moduleName}' }], null, requestIdentifier as string)); } }`; break; default: controllerMethod = ` /** * @openapi * /api/${moduleName}/${customPath}: * ${method.toLowerCase()}: * summary: ${endpointNameCapitalized} * description: ${description} * tags: [${moduleNameCapitalized}s] * responses: * 200: * description: ${endpointNameCapitalized} executed successfully * content: * application/json: * schema: * type: object * properties: * Data: * type: object * StatusCode: * type: integer * example: 1000 * 400: * description: Bad request */ async ${endpointName}(req, res) { const requestIdentifier = req.headers['x-request-id'] || uuidv4(); try { const params = req.query || req.body || req.params; const result = await this.service.${endpointName}(params); result.requestIdentifier = requestIdentifier as string; handleResponse(res, result); } catch (error: any) { handleResponse(res, new Result(null, ResponseCode.Error, [{ message: 'Failed to ${endpointName} ${moduleName}' }], null, requestIdentifier as string)); } }`; } // If method exists and force is enabled, replace it if (controllerContent.includes(`async ${endpointName}(`) && force) { const methodRegex = new RegExp(`\\s*async ${endpointName}\\([^)]*\\)\\s*\\{[^}]*\\}`, 's'); controllerContent = controllerContent.replace(methodRegex, controllerMethod); } else { // Add method before the closing brace const lastBraceIndex = controllerContent.lastIndexOf('}'); controllerContent = controllerContent.slice(0, lastBraceIndex) + controllerMethod + '\n' + controllerContent.slice(lastBraceIndex); } fs.writeFileSync(controllerPath, controllerContent); } async function addRoute(modulePath, moduleName, moduleNameCapitalized, endpointName, endpointNameCapitalized, method, customPath) { // Resolve route file path, supporting both singular and plural camelCase filenames const singularName = moduleName.endsWith('s') && moduleName.length > 1 ? moduleName.slice(0, -1) : moduleName; const toCamel = (s) => s.replace(/[-_]([a-z0-9])/g, (_, c) => c.toUpperCase()); const singularCamel = toCamel(singularName); const moduleCamel = toCamel(moduleName); const candidates = [ path.join(modulePath, 'routes', `${singularCamel}.routes.ts`), path.join(modulePath, 'routes', `${moduleCamel}.routes.ts`) ]; const routePath = candidates.find(p => fs.existsSync(p)); if (!routePath) { throw new Error(`Route file not found: ${candidates.join(' or ')}`); } let routeContent = fs.readFileSync(routePath, 'utf8'); // Generate route based on HTTP method let routeLine = ''; switch (method) { case 'GET': routeLine = `router.get('/${customPath}', (req, res) => controller.${endpointName}(req, res));`; break; case 'POST': routeLine = `router.post('/${customPath}', (req, res) => controller.${endpointName}(req, res));`; break; case 'PUT': routeLine = `router.put('/${customPath}/:id', (req, res) => controller.${endpointName}(req, res));`; break; case 'DELETE': routeLine = `router.delete('/${customPath}/:id', (req, res) => controller.${endpointName}(req, res));`; break; default: routeLine = `router.${method.toLowerCase()}('/${customPath}', (req, res) => controller.${endpointName}(req, res));`; } // Add route before the export statement const exportIndex = routeContent.indexOf('export default router;'); if (exportIndex !== -1) { routeContent = routeContent.slice(0, exportIndex) + routeLine + '\n\n' + routeContent.slice(exportIndex); } else { // If no export found, add at the end routeContent += '\n' + routeLine + '\n'; } fs.writeFileSync(routePath, routeContent); } //# sourceMappingURL=api-generator.js.map