kira-crud
Version:
Intelligent CRUD Generator for Laravel and Angular
451 lines (384 loc) • 12.9 kB
JavaScript
#!/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
};