UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

451 lines (384 loc) 12.9 kB
#!/usr/bin/env node /** * Configuration Validator CLI * Validates CRUD configuration files and provides detailed feedback */ const chalk = require('chalk'); const ora = require('ora'); const figlet = require('figlet'); const boxen = require('boxen'); const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); const inquirer = require('inquirer'); const gradient = require('gradient-string'); const { program } = require('commander'); // Import validation utilities const { validateConfig, displayValidationResults } = require('./utils/config-validator'); // Constants for styling const titleGradient = gradient(['#34A853', '#4285F4']); /** * Display the banner */ function displayBanner() { console.log( titleGradient.multiline( figlet.textSync('Config Validator', { font: 'Small', horizontalLayout: 'default' }) ) ); console.log( boxen( `${chalk.bold('KIRA Configuration Validator')} ${chalk.dim('v1.0.0')}\n` + `Validate CRUD configuration files and detect potential issues`, { padding: 1, margin: { top: 1, bottom: 1 }, borderStyle: 'round', borderColor: 'green' } ) ); } /** * Load a configuration file * @param {string} filePath - Path to the configuration file * @returns {Promise<Object>} - Parsed configuration */ async function loadConfig(filePath) { try { const content = await fs.readFile(filePath, 'utf8'); if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) { return yaml.load(content); } else if (filePath.endsWith('.json')) { return JSON.parse(content); } else { throw new Error('Unsupported file format. Use YAML or JSON.'); } } catch (error) { throw new Error(`Failed to load configuration: ${error.message}`); } } /** * Find configuration files in a directory * @param {string} directory - Directory to search * @returns {Promise<Array>} - List of configuration files */ async function findConfigFiles(directory) { try { const files = await fs.readdir(directory); return files.filter(file => file.endsWith('.yml') || file.endsWith('.yaml') || file.endsWith('.json') ); } catch (error) { console.error(chalk.red(`Error reading directory: ${error.message}`)); return []; } } /** * Validate a specific configuration file * @param {string} filePath - Path to the configuration file * @param {Object} options - Validation options */ async function validateConfigFile(filePath, options) { const spinner = ora(`Validating ${path.basename(filePath)}...`).start(); try { // Load configuration const config = await loadConfig(filePath); // Enhanced validation with additional checks const validationResult = validateConfig(config, { ...options, checkDependencies: true, checkConsistency: true }); spinner.succeed(`Validation complete for ${path.basename(filePath)}`); console.log('\n'); displayValidationResults(validationResult); return validationResult; } catch (error) { spinner.fail(`Validation failed for ${path.basename(filePath)}`); console.error(chalk.red(`Error: ${error.message}`)); return { valid: false, errors: [error.message], warnings: [] }; } } /** * Validate all configuration files in a directory * @param {string} directory - Directory containing configuration files * @param {Object} options - Validation options */ async function validateDirectory(directory, options) { console.log(chalk.blue(`\nScanning directory: ${directory}`)); const files = await findConfigFiles(directory); if (files.length === 0) { console.log(chalk.yellow('No configuration files found in the directory.')); return; } console.log(chalk.blue(`Found ${files.length} configuration files.\n`)); let validCount = 0; let issuesCount = 0; for (const file of files) { const filePath = path.join(directory, file); console.log(chalk.bold(`\nValidating: ${file}`)); const result = await validateConfigFile(filePath, options); if (result.valid) { validCount++; } else { issuesCount++; } } console.log(chalk.bold.blue('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); console.log(chalk.bold(`\nValidation Summary for ${files.length} files:`)); console.log(chalk.green(`✓ Valid: ${validCount}`)); console.log(chalk.red(`✗ With Issues: ${issuesCount}`)); } /** * Additional validation checks for database schema * @param {Object} config - The configuration object * @returns {Array} - Warnings about potential database issues */ function validateDatabaseSchema(config) { const warnings = []; if (!config.model || !config.model.fields) { return warnings; } // Check for missing primary key const hasPrimaryKey = config.model.fields.some(field => field.name === 'id' || field.primary === true ); if (!hasPrimaryKey) { warnings.push('No primary key (id) defined in the model fields'); } // Check for potential index fields const potentialIndexFields = config.model.fields.filter(field => field.name.endsWith('_id') || field.name === 'email' || field.name === 'username' ); potentialIndexFields.forEach(field => { if (!field.index) { warnings.push(`Field '${field.name}' might benefit from an index`); } }); // Check for missing timestamps const hasTimestamps = config.model.fields.some(field => field.name === 'created_at' || field.name === 'updated_at' ); if (!hasTimestamps && config.model.timestamps !== false) { warnings.push('No timestamp fields defined (created_at, updated_at)'); } return warnings; } /** * Validate relationships for potential issues * @param {Object} config - The configuration object * @returns {Array} - Warnings about potential relationship issues */ function validateRelationships(config) { const warnings = []; if (!config.model || !config.model.relationships) { return warnings; } const relationships = config.model.relationships; // Check for missing foreign keys relationships.forEach(rel => { if (rel.type === 'belongsTo') { const foreignKey = rel.foreignKey || `${rel.name}_id`; const hasForeignKey = config.model.fields.some(field => field.name === foreignKey); if (!hasForeignKey) { warnings.push(`Relationship '${rel.name}' (${rel.type}) is missing its foreign key field '${foreignKey}'`); } } }); // Check for polymorphic relationships without proper configuration relationships.forEach(rel => { if (rel.type === 'morphTo') { if (!rel.polymorphicTypes || rel.polymorphicTypes.length === 0) { warnings.push(`Polymorphic relationship '${rel.name}' has no defined types`); } // Check for missing morphable fields const morphIdField = `${rel.name}_id`; const morphTypeField = `${rel.name}_type`; const hasMorphFields = config.model.fields.some(field => field.name === morphIdField) && config.model.fields.some(field => field.name === morphTypeField); if (!hasMorphFields) { warnings.push(`Polymorphic relationship '${rel.name}' is missing morph fields ('${morphIdField}' and '${morphTypeField}')`); } } }); // Check for potential many-to-many without pivot information relationships.forEach(rel => { if (rel.type === 'belongsToMany' && !rel.pivotTable) { warnings.push(`Many-to-many relationship '${rel.name}' is missing pivot table information`); } }); return warnings; } /** * Interactive validation wizard */ async function interactiveValidation() { displayBanner(); // Ask for file or directory const { validationType } = await inquirer.prompt({ type: 'list', name: 'validationType', message: 'What would you like to validate?', choices: [ { name: 'Single configuration file', value: 'file' }, { name: 'All configurations in a directory', value: 'directory' } ] }); if (validationType === 'file') { // Find configuration files const directories = ['examples', '.']; let configFiles = []; for (const dir of directories) { try { const files = await findConfigFiles(dir); configFiles = [ ...configFiles, ...files.map(file => ({ name: `${file} (${dir})`, value: path.join(dir, file) })) ]; } catch (error) { // Directory might not exist, that's fine } } if (configFiles.length === 0) { console.log(chalk.yellow('No configuration files found.')); return; } const { selectedFile } = await inquirer.prompt({ type: 'list', name: 'selectedFile', message: 'Select a configuration file to validate:', choices: configFiles }); // Validation options const options = await inquirer.prompt([ { type: 'confirm', name: 'strict', message: 'Use strict validation mode?', default: false }, { type: 'confirm', name: 'validateDatabase', message: 'Validate database schema?', default: true }, { type: 'confirm', name: 'validateRelationships', message: 'Perform additional relationship validation?', default: true } ]); await validateConfigFile(selectedFile, options); } else { // Directory validation const { directory } = await inquirer.prompt({ type: 'list', name: 'directory', message: 'Select directory to validate:', choices: [ { name: 'examples/ directory', value: 'examples' }, { name: 'Current directory', value: '.' }, { name: 'Other directory (specify)', value: 'other' } ] }); let targetDir = directory; if (directory === 'other') { const { customDir } = await inquirer.prompt({ type: 'input', name: 'customDir', message: 'Enter directory path:', validate: input => input && input.length > 0 ? true : 'Directory path is required' }); targetDir = customDir; } // Validation options const options = await inquirer.prompt([ { type: 'confirm', name: 'strict', message: 'Use strict validation mode?', default: false }, { type: 'confirm', name: 'validateDatabase', message: 'Validate database schema?', default: true }, { type: 'confirm', name: 'validateRelationships', message: 'Perform additional relationship validation?', default: true } ]); await validateDirectory(targetDir, options); } console.log(chalk.blue('\nValidation complete!')); } /** * Command line interface setup */ program .version('1.0.0') .description('KIRA Configuration Validator') .option('-f, --file <path>', 'Validate a specific configuration file') .option('-d, --directory <path>', 'Validate all configuration files in a directory') .option('-s, --strict', 'Use strict validation mode') .option('--no-db-check', 'Skip database schema validation') .option('--no-rel-check', 'Skip relationship validation') .option('-i, --interactive', 'Start interactive validation wizard'); program.parse(process.argv); /** * Main application entry point */ async function main() { const options = program.opts(); // Default to interactive mode if no specific options provided if (!options.file && !options.directory && !options.interactive) { options.interactive = true; } try { if (options.interactive) { await interactiveValidation(); } else if (options.file) { displayBanner(); await validateConfigFile(options.file, { strict: options.strict, validateDatabase: options.dbCheck, validateRelationships: options.relCheck }); } else if (options.directory) { displayBanner(); await validateDirectory(options.directory, { strict: options.strict, validateDatabase: options.dbCheck, validateRelationships: options.relCheck }); } } catch (error) { console.error(chalk.red(`\nAn error occurred: ${error.message}\n`)); process.exit(1); } } // Run the application if (require.main === module) { main(); } module.exports = { validateConfig, validateConfigFile, validateDirectory, validateDatabaseSchema, validateRelationships };