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