@diagramers/cli
Version:
Diagramers CLI - Command-line tools for managing Diagramers projects
1,092 lines (1,067 loc) • 49.1 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.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