UNPKG

modurize

Version:

Intelligent CLI tool to scaffold dynamic, context-aware modules for Node.js apps with smart CRUD generation and database integration

326 lines (272 loc) 11.3 kB
import { ensureDir, writeFile } from './fileUtils.js'; import { CRUDGenerator } from './crudGenerator.js'; import { ModelGenerator } from './modelGenerator.js'; import path from 'path'; export function createModule(name, dir, type = 'func', options = {}) { ensureDir(dir); const { useTypeScript = false, includeTests = false, analysis = {} } = options; // Initialize dynamic generators const crudGenerator = new CRUDGenerator(name, analysis); const modelGenerator = new ModelGenerator(name, analysis); const files = { [`${name}.controller.${useTypeScript ? 'ts' : 'js'}`]: crudGenerator.generateController(useTypeScript, type === 'class'), [`${name}.service.${useTypeScript ? 'ts' : 'js'}`]: crudGenerator.generateService(useTypeScript, type === 'class'), [`${name}.routes.${useTypeScript ? 'ts' : 'js'}`]: crudGenerator.generateRoutes(useTypeScript, type === 'class'), [`${name}.model.${useTypeScript ? 'ts' : 'js'}`]: modelGenerator.generateModel(useTypeScript), [`${name}.middleware.${useTypeScript ? 'ts' : 'js'}`]: middlewareTemplate(name, useTypeScript), [`${name}.validator.${useTypeScript ? 'ts' : 'js'}`]: validatorTemplate(name, useTypeScript, crudGenerator.generateFields()) }; // Add test files if requested if (includeTests) { files[`${name}.controller.test.${useTypeScript ? 'ts' : 'js'}`] = testTemplate(name, 'controller', type, useTypeScript, analysis); files[`${name}.service.test.${useTypeScript ? 'ts' : 'js'}`] = testTemplate(name, 'service', type, useTypeScript, analysis); } for (const [fileName, content] of Object.entries(files)) { writeFile(path.join(dir, fileName), content); } const typeText = type === 'class' ? 'Class-based' : 'Function-based'; const tsText = useTypeScript ? ' (TypeScript)' : ''; const testText = includeTests ? ' with tests' : ''; const dynamicText = analysis.database && analysis.database !== 'none' ? ' with database integration' : ''; console.log(`✅ ${typeText} module '${name}' created${tsText}${testText}${dynamicText}.`); // Show dynamic features detected if (analysis.database && analysis.database !== 'none') { console.log(` 📊 Database: ${analysis.database}`); } if (analysis.framework && analysis.framework !== 'express') { console.log(` 🚀 Framework: ${analysis.framework}`); } } const cap = str => str.charAt(0).toUpperCase() + str.slice(1); // 🧱 MIDDLEWARE function middlewareTemplate(name, useTypeScript) { const tsImports = useTypeScript ? `import { Request, Response, NextFunction } from 'express';\n` : ''; const tsTypes = useTypeScript ? ': Request, res: Response, next: NextFunction' : ''; return `// ${name}.middleware.${useTypeScript ? 'ts' : 'js'} ${tsImports}export function exampleMiddleware(req${tsTypes}) { console.log("${name} middleware active"); console.log("Request URL:", req.url); console.log("Request method:", req.method); // Add your middleware logic here next(); } export function validate${cap(name)}Input(req${tsTypes}) { // Add validation logic here const { body } = req; if (!body || Object.keys(body).length === 0) { return res.status(400).json({ error: "Request body is required" }); } next(); } export function authenticate${cap(name)}Access(req${tsTypes}) { // Add authentication logic here // Example: Check if user has permission to access ${name} resources const user = req.user; // Assuming user is attached by auth middleware if (!user) { return res.status(401).json({ error: "Authentication required" }); } // Add role-based access control if needed // if (user.role !== 'admin' && user.role !== '${name}_manager') { // return res.status(403).json({ error: "Insufficient permissions" }); // } next(); }`; } // 🧱 VALIDATOR function validatorTemplate(name, useTypeScript, fields = {}) { const tsTypes = useTypeScript ? `(data: any): boolean` : '(data)'; const fieldNames = Object.keys(fields).filter(f => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt'); return `// ${name}.validator.${useTypeScript ? 'ts' : 'js'} export function validate${cap(name)}Input${tsTypes} { if (!data || typeof data !== 'object') { return false; } // Required field validation const requiredFields = [${fieldNames.filter(f => fields[f].required).map(f => `'${f}'`).join(', ')}]; for (const field of requiredFields) { if (!data[field] || data[field].toString().trim() === '') { return false; } } // Type validation ${fieldNames.map(fieldName => { const field = fields[fieldName]; if (field.type === 'string') { return `if (data.${fieldName} && typeof data.${fieldName} !== 'string') return false;`; } else if (field.type === 'number') { return `if (data.${fieldName} && (typeof data.${fieldName} !== 'number' || isNaN(data.${fieldName}))) return false;`; } else if (field.type === 'boolean') { return `if (data.${fieldName} !== undefined && typeof data.${fieldName} !== 'boolean') return false;`; } return ''; }).filter(Boolean).join('\n ')} return true; } export function sanitize${cap(name)}Data(data${useTypeScript ? ': any' : ''}) { const sanitized = { ...data }; // String sanitization ${fieldNames.filter(f => fields[f].type === 'string').map(fieldName => `if (sanitized.${fieldName}) sanitized.${fieldName} = sanitized.${fieldName}.trim();` ).join('\n ')} // Email validation ${fieldNames.filter(f => f === 'email').map(fieldName => `if (sanitized.${fieldName}) { const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/; if (!emailRegex.test(sanitized.${fieldName})) { throw new Error('Invalid email format'); } }` ).join('\n ')} // Number validation ${fieldNames.filter(f => fields[f].type === 'number').map(fieldName => `if (sanitized.${fieldName} !== undefined) { const num = Number(sanitized.${fieldName}); if (isNaN(num)) { throw new Error('${fieldName} must be a valid number'); } sanitized.${fieldName} = num; }` ).join('\n ')} return sanitized; } export function validate${cap(name)}Update(data${useTypeScript ? ': any' : ''}) { // Less strict validation for updates - only validate provided fields if (!data || typeof data !== 'object') { return false; } // Check if at least one field is provided const providedFields = Object.keys(data).filter(key => data[key] !== undefined && data[key] !== null ); if (providedFields.length === 0) { return false; } // Validate only provided fields for (const field of providedFields) { if (data[field] !== undefined && data[field] !== null) { // Add field-specific validation here } } return true; }`; } function testTemplate(name, fileType, codeType, useTypeScript, analysis = {}) { const capName = cap(name); const className = codeType === 'class' ? `${capName}${fileType === 'controller' ? 'Controller' : 'Service'}` : ''; const database = analysis.database; return `// ${name}.${fileType}.test.${useTypeScript ? 'ts' : 'js'} import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; ${useTypeScript ? `import { Request, Response } from 'express';\n` : ''} ${codeType === 'class' ? `import { ${className} } from './${name}.${fileType}.${useTypeScript ? 'ts' : 'js'}';` : `import { getAll, getById, create, update, delete${capName} } from './${name}.${fileType}.${useTypeScript ? 'ts' : 'js'}';` } // Mock database if needed ${database && database !== 'none' ? ` // Mock ${database} connection jest.mock('./${name}.model.${useTypeScript ? 'ts' : 'js'}', () => ({ ${capName}Model: { find: jest.fn(), findById: jest.fn(), create: jest.fn(), findByIdAndUpdate: jest.fn(), findByIdAndDelete: jest.fn() } }));` : ''} describe('${capName} ${fileType}', () => { beforeEach(() => { jest.clearAllMocks(); }); ${fileType === 'controller' ? ` describe('getAll', () => { it('should return all ${name}s', async () => { const mockReq = { query: {} } as Request; const mockRes = { json: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis() } as unknown as Response; ${codeType === 'class' ? `await ${className}.getAll(mockReq, mockRes);` : `await getAll(mockReq, mockRes);` } expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ success: true, data: expect.any(Array), count: expect.any(Number) }) ); }); }); describe('getById', () => { it('should return ${name} by id', async () => { const mockReq = { params: { id: 'test-id' } } as Request; const mockRes = { json: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis() } as unknown as Response; ${codeType === 'class' ? `await ${className}.getById(mockReq, mockRes);` : `await getById(mockReq, mockRes);` } expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ success: true, data: expect.any(Object) }) ); }); }); describe('create', () => { it('should create new ${name}', async () => { const mockReq = { body: { name: 'Test ${name}', description: 'Test description' } } as Request; const mockRes = { json: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis() } as unknown as Response; ${codeType === 'class' ? `await ${className}.create(mockReq, mockRes);` : `await create(mockReq, mockRes);` } expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ success: true, data: expect.any(Object) }) ); }); });` : ` describe('findAll', () => { it('should return all ${name}s', async () => { ${codeType === 'class' ? `const result = await ${className}.findAll();` : `const result = await getAll();` } expect(Array.isArray(result)).toBe(true); }); }); describe('findById', () => { it('should return ${name} by id', async () => { ${codeType === 'class' ? `const result = await ${className}.findById('test-id');` : `const result = await getById('test-id');` } expect(result).toBeDefined(); }); }); describe('create', () => { it('should create new ${name}', async () => { const testData = { name: 'Test ${name}', description: 'Test description' }; ${codeType === 'class' ? `const result = await ${className}.create(testData);` : `const result = await create(testData);` } expect(result).toHaveProperty('id'); expect(result).toHaveProperty('name', testData.name); }); });`} });`; }