UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

611 lines (515 loc) 20.7 kB
/** * Enhanced Configuration Validator * Provides deep validation for CRUD configuration files */ const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); const yaml = require('js-yaml'); const { validateConfig } = require('./config-validator'); /** * Supported data types for model fields */ const SUPPORTED_DATA_TYPES = [ 'string', 'integer', 'decimal', 'boolean', 'date', 'datetime', 'text', 'json', 'enum', 'array', 'object', 'file', 'image', 'email', 'url', 'password', 'uuid' ]; /** * Supported relationship types */ const SUPPORTED_RELATIONSHIP_TYPES = [ 'belongsTo', 'hasMany', 'belongsToMany', 'hasOne', 'morphTo', 'morphMany', 'morphToMany', 'morphedByMany', 'hasManyThrough', 'hasOneThrough' ]; /** * Enhanced validation of configuration with detailed checks * @param {Object} config - The configuration object * @param {Object} options - Validation options * @returns {Object} Validation result with errors and warnings */ function enhancedValidation(config, options = {}) { // First run basic validation const basicResult = validateConfig(config); // Add enhanced validation checks const enhancedResult = { valid: basicResult.valid, errors: [...basicResult.errors], warnings: [...basicResult.warnings], suggestions: [] }; // Run enhanced validation only if basic validation passed if (basicResult.valid) { // Validate data types validateDataTypes(config, enhancedResult); // Validate relationships validateRelationships(config, enhancedResult); // Validate UI configuration validateUIConfiguration(config, enhancedResult); // Validate validation rules validateValidationRules(config, enhancedResult); // Check for potential naming issues checkNamingIssues(config, enhancedResult); // Check for database schema issues if enabled if (options.validateDatabase) { validateDatabaseSchema(config, enhancedResult); } // Advanced relationship validation if enabled if (options.validateRelationships) { validateAdvancedRelationships(config, enhancedResult); } // Check for dependency issues if enabled if (options.checkDependencies) { checkDependencies(config, enhancedResult); } // Check for consistency issues if enabled if (options.checkConsistency) { checkConsistency(config, enhancedResult); } // Strict mode adds warnings as errors if (options.strict) { enhancedResult.errors = [...enhancedResult.errors, ...enhancedResult.warnings]; enhancedResult.warnings = []; } // Update valid status based on errors enhancedResult.valid = enhancedResult.errors.length === 0; } return enhancedResult; } /** * Validate field data types * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function validateDataTypes(config, result) { if (!config.model || !config.model.fields) { return; } config.model.fields.forEach(field => { // Check if data type is supported if (!SUPPORTED_DATA_TYPES.includes(field.type)) { result.warnings.push(`Field '${field.name}' has unsupported data type: ${field.type}`); } // Check if enum type has values if (field.type === 'enum' && (!field.enumValues || field.enumValues.length === 0)) { result.errors.push(`Enum field '${field.name}' must have enumValues defined`); } // Check for potential data type mismatches if (field.name.includes('_id') && field.type !== 'integer' && field.type !== 'uuid') { result.warnings.push(`Field '${field.name}' appears to be an ID but has type '${field.type}' instead of 'integer' or 'uuid'`); } if (field.name.includes('email') && field.type !== 'email' && field.type !== 'string') { result.warnings.push(`Field '${field.name}' appears to be an email but has type '${field.type}' instead of 'email' or 'string'`); } if (field.name.includes('date') && !['date', 'datetime'].includes(field.type)) { result.warnings.push(`Field '${field.name}' appears to be a date but has type '${field.type}' instead of 'date' or 'datetime'`); } if (field.name.includes('password') && field.type !== 'password' && field.type !== 'string') { result.warnings.push(`Field '${field.name}' appears to be a password but has type '${field.type}' instead of 'password' or 'string'`); } }); } /** * Validate relationship configuration * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function validateRelationships(config, result) { if (!config.model || !config.model.relationships) { return; } config.model.relationships.forEach(rel => { // Check if relationship type is supported if (!SUPPORTED_RELATIONSHIP_TYPES.includes(rel.type)) { result.errors.push(`Relationship '${rel.name}' has unsupported type: ${rel.type}`); } // Check for required relationship properties if (!rel.name) { result.errors.push(`Relationship without a name defined`); } if (rel.type !== 'morphTo' && !rel.model) { result.errors.push(`Relationship '${rel.name}' is missing the related model`); } // Check for naming conventions if (rel.type === 'hasMany' && !rel.name.endsWith('s')) { result.suggestions.push(`'hasMany' relationship '${rel.name}' should typically use a plural name`); } if (rel.type === 'hasOne' && rel.name.endsWith('s')) { result.suggestions.push(`'hasOne' relationship '${rel.name}' should typically use a singular name`); } // Check for polymorphic relationship configuration if (rel.type === 'morphTo') { if (!rel.polymorphicTypes || rel.polymorphicTypes.length === 0) { result.warnings.push(`Polymorphic relationship '${rel.name}' has no defined types`); } } // Check for belongsToMany pivot table if (rel.type === 'belongsToMany' && !rel.pivotTable) { result.warnings.push(`Many-to-many relationship '${rel.name}' should define a pivotTable`); } // Check for UI configuration in relationships if (!rel.ui) { result.suggestions.push(`Relationship '${rel.name}' does not have UI configuration`); } else { if (rel.ui.showInForm && !rel.ui.type) { result.warnings.push(`Relationship '${rel.name}' is shown in form but has no UI type defined`); } } }); } /** * Validate UI configuration * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function validateUIConfiguration(config, result) { if (!config.ui) { result.suggestions.push('No UI configuration defined'); return; } // Check for tableFields configuration if (!config.ui.tableFields || config.ui.tableFields.length === 0) { result.warnings.push('No tableFields defined in UI configuration'); } else { // Check if tableFields reference valid fields const fieldNames = config.model.fields.map(f => f.name); const relationshipNames = (config.model.relationships || []).map(r => r.name); config.ui.tableFields.forEach(field => { if (!fieldNames.includes(field) && !relationshipNames.includes(field)) { result.warnings.push(`Table field '${field}' is not defined in model fields or relationships`); } }); } // Check for itemsPerPage being reasonable if (config.ui.itemsPerPage) { if (config.ui.itemsPerPage < 5 || config.ui.itemsPerPage > 100) { result.warnings.push(`itemsPerPage value (${config.ui.itemsPerPage}) seems unusual. Typical values are between 10-50.`); } } } /** * Validate validation rules * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function validateValidationRules(config, result) { if (!config.model || !config.model.fields) { return; } config.model.fields.forEach(field => { if (!field.validations || field.validations.length === 0) { return; } // Check for common validation issues if (field.validations.includes('required') && field.nullable) { result.warnings.push(`Field '${field.name}' is marked as both required and nullable`); } // Check for type-specific validations if (field.type === 'string' || field.type === 'text') { const hasMaxLength = field.validations.some(v => typeof v === 'string' ? v === 'max' : v.max !== undefined ); if (!hasMaxLength) { result.suggestions.push(`String field '${field.name}' should have a maximum length validation`); } } if (field.type === 'email' && !field.validations.includes('email')) { result.suggestions.push(`Email field '${field.name}' should have email validation`); } if ((field.type === 'integer' || field.type === 'decimal') && !field.validations.includes('numeric')) { result.suggestions.push(`Numeric field '${field.name}' should have numeric validation`); } // Check for uniqueness validation on identifying fields if (['email', 'username', 'slug'].some(term => field.name.includes(term)) && !field.validations.includes('unique')) { result.suggestions.push(`Field '${field.name}' might need a uniqueness constraint`); } }); } /** * Check for naming issues * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function checkNamingIssues(config, result) { if (!config.model) { return; } // Check model name (should be PascalCase singular) if (config.model.name) { if (!/^[A-Z][a-zA-Z0-9]*$/.test(config.model.name)) { result.warnings.push(`Model name '${config.model.name}' should be in PascalCase`); } if (config.model.name.endsWith('s')) { result.warnings.push(`Model name '${config.model.name}' should be singular, not plural`); } } // Check table name (should be snake_case plural) if (config.model.tableName) { if (!/^[a-z][a-z0-9_]*$/.test(config.model.tableName)) { result.warnings.push(`Table name '${config.model.tableName}' should be in snake_case`); } if (!config.model.tableName.endsWith('s')) { result.warnings.push(`Table name '${config.model.tableName}' should be plural`); } } // Check field names (should be camelCase or snake_case) if (config.model.fields) { config.model.fields.forEach(field => { if (!/^[a-z][a-zA-Z0-9_]*$/.test(field.name)) { result.warnings.push(`Field name '${field.name}' should be in camelCase or snake_case`); } // Check for reserved words const reservedWords = ['class', 'function', 'return', 'var', 'let', 'const', 'if', 'else', 'for', 'while', 'true', 'false', 'null', 'undefined']; if (reservedWords.includes(field.name)) { result.errors.push(`Field name '${field.name}' is a reserved word and should not be used`); } }); } } /** * Validate database schema issues * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function validateDatabaseSchema(config, result) { if (!config.model || !config.model.fields) { return; } // Check for primary key const hasPrimaryKey = config.model.fields.some(field => field.name === 'id' || field.primary === true ); if (!hasPrimaryKey) { result.warnings.push('No primary key (id) defined in the model fields'); } // Check for timestamps const hasTimestamps = config.model.fields.some(field => field.name === 'created_at' || field.name === 'updated_at' ); if (!hasTimestamps && config.model.timestamps !== false) { result.suggestions.push('Consider adding timestamp fields (created_at, updated_at)'); } // Check for indexes on searchable fields const searchableFields = config.ui?.searchableFields || []; searchableFields.forEach(fieldName => { const field = config.model.fields.find(f => f.name === fieldName); if (field && !field.index) { result.suggestions.push(`Searchable field '${fieldName}' should have an index for better performance`); } }); // Check for missing foreign keys if (config.model.relationships) { config.model.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) { result.warnings.push(`Relationship '${rel.name}' needs a foreign key field '${foreignKey}'`); } } }); } } /** * Validate advanced relationship configurations * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function validateAdvancedRelationships(config, result) { if (!config.model || !config.model.relationships) { return; } // Check for polymorphic relationships const polymorphicRelationships = config.model.relationships.filter(rel => rel.type === 'morphTo' || rel.type === 'morphMany' || rel.type === 'morphToMany' || rel.type === 'morphedByMany' ); polymorphicRelationships.forEach(rel => { if (rel.type === 'morphTo') { // Check for missing polymorphic fields const morphIdField = `${rel.name}_id`; const morphTypeField = `${rel.name}_type`; const hasMorphIdField = config.model.fields.some(field => field.name === morphIdField); const hasMorphTypeField = config.model.fields.some(field => field.name === morphTypeField); if (!hasMorphIdField) { result.errors.push(`Polymorphic relationship '${rel.name}' is missing the '${morphIdField}' field`); } if (!hasMorphTypeField) { result.errors.push(`Polymorphic relationship '${rel.name}' is missing the '${morphTypeField}' field`); } // Check for polymorphic types if (!rel.polymorphicTypes || rel.polymorphicTypes.length === 0) { result.errors.push(`Polymorphic relationship '${rel.name}' must define polymorphicTypes`); } else { // Check each polymorphic type rel.polymorphicTypes.forEach(type => { if (!type.model) { result.errors.push(`Polymorphic type in relationship '${rel.name}' is missing the model name`); } if (!type.displayField) { result.warnings.push(`Polymorphic type '${type.model}' in relationship '${rel.name}' should define a displayField`); } }); } } if (rel.type === 'morphMany' || rel.type === 'morphToMany' || rel.type === 'morphedByMany') { if (!rel.morphName) { result.errors.push(`Morphable relationship '${rel.name}' must define a morphName`); } } }); // Check for many-to-many relationships const manyToManyRelationships = config.model.relationships.filter(rel => rel.type === 'belongsToMany' ); manyToManyRelationships.forEach(rel => { if (!rel.pivotTable) { result.warnings.push(`Many-to-many relationship '${rel.name}' should define a pivotTable`); } if (rel.pivotFields && rel.pivotFields.length > 0) { // Check if there's UI for editing pivot fields if (rel.ui && rel.ui.showInForm && (!rel.ui.pivotFieldsInForm)) { result.suggestions.push(`Relationship '${rel.name}' has pivot fields but no UI configuration for editing them`); } } }); // Check for has-through relationships const hasThroughRelationships = config.model.relationships.filter(rel => rel.type === 'hasManyThrough' || rel.type === 'hasOneThrough' ); hasThroughRelationships.forEach(rel => { if (!rel.through) { result.errors.push(`Through relationship '${rel.name}' must define a 'through' model`); } if (!rel.foreignKey) { result.warnings.push(`Through relationship '${rel.name}' should define foreignKey`); } if (!rel.throughForeignKey) { result.warnings.push(`Through relationship '${rel.name}' should define throughForeignKey`); } }); } /** * Check for dependency issues between related models * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function checkDependencies(config, result) { if (!config.model || !config.model.relationships) { return; } // This would typically involve checking if related models exist // For this implementation, we'll just add a suggestion result.suggestions.push('Consider validating that all related models exist in your database schema'); } /** * Check for consistency issues in the configuration * @param {Object} config - The configuration object * @param {Object} result - The validation result to update */ function checkConsistency(config, result) { if (!config.model) { return; } // Check for consistent naming between model and table if (config.model.name && config.model.tableName) { const modelNameSnake = config.model.name .replace(/([a-z])([A-Z])/g, '$1_$2') .toLowerCase(); const expectedTableName = modelNameSnake + 's'; if (config.model.tableName !== expectedTableName) { result.suggestions.push(`Table name '${config.model.tableName}' doesn't match the expected name '${expectedTableName}' based on model name`); } } // Check for consistent field names in UI configuration if (config.ui && config.ui.tableFields) { const allFieldNames = [ ...(config.model.fields || []).map(f => f.name), ...(config.model.relationships || []).map(r => r.name) ]; config.ui.tableFields.forEach(field => { if (!allFieldNames.includes(field)) { result.warnings.push(`UI tableField '${field}' doesn't match any model field or relationship`); } }); } // Check for consistent route naming if (config.routes) { if (config.routes.apiPrefix && config.model.tableName) { if (!config.routes.apiPrefix.includes(config.model.tableName)) { result.suggestions.push(`API route prefix '${config.routes.apiPrefix}' should typically include the table name`); } } if (config.routes.frontendPath && config.model.name) { const modelNameKebab = config.model.name .replace(/([a-z])([A-Z])/g, '$1-$2') .toLowerCase(); if (!config.routes.frontendPath.includes(modelNameKebab)) { result.suggestions.push(`Frontend path '${config.routes.frontendPath}' should typically match the kebab-case model name`); } } } } /** * Load a configuration file and validate it * @param {string} filePath - Path to the configuration file * @param {Object} options - Validation options * @returns {Promise<Object>} Validation result */ async function validateConfigFile(filePath, options = {}) { try { const content = await fs.readFile(filePath, 'utf8'); let config; if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) { config = yaml.load(content); } else if (filePath.endsWith('.json')) { config = JSON.parse(content); } else { throw new Error('Unsupported file format. Use YAML or JSON.'); } return enhancedValidation(config, options); } catch (error) { return { valid: false, errors: [`Failed to load or parse configuration: ${error.message}`], warnings: [], suggestions: [] }; } } /** * Display enhanced validation results * @param {Object} result - The validation result */ function displayEnhancedResults(result) { if (result.valid) { console.log(chalk.green.bold('✓ Configuration is valid')); } else { console.log(chalk.red.bold('✗ Configuration has errors:')); result.errors.forEach(error => { console.log(chalk.red(` • ${error}`)); }); } if (result.warnings.length > 0) { console.log(chalk.yellow.bold('\n⚠️ Warnings:')); result.warnings.forEach(warning => { console.log(chalk.yellow(` • ${warning}`)); }); } if (result.suggestions.length > 0) { console.log(chalk.blue.bold('\n💡 Suggestions:')); result.suggestions.forEach(suggestion => { console.log(chalk.blue(` • ${suggestion}`)); }); } } module.exports = { enhancedValidation, validateConfigFile, displayEnhancedResults, SUPPORTED_DATA_TYPES, SUPPORTED_RELATIONSHIP_TYPES };