heyjarvis
Version:
J.A.R.V.I.S. - Advanced Node.js MVC Framework with ORM, built-in validation, soft delete, and query builder
918 lines (771 loc) • 32.9 kB
JavaScript
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { getText } = require('../config');
// Helper functions
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const pluralize = (str) => str.endsWith('y') ? str.slice(0, -1) + 'ies' : str.endsWith('s') ? str : str + 's';
const writeFile = (filePath, content) => {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, content);
};
// Main generate function that handles all types
const generateModel = async (type, name, options = {}) => {
try {
switch (type.toLowerCase()) {
case 'model':
return await generateModelFile(name, options);
case 'controller':
return await generateControllerFile(name, options);
case 'route':
return await generateRouteFile(name, options);
case 'migration':
return await generateMigrationFile(name, options);
case 'all':
return await generateAllFiles(name, options);
default:
throw new Error(getText('errors.unknownGenerator', { type }));
}
} catch (error) {
console.error(chalk.red(getText('errors.failedToGenerate', { type })), error.message);
throw error;
}
};
// Enhanced Model Generator
const generateModelFile = async (name, options) => {
const modelName = capitalize(name);
const tableName = pluralize(name.toLowerCase());
let fields;
if (!options.fields) {
console.log(chalk.cyan(getText('model.noFields', { model: modelName }) + '\n'));
fields = await createFieldsInteractively();
} else {
fields = parseFields(options.fields);
}
const hasRelationships = await askForRelationships(modelName);
let relationships = '';
if (hasRelationships.length > 0) {
relationships = generateRelationships(hasRelationships);
}
const modelContent = `const { DataTypes } = require('sequelize');
const { v4: uuidv4 } = require('uuid');
const sequelize = require('../config/database');
const ${modelName} = sequelize.define('${modelName}', {
id: {
type: DataTypes.UUID,
defaultValue: () => uuidv4(),
primaryKey: true,
allowNull: false
}${fields},
visible: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: '${tableName}',
paranoid: true,
timestamps: true,
underscored: true,
indexes: [
{
name: '${tableName}_visible_idx',
fields: ['visible']
},
{
name: '${tableName}_created_at_idx',
fields: ['created_at']
}
],
hooks: {
beforeCreate: (instance) => {
if (!instance.id) {
instance.id = uuidv4();
}
}
}
});
${relationships}
module.exports = ${modelName};`;
const filePath = path.join('src/models', `${modelName}.js`);
writeFile(filePath, modelContent);
console.log(chalk.green(getText('model.success.generated', { path: filePath })));
console.log(chalk.blue(getText('model.success.table', { table: tableName })));
console.log(chalk.blue(getText('model.success.primaryKey')));
console.log(chalk.blue(getText('model.success.features')));
const fieldCount = fields.split('type: DataTypes.').length - 1;
console.log(chalk.cyan(getText('model.success.fieldCount', { count: fieldCount })));
return filePath;
};
// Interactive field creation
const createFieldsInteractively = async () => {
const fields = [];
let addMore = true;
while (addMore) {
console.log(chalk.cyan(`\n${getText('model.addingField', { count: fields.length + 1 })}:`));
const fieldData = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: getText('model.fieldName'),
validate: (input) => {
if (!input) return getText('validation.required', { field: 'Field name' });
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input)) return getText('validation.invalidFormat', { field: 'Field name' });
return true;
}
},
{
type: 'list',
name: 'type',
message: getText('model.fieldType'),
choices: [
{ name: getText('model.fieldTypes.string'), value: 'string' },
{ name: getText('model.fieldTypes.text'), value: 'text' },
{ name: getText('model.fieldTypes.integer'), value: 'integer' },
{ name: getText('model.fieldTypes.decimal'), value: 'decimal' },
{ name: getText('model.fieldTypes.boolean'), value: 'boolean' },
{ name: getText('model.fieldTypes.date'), value: 'date' },
{ name: getText('model.fieldTypes.datetime'), value: 'datetime' },
{ name: getText('model.fieldTypes.email'), value: 'email' },
{ name: getText('model.fieldTypes.uuid'), value: 'uuid' },
{ name: getText('model.fieldTypes.json'), value: 'json' }
]
},
{
type: 'checkbox',
name: 'options',
message: getText('model.fieldOptions'),
choices: [
{ name: getText('model.fieldOptions.required'), value: 'required' },
{ name: getText('model.fieldOptions.unique'), value: 'unique' },
{ name: getText('model.fieldOptions.indexed'), value: 'indexed' }
]
},
{
type: 'input',
name: 'defaultValue',
message: getText('model.defaultValue'),
when: (answers) => !answers.options.includes('required')
}
]);
let fieldDef = `,
${fieldData.name}: {
type: DataTypes.${getDataType(fieldData.type)}`;
if (fieldData.options.includes('required')) {
fieldDef += ',\n allowNull: false';
}
if (fieldData.options.includes('unique')) {
fieldDef += ',\n unique: true';
}
if (fieldData.options.includes('indexed')) {
fieldDef += ',\n indexes: [{ fields: [\'' + fieldData.name + '\'] }]';
}
if (fieldData.defaultValue) {
if (['true', 'false'].includes(fieldData.defaultValue.toLowerCase())) {
fieldDef += `,\n defaultValue: ${fieldData.defaultValue.toLowerCase()}`;
} else if (!isNaN(fieldData.defaultValue)) {
fieldDef += `,\n defaultValue: ${fieldData.defaultValue}`;
} else {
fieldDef += `,\n defaultValue: '${fieldData.defaultValue}'`;
}
}
if (fieldData.type === 'email') {
fieldDef += `,\n validate: {
isEmail: {
msg: 'Must be a valid email address'
}
}`;
}
fieldDef += '\n }';
fields.push(fieldDef);
const continueAdding = await inquirer.prompt([
{
type: 'confirm',
name: 'addMore',
message: getText('model.addMoreFields'),
default: false
}
]);
addMore = continueAdding.addMore;
}
return fields.join('');
};
// Ask for relationships
const askForRelationships = async (modelName) => {
const hasRelations = await inquirer.prompt([
{
type: 'confirm',
name: 'addRelationships',
message: getText('model.addRelationships', { model: modelName }),
default: false
}
]);
if (!hasRelations.addRelationships) return [];
const relationships = [];
let addMore = true;
while (addMore) {
const relationData = await inquirer.prompt([
{
type: 'list',
name: 'type',
message: getText('model.relationshipType'),
choices: [
{ name: getText('model.relationships.belongsTo'), value: 'belongsTo' },
{ name: getText('model.relationships.hasMany'), value: 'hasMany' },
{ name: getText('model.relationships.hasOne'), value: 'hasOne' },
{ name: getText('model.relationships.belongsToMany'), value: 'belongsToMany' }
]
},
{
type: 'input',
name: 'model',
message: getText('model.targetModel'),
validate: (input) => input.length > 0 || getText('validation.required', { field: 'Model name' })
},
{
type: 'input',
name: 'foreignKey',
message: getText('model.foreignKey'),
when: (answers) => ['belongsTo', 'hasMany', 'hasOne'].includes(answers.type)
}
]);
relationships.push(relationData);
const continueAdding = await inquirer.prompt([
{
type: 'confirm',
name: 'addMore',
message: getText('model.addMoreRelations'),
default: false
}
]);
addMore = continueAdding.addMore;
}
return relationships;
};
// Generate relationship code
const generateRelationships = (relationships) => {
if (relationships.length === 0) return '';
let code = '// Model Relationships\n';
relationships.forEach(rel => {
const targetModel = capitalize(rel.model);
const options = rel.foreignKey ? `{ foreignKey: '${rel.foreignKey}' }` : '';
code += `${rel.type === 'belongsToMany' ? `// ` : ''}// ${rel.type} relationship with ${targetModel}\n`;
code += `// ${targetModel}.${rel.type}(require('./${targetModel}')${options ? `, ${options}` : ''});\n\n`;
});
return code;
};
// Enhanced field parser
const parseFields = (fieldsString) => {
if (!fieldsString) {
return `,
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: {
msg: 'Name cannot be empty'
}
}
}`;
}
const cleanInput = fieldsString.replace(/\s+/g, ' ').trim();
const fields = cleanInput.split(',').map(field => field.trim()).filter(field => field.length > 0);
if (fields.length === 0) {
return `,
name: {
type: DataTypes.STRING,
allowNull: false
}`;
}
const parsedFields = fields.map((field) => {
const parts = field.split(':');
const name = parts[0] ? parts[0].trim() : '';
const type = parts[1] ? parts[1].trim() : 'string';
const options = parts.slice(2).map(opt => opt.trim()).filter(opt => opt.length > 0);
if (!name) return null;
const dataType = getDataType(type);
let fieldDef = `,
${name}: {
type: DataTypes.${dataType}`;
if (options.includes('required')) {
fieldDef += ',\n allowNull: false';
}
if (options.includes('unique')) {
fieldDef += ',\n unique: true';
}
if (options.includes('nullable')) {
fieldDef += ',\n allowNull: true';
}
const defaultOption = options.find(opt => opt.startsWith('default:'));
if (defaultOption) {
const defaultValue = defaultOption.split('default:')[1];
if (['true', 'false'].includes(defaultValue)) {
fieldDef += `,\n defaultValue: ${defaultValue}`;
} else if (!isNaN(defaultValue)) {
fieldDef += `,\n defaultValue: ${defaultValue}`;
} else {
fieldDef += `,\n defaultValue: '${defaultValue}'`;
}
}
if (type === 'email') {
fieldDef += `,\n validate: {
isEmail: {
msg: 'Must be a valid email address'
}
}`;
}
fieldDef += '\n }';
return fieldDef;
}).filter(field => field !== null);
return parsedFields.join('');
};
// Helper function to map field types
const getDataType = (type) => {
const typeMap = {
'string': 'STRING',
'text': 'TEXT',
'integer': 'INTEGER',
'int': 'INTEGER',
'decimal': 'DECIMAL(10,2)',
'float': 'FLOAT',
'double': 'DOUBLE',
'boolean': 'BOOLEAN',
'bool': 'BOOLEAN',
'date': 'DATEONLY',
'datetime': 'DATE',
'timestamp': 'DATE',
'time': 'TIME',
'json': 'JSON',
'uuid': 'UUID',
'email': 'STRING'
};
return typeMap[type.toLowerCase()] || 'STRING';
};
// Generate Migration File
const generateMigrationFile = async (name, options) => {
const modelName = capitalize(name);
const tableName = pluralize(name.toLowerCase());
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
const fields = parseFields(options.fields);
const migrationContent = `'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('${tableName}', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
}${fields.replace(/DataTypes\./g, 'Sequelize.')},
visible: {
type: Sequelize.BOOLEAN,
defaultValue: true
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
},
deleted_at: {
type: Sequelize.DATE
}
});
await queryInterface.addIndex('${tableName}', ['visible']);
await queryInterface.addIndex('${tableName}', ['created_at']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('${tableName}');
}
};`;
const filePath = path.join('src/migrations', `${timestamp}-create-${tableName}.js`);
writeFile(filePath, migrationContent);
console.log(chalk.green(getText('migration.generated', { path: filePath })));
return filePath;
};
// Generate all files at once
const generateAllFiles = async (name, options) => {
console.log(chalk.cyan(getText('all.generating', { name }) + '\n'));
try {
const modelPath = await generateModelFile(name, options);
const controllerPath = await generateControllerFile(name, options);
const routePath = await generateRouteFile(name, options);
const migrationPath = await generateMigrationFile(name, options);
console.log(chalk.green('\n' + getText('all.success', { name })));
console.log(chalk.cyan(getText('all.filesCreated')));
console.log(chalk.white(` ${modelPath}`));
console.log(chalk.white(` ${controllerPath}`));
console.log(chalk.white(` ${routePath}`));
console.log(chalk.white(` ${migrationPath}`));
return { modelPath, controllerPath, routePath, migrationPath };
} catch (error) {
console.error(chalk.red('Failed to generate complete resource:'), error.message);
throw error;
}
};
// Controller Generator
const generateControllerFile = async (name, options) => {
const modelName = capitalize(name);
const controllerName = `${modelName}Controller`;
const routeName = name.toLowerCase();
const controllerContent = `const ${modelName} = require('../models/${modelName}');
const { sendSuccess, sendError, sendNotFound } = require('../helpers/response');
const { paginate } = require('../helpers/pagination');
const { Op } = require('sequelize');
const { validate: isValidUUID } = require('uuid');
const ${controllerName} = {
// GET /api/${pluralize(routeName)}
index: async (req, res) => {
try {
const { page = 1, limit = 10, search, sortBy = 'created_at', sortOrder = 'DESC' } = req.query;
let whereClause = { visible: true };
if (search) {
whereClause[Op.or] = [
{ name: { [Op.like]: \`%\${search}%\` } }
];
}
const result = await paginate(${modelName}, {
page,
limit,
where: whereClause,
order: [[sortBy, sortOrder.toUpperCase()]]
});
sendSuccess(res, '${modelName}s retrieved successfully', result);
} catch (error) {
console.error('${modelName} Index Error:', error);
sendError(res, 'Failed to retrieve ${pluralize(routeName)}', 500);
}
},
// GET /api/${pluralize(routeName)}/:id
show: async (req, res) => {
try {
const { id } = req.params;
if (!isValidUUID(id)) {
return sendError(res, 'Invalid ${routeName} ID format', 400);
}
const ${routeName} = await ${modelName}.findOne({
where: { id, visible: true }
});
if (!${routeName}) {
return sendNotFound(res, '${modelName} not found');
}
sendSuccess(res, '${modelName} retrieved successfully', ${routeName});
} catch (error) {
console.error('${modelName} Show Error:', error);
sendError(res, 'Failed to retrieve ${routeName}', 500);
}
},
// POST /api/${pluralize(routeName)}
store: async (req, res) => {
try {
const ${routeName} = await ${modelName}.create(req.body);
sendSuccess(res, '${modelName} created successfully', ${routeName}, 201);
} catch (error) {
console.error('${modelName} Store Error:', error);
if (error.name === 'SequelizeValidationError') {
const errors = error.errors.map(err => ({
field: err.path,
message: err.message
}));
return sendError(res, 'Validation failed', 400, errors);
}
sendError(res, 'Failed to create ${routeName}', 400);
}
},
// PUT /api/${pluralize(routeName)}/:id
update: async (req, res) => {
try {
const { id } = req.params;
if (!isValidUUID(id)) {
return sendError(res, 'Invalid ${routeName} ID format', 400);
}
const [updated] = await ${modelName}.update(req.body, {
where: { id, visible: true }
});
if (!updated) {
return sendNotFound(res, '${modelName} not found');
}
const ${routeName} = await ${modelName}.findByPk(id);
sendSuccess(res, '${modelName} updated successfully', ${routeName});
} catch (error) {
console.error('${modelName} Update Error:', error);
sendError(res, 'Failed to update ${routeName}', 400);
}
},
// DELETE /api/${pluralize(routeName)}/:id
destroy: async (req, res) => {
try {
const { id } = req.params;
if (!isValidUUID(id)) {
return sendError(res, 'Invalid ${routeName} ID format', 400);
}
const deleted = await ${modelName}.destroy({
where: { id }
});
if (!deleted) {
return sendNotFound(res, '${modelName} not found');
}
sendSuccess(res, '${modelName} deleted successfully', { id });
} catch (error) {
console.error('${modelName} Destroy Error:', error);
sendError(res, 'Failed to delete ${routeName}', 500);
}
}
};
module.exports = ${controllerName};`;
const filePath = path.join('src/controllers', `${controllerName}.js`);
writeFile(filePath, controllerContent);
console.log(chalk.green(getText('controller.generated', { path: filePath })));
return filePath;
};
// Route Generator - Smart & Simple
const generateRouteFile = async (name, options) => {
const routeName = name.toLowerCase();
const routePlural = pluralize(routeName);
const modelName = capitalize(name);
const controllerName = `${modelName}Controller`;
// Ask for validation middleware if not specified
let withValidation = options.withValidation || false;
if (!options.withValidation && !options.noValidation) {
const validationChoice = await inquirer.prompt([
{
type: 'confirm',
name: 'addValidation',
message: getText('route.askValidation', { model: modelName }) || `Add validation middleware for ${modelName}?`,
default: false
}
]);
withValidation = validationChoice.addValidation;
}
// Create route content
let routeContent = `const express = require('express');
const router = express.Router();
const ${controllerName} = require('../controllers/${controllerName}');`;
if (withValidation) {
// Generate validation middleware first
await generateValidationMiddleware(name);
routeContent += `
const { validate${modelName}Create, validate${modelName}Update } = require('../middleware/validation/${routeName}Validation');
const { validateUUID } = require('../middleware/validation/common');`;
}
routeContent += `
// Routes for ${modelName}
router.get('/', ${controllerName}.index); // GET /api/${routePlural}
router.get('/:id', ${withValidation ? 'validateUUID(\'id\'), ' : ''}${controllerName}.show); // GET /api/${routePlural}/:id
router.post('/', ${withValidation ? `validate${modelName}Create, ` : ''}${controllerName}.store); // POST /api/${routePlural}
router.put('/:id', ${withValidation ? `validateUUID(\'id\'), validate${modelName}Update, ` : ''}${controllerName}.update); // PUT /api/${routePlural}/:id
router.delete('/:id', ${withValidation ? 'validateUUID(\'id\'), ' : ''}${controllerName}.destroy); // DELETE /api/${routePlural}/:id
module.exports = router;`;
const filePath = path.join('src/routes', `${routePlural}.js`);
writeFile(filePath, routeContent);
// Auto register route in index.js
await registerRouteAutomatically(routePlural);
// Create common validation if needed
if (withValidation) {
await ensureCommonValidationExists();
}
console.log(chalk.green(getText('route.generated', { path: filePath })));
if (withValidation) {
console.log(chalk.blue(`🛡️ Validation middleware: src/middleware/validation/${routeName}Validation.js`));
}
console.log(chalk.green(`🔄 Route automatically registered in src/routes/index.js`));
return filePath;
};
// Generate validation middleware (only when needed)
const generateValidationMiddleware = async (name) => {
const modelName = capitalize(name);
const routeName = name.toLowerCase();
// Try to read existing model to get field information
let fields = { createRules: [], updateRules: [] };
try {
const modelPath = path.join('src/models', `${modelName}.js`);
if (fs.existsSync(modelPath)) {
const modelContent = fs.readFileSync(modelPath, 'utf8');
fields = parseModelForValidation(modelContent);
} else {
fields = getDefaultValidationFields();
}
} catch (error) {
fields = getDefaultValidationFields();
}
const validationContent = `const { body, validationResult } = require('express-validator');
const { sendError } = require('../../helpers/response');
// Validation rules for ${modelName} creation
const validate${modelName}Create = [
${fields.createRules.map(rule => ` ${rule}`).join(',\n')},
// Check validation result
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const formattedErrors = errors.array().map(error => ({
field: error.path,
message: error.msg,
value: error.value
}));
return sendError(res, 'Validation failed', 400, formattedErrors);
}
next();
}
];
// Validation rules for ${modelName} update
const validate${modelName}Update = [
${fields.updateRules.map(rule => ` ${rule}`).join(',\n')},
// Check validation result
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const formattedErrors = errors.array().map(error => ({
field: error.path,
message: error.msg,
value: error.value
}));
return sendError(res, 'Validation failed', 400, formattedErrors);
}
next();
}
];
module.exports = {
validate${modelName}Create,
validate${modelName}Update
};`;
const validationDir = path.join('src/middleware/validation');
const validationFilePath = path.join(validationDir, `${routeName}Validation.js`);
if (!fs.existsSync(validationDir)) {
fs.mkdirSync(validationDir, { recursive: true });
}
writeFile(validationFilePath, validationContent);
return validationFilePath;
};
// Parse existing model file for validation rules
const parseModelForValidation = (modelContent) => {
const createRules = [];
const updateRules = [];
// Extract field definitions from model
const fieldMatches = modelContent.match(/(\w+):\s*\{[\s\S]*?type:\s*DataTypes\.(\w+)[\s\S]*?\}/g);
if (fieldMatches) {
fieldMatches.forEach(fieldMatch => {
const nameMatch = fieldMatch.match(/(\w+):/);
const typeMatch = fieldMatch.match(/DataTypes\.(\w+)/);
const allowNullMatch = fieldMatch.match(/allowNull:\s*false/);
const isEmailMatch = fieldMatch.match(/isEmail/);
if (nameMatch && typeMatch) {
const fieldName = nameMatch[1];
const fieldType = typeMatch[1].toLowerCase();
// Skip system fields
if (['id', 'visible', 'created_at', 'updated_at', 'deleted_at'].includes(fieldName)) {
return;
}
let createRule = '';
let updateRule = '';
if (isEmailMatch) {
createRule = `body('${fieldName}').isEmail().withMessage('${fieldName} must be a valid email')`;
updateRule = `body('${fieldName}').optional().isEmail().withMessage('${fieldName} must be a valid email')`;
} else {
switch (fieldType) {
case 'string':
createRule = `body('${fieldName}').isString().withMessage('${fieldName} must be a string')`;
updateRule = `body('${fieldName}').optional().isString().withMessage('${fieldName} must be a string')`;
break;
case 'integer':
createRule = `body('${fieldName}').isInt().withMessage('${fieldName} must be an integer')`;
updateRule = `body('${fieldName}').optional().isInt().withMessage('${fieldName} must be an integer')`;
break;
case 'boolean':
createRule = `body('${fieldName}').isBoolean().withMessage('${fieldName} must be true or false')`;
updateRule = `body('${fieldName}').optional().isBoolean().withMessage('${fieldName} must be true or false')`;
break;
case 'uuid':
createRule = `body('${fieldName}').isUUID().withMessage('${fieldName} must be a valid UUID')`;
updateRule = `body('${fieldName}').optional().isUUID().withMessage('${fieldName} must be a valid UUID')`;
break;
default:
createRule = `body('${fieldName}').notEmpty().withMessage('${fieldName} is required')`;
updateRule = `body('${fieldName}').optional()`;
}
}
// Add required validation
if (allowNullMatch) {
createRule += `.notEmpty().withMessage('${fieldName} is required')`;
}
createRules.push(createRule);
updateRules.push(updateRule);
}
});
}
return { createRules, updateRules };
};
// Default validation fields
const getDefaultValidationFields = () => ({
createRules: [`body('name').isString().notEmpty().withMessage('Name is required')`],
updateRules: [`body('name').optional().isString().withMessage('Name must be a string')`]
});
// Auto register route in index.js
const registerRouteAutomatically = async (routePlural) => {
const indexPath = path.join('src/routes/index.js');
const routeRegistration = `router.use('/${routePlural}', require('./${routePlural}'));`;
try {
let indexContent = '';
if (fs.existsSync(indexPath)) {
indexContent = fs.readFileSync(indexPath, 'utf8');
} else {
indexContent = `const express = require('express');
const router = express.Router();
// API Routes
router.get('/', (req, res) => {
res.json({
message: 'J.A.R.V.I.S. API is running',
version: '1.0.0',
timestamp: new Date().toISOString()
});
});
module.exports = router;`;
}
// Check if route already registered
if (indexContent.includes(`'/${routePlural}'`)) {
return;
}
// Add route registration before module.exports
const lines = indexContent.split('\n');
const exportIndex = lines.findIndex(line => line.includes('module.exports'));
if (exportIndex > -1) {
lines.splice(exportIndex, 0, routeRegistration, '');
} else {
lines.push('', routeRegistration);
}
const updatedContent = lines.join('\n');
fs.writeFileSync(indexPath, updatedContent);
} catch (error) {
console.warn(chalk.yellow(`⚠️ Could not auto-register route: ${error.message}`));
}
};
// Ensure common validation middleware exists
const ensureCommonValidationExists = async () => {
const commonValidationPath = path.join('src/middleware/validation/common.js');
if (fs.existsSync(commonValidationPath)) {
return;
}
const commonValidationContent = `const { param } = require('express-validator');
const { validate: isValidUUID } = require('uuid');
// UUID validation middleware
const validateUUID = (paramName = 'id') => {
return param(paramName)
.custom((value) => {
if (!isValidUUID(value)) {
throw new Error(\`\${paramName} must be a valid UUID\`);
}
return true;
});
};
module.exports = {
validateUUID
};`;
writeFile(commonValidationPath, commonValidationContent);
};
module.exports = {
generateModel
};