UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

318 lines (282 loc) 9.4 kB
/** * Configuration Validator * Enhanced validation for CRUD configuration files */ const chalk = require('chalk'); const yaml = require('js-yaml'); const fs = require('fs').promises; const path = require('path'); /** * Validation schema for model configurations */ const validationSchema = { backend: { properties: { architecture: { type: 'string', enum: ['advanced', 'classic'] } } }, model: { required: true, properties: { name: { required: true, type: 'string', pattern: /^[A-Z][a-zA-Z0-9]*$/ }, tableName: { required: true, type: 'string' }, fields: { required: true, type: 'array', minItems: 1, itemSchema: { properties: { name: { required: true, type: 'string', pattern: /^[a-z][a-zA-Z0-9]*$/ }, type: { required: true, type: 'string', enum: ['string', 'integer', 'decimal', 'boolean', 'date', 'datetime', 'text', 'json', 'enum'] } } } }, relationships: { type: 'array', itemSchema: { properties: { type: { required: true, type: 'string', enum: ['belongsTo', 'hasMany', 'belongsToMany', 'hasOne', 'morphTo', 'morphMany'] }, name: { required: true, type: 'string' }, relatedModel: { required: true, type: 'string' } } } } } }, ui: { properties: { tableFields: { type: 'array' }, itemsPerPage: { type: 'number' } } }, routes: { properties: { apiPrefix: { type: 'string' }, frontendPath: { type: 'string' } } } }; /** * Load and parse a configuration file * @param {string} filePath - Path to the configuration file * @returns {Object} Parsed configuration object */ async function loadConfig(filePath) { try { const fullPath = path.resolve(filePath); const content = await fs.readFile(fullPath, 'utf8'); // Parse based on file extension 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}`); } } /** * Validate a value against a schema property * @param {any} value - The value to validate * @param {Object} schema - The schema definition * @param {string} path - Current property path for error messages * @returns {Array} Array of validation errors */ function validateProperty(value, schema, path) { const errors = []; // Check required if (schema.required && (value === undefined || value === null)) { errors.push(`${path} is required`); return errors; } // Skip further validation if value is not provided and not required if (value === undefined || value === null) { return errors; } // Check type if (schema.type) { let typeValid = false; switch (schema.type) { case 'string': typeValid = typeof value === 'string'; break; case 'number': typeValid = typeof value === 'number'; break; case 'boolean': typeValid = typeof value === 'boolean'; break; case 'array': typeValid = Array.isArray(value); break; case 'object': typeValid = typeof value === 'object' && !Array.isArray(value); break; } if (!typeValid) { errors.push(`${path} should be of type ${schema.type}`); return errors; } } // Check pattern if (schema.pattern && typeof value === 'string') { if (!schema.pattern.test(value)) { errors.push(`${path} does not match the required pattern ${schema.pattern}`); } } // Check enum if (schema.enum && Array.isArray(schema.enum)) { if (!schema.enum.includes(value)) { errors.push(`${path} must be one of: ${schema.enum.join(', ')}`); } } // Check min/max for arrays if (schema.type === 'array') { if (schema.minItems !== undefined && value.length < schema.minItems) { errors.push(`${path} should have at least ${schema.minItems} items`); } if (schema.maxItems !== undefined && value.length > schema.maxItems) { errors.push(`${path} should have at most ${schema.maxItems} items`); } // Validate array items if (schema.itemSchema && value.length > 0) { value.forEach((item, index) => { const itemPath = `${path}[${index}]`; // Check if item is an object when itemSchema is provided if (schema.itemSchema.properties && (typeof item !== 'object' || Array.isArray(item))) { errors.push(`${itemPath} should be an object`); } else if (schema.itemSchema.properties) { // Validate each property in the item for (const propName in schema.itemSchema.properties) { const propSchema = schema.itemSchema.properties[propName]; const propPath = `${itemPath}.${propName}`; const propValue = item[propName]; errors.push(...validateProperty(propValue, propSchema, propPath)); } } }); } } // Validate object properties if (schema.properties && typeof value === 'object' && !Array.isArray(value)) { for (const propName in schema.properties) { const propSchema = schema.properties[propName]; const propPath = path ? `${path}.${propName}` : propName; const propValue = value[propName]; errors.push(...validateProperty(propValue, propSchema, propPath)); } } return errors; } /** * Validate a configuration object against the schema * @param {Object} config - The configuration object to validate * @returns {Object} Validation result with errors and warnings */ function validateConfig(config) { const result = { valid: true, errors: [], warnings: [] }; // Validate against schema for (const sectionName in validationSchema) { const sectionSchema = validationSchema[sectionName]; const sectionValue = config[sectionName]; const errors = validateProperty(sectionValue, sectionSchema, sectionName); result.errors.push(...errors); } // Add warnings for potential issues // Check for potential relationship issues if (config.model && config.model.relationships) { const relationshipNames = new Set(); config.model.relationships.forEach((relationship, index) => { // Check for duplicate relationship names if (relationshipNames.has(relationship.name)) { result.warnings.push(`Duplicate relationship name: ${relationship.name}`); } relationshipNames.add(relationship.name); // Check for missing pivot table in belongsToMany if (relationship.type === 'belongsToMany' && !relationship.pivotTable) { result.warnings.push(`Relationship ${relationship.name} (belongsToMany) should have a pivotTable defined`); } // Check for missing foreign key in belongsTo if (relationship.type === 'belongsTo' && !relationship.foreignKey) { // Not a critical issue, but worth mentioning result.warnings.push(`Relationship ${relationship.name} (belongsTo) has no explicit foreignKey, will use default naming`); } }); } // Check UI configuration if (config.model && config.model.fields && config.ui && config.ui.tableFields) { const fieldNames = new Set(config.model.fields.map(f => f.name)); config.ui.tableFields.forEach(field => { if (!fieldNames.has(field)) { result.warnings.push(`Table field "${field}" is not defined in model fields`); } }); } result.valid = result.errors.length === 0; return result; } /** * Display validation results in a user-friendly format * @param {Object} validationResult - The validation result object */ function displayValidationResults(validationResult) { if (validationResult.valid) { console.log(chalk.green('✓ Configuration is valid')); if (validationResult.warnings.length > 0) { console.log(chalk.yellow('\n⚠️ Warnings:')); validationResult.warnings.forEach(warning => { console.log(chalk.yellow(` - ${warning}`)); }); } } else { console.log(chalk.red('❌ Configuration has errors:')); validationResult.errors.forEach(error => { console.log(chalk.red(` - ${error}`)); }); if (validationResult.warnings.length > 0) { console.log(chalk.yellow('\n⚠️ Warnings:')); validationResult.warnings.forEach(warning => { console.log(chalk.yellow(` - ${warning}`)); }); } } } /** * Validate a configuration file * @param {string} filePath - Path to the configuration file * @returns {Promise<Object>} Validation result */ async function validateConfigFile(filePath) { try { const config = await loadConfig(filePath); return validateConfig(config); } catch (error) { return { valid: false, errors: [error.message], warnings: [] }; } } module.exports = { validateConfig, validateConfigFile, displayValidationResults };