kira-crud
Version:
Intelligent CRUD Generator for Laravel and Angular
611 lines (515 loc) • 20.7 kB
JavaScript
/**
* 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
};