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
JavaScript
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);
});
});`}
});`;
}