326 lines
8.7 KiB
TypeScript
326 lines
8.7 KiB
TypeScript
|
import * as plugins from '../plugins.js';
|
||
|
import { ValidationError } from '../errors/base.errors.js';
|
||
|
import type { IBaseConfig } from './base.config.js';
|
||
|
|
||
|
/**
|
||
|
* Validation result
|
||
|
*/
|
||
|
export interface IValidationResult {
|
||
|
/**
|
||
|
* Whether the validation passed
|
||
|
*/
|
||
|
valid: boolean;
|
||
|
|
||
|
/**
|
||
|
* Validation errors if any
|
||
|
*/
|
||
|
errors?: string[];
|
||
|
|
||
|
/**
|
||
|
* Validated configuration (may include defaults)
|
||
|
*/
|
||
|
config?: any;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validation schema types
|
||
|
*/
|
||
|
export type ValidationSchema = Record<string, {
|
||
|
/**
|
||
|
* Type of the value
|
||
|
*/
|
||
|
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||
|
|
||
|
/**
|
||
|
* Whether the field is required
|
||
|
*/
|
||
|
required?: boolean;
|
||
|
|
||
|
/**
|
||
|
* Default value if not specified
|
||
|
*/
|
||
|
default?: any;
|
||
|
|
||
|
/**
|
||
|
* Minimum value (for numbers)
|
||
|
*/
|
||
|
min?: number;
|
||
|
|
||
|
/**
|
||
|
* Maximum value (for numbers)
|
||
|
*/
|
||
|
max?: number;
|
||
|
|
||
|
/**
|
||
|
* Minimum length (for strings or arrays)
|
||
|
*/
|
||
|
minLength?: number;
|
||
|
|
||
|
/**
|
||
|
* Maximum length (for strings or arrays)
|
||
|
*/
|
||
|
maxLength?: number;
|
||
|
|
||
|
/**
|
||
|
* Pattern to match (for strings)
|
||
|
*/
|
||
|
pattern?: RegExp;
|
||
|
|
||
|
/**
|
||
|
* Allowed values (for strings, numbers)
|
||
|
*/
|
||
|
enum?: any[];
|
||
|
|
||
|
/**
|
||
|
* Nested schema (for objects)
|
||
|
*/
|
||
|
schema?: ValidationSchema;
|
||
|
|
||
|
/**
|
||
|
* Item schema (for arrays)
|
||
|
*/
|
||
|
items?: {
|
||
|
type: 'string' | 'number' | 'boolean' | 'object';
|
||
|
schema?: ValidationSchema;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Custom validation function
|
||
|
*/
|
||
|
validate?: (value: any) => boolean | string;
|
||
|
}>;
|
||
|
|
||
|
/**
|
||
|
* Configuration validator
|
||
|
* Validates configuration objects against schemas and provides default values
|
||
|
*/
|
||
|
export class ConfigValidator {
|
||
|
/**
|
||
|
* Basic schema for IBaseConfig
|
||
|
*/
|
||
|
private static baseConfigSchema: ValidationSchema = {
|
||
|
id: {
|
||
|
type: 'string',
|
||
|
required: false
|
||
|
},
|
||
|
version: {
|
||
|
type: 'string',
|
||
|
required: false
|
||
|
},
|
||
|
environment: {
|
||
|
type: 'string',
|
||
|
required: false,
|
||
|
enum: ['development', 'test', 'staging', 'production'],
|
||
|
default: 'production'
|
||
|
},
|
||
|
name: {
|
||
|
type: 'string',
|
||
|
required: false
|
||
|
},
|
||
|
enabled: {
|
||
|
type: 'boolean',
|
||
|
required: false,
|
||
|
default: true
|
||
|
},
|
||
|
logging: {
|
||
|
type: 'object',
|
||
|
required: false,
|
||
|
schema: {
|
||
|
level: {
|
||
|
type: 'string',
|
||
|
required: false,
|
||
|
enum: ['error', 'warn', 'info', 'debug'],
|
||
|
default: 'info'
|
||
|
},
|
||
|
structured: {
|
||
|
type: 'boolean',
|
||
|
required: false,
|
||
|
default: true
|
||
|
},
|
||
|
correlationTracking: {
|
||
|
type: 'boolean',
|
||
|
required: false,
|
||
|
default: true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Validate a configuration object against a schema
|
||
|
*
|
||
|
* @param config Configuration object to validate
|
||
|
* @param schema Validation schema
|
||
|
* @returns Validation result
|
||
|
*/
|
||
|
public static validate<T>(config: T, schema: ValidationSchema): IValidationResult {
|
||
|
const errors: string[] = [];
|
||
|
const validatedConfig = { ...config };
|
||
|
|
||
|
// Validate each field against the schema
|
||
|
for (const [key, rules] of Object.entries(schema)) {
|
||
|
const value = config[key];
|
||
|
|
||
|
// Check if required
|
||
|
if (rules.required && (value === undefined || value === null)) {
|
||
|
errors.push(`${key} is required`);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// If not present and not required, apply default if available
|
||
|
if ((value === undefined || value === null)) {
|
||
|
if (rules.default !== undefined) {
|
||
|
validatedConfig[key] = rules.default;
|
||
|
}
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Type validation
|
||
|
if (value !== undefined && value !== null) {
|
||
|
const valueType = Array.isArray(value) ? 'array' : typeof value;
|
||
|
if (valueType !== rules.type) {
|
||
|
errors.push(`${key} must be of type ${rules.type}, got ${valueType}`);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Type-specific validations
|
||
|
switch (rules.type) {
|
||
|
case 'number':
|
||
|
if (rules.min !== undefined && value < rules.min) {
|
||
|
errors.push(`${key} must be at least ${rules.min}`);
|
||
|
}
|
||
|
if (rules.max !== undefined && value > rules.max) {
|
||
|
errors.push(`${key} must be at most ${rules.max}`);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'string':
|
||
|
if (rules.minLength !== undefined && value.length < rules.minLength) {
|
||
|
errors.push(`${key} must be at least ${rules.minLength} characters`);
|
||
|
}
|
||
|
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
|
||
|
errors.push(`${key} must be at most ${rules.maxLength} characters`);
|
||
|
}
|
||
|
if (rules.pattern && !rules.pattern.test(value)) {
|
||
|
errors.push(`${key} must match pattern ${rules.pattern}`);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'array':
|
||
|
if (rules.minLength !== undefined && value.length < rules.minLength) {
|
||
|
errors.push(`${key} must have at least ${rules.minLength} items`);
|
||
|
}
|
||
|
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
|
||
|
errors.push(`${key} must have at most ${rules.maxLength} items`);
|
||
|
}
|
||
|
if (rules.items && value.length > 0) {
|
||
|
for (let i = 0; i < value.length; i++) {
|
||
|
const itemType = Array.isArray(value[i]) ? 'array' : typeof value[i];
|
||
|
if (itemType !== rules.items.type) {
|
||
|
errors.push(`${key}[${i}] must be of type ${rules.items.type}, got ${itemType}`);
|
||
|
} else if (rules.items.schema && itemType === 'object') {
|
||
|
const itemResult = this.validate(value[i], rules.items.schema);
|
||
|
if (!itemResult.valid) {
|
||
|
errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'object':
|
||
|
if (rules.schema) {
|
||
|
const nestedResult = this.validate(value, rules.schema);
|
||
|
if (!nestedResult.valid) {
|
||
|
errors.push(...nestedResult.errors.map(err => `${key}.${err}`));
|
||
|
}
|
||
|
validatedConfig[key] = nestedResult.config;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Enum validation
|
||
|
if (rules.enum && !rules.enum.includes(value)) {
|
||
|
errors.push(`${key} must be one of [${rules.enum.join(', ')}]`);
|
||
|
}
|
||
|
|
||
|
// Custom validation
|
||
|
if (rules.validate) {
|
||
|
const result = rules.validate(value);
|
||
|
if (result !== true) {
|
||
|
errors.push(typeof result === 'string' ? result : `${key} failed custom validation`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
valid: errors.length === 0,
|
||
|
errors: errors.length > 0 ? errors : undefined,
|
||
|
config: validatedConfig
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validate base configuration
|
||
|
*
|
||
|
* @param config Base configuration
|
||
|
* @returns Validation result for base configuration
|
||
|
*/
|
||
|
public static validateBaseConfig(config: IBaseConfig): IValidationResult {
|
||
|
return this.validate(config, this.baseConfigSchema);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply defaults to a configuration object based on a schema
|
||
|
*
|
||
|
* @param config Configuration object to apply defaults to
|
||
|
* @param schema Validation schema with defaults
|
||
|
* @returns Configuration with defaults applied
|
||
|
*/
|
||
|
public static applyDefaults<T>(config: T, schema: ValidationSchema): T {
|
||
|
const result = { ...config };
|
||
|
|
||
|
for (const [key, rules] of Object.entries(schema)) {
|
||
|
if (result[key] === undefined && rules.default !== undefined) {
|
||
|
result[key] = rules.default;
|
||
|
}
|
||
|
|
||
|
// Apply defaults to nested objects
|
||
|
if (result[key] && rules.type === 'object' && rules.schema) {
|
||
|
result[key] = this.applyDefaults(result[key], rules.schema);
|
||
|
}
|
||
|
|
||
|
// Apply defaults to array items
|
||
|
if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) {
|
||
|
result[key] = result[key].map(item =>
|
||
|
typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Throw a validation error if the configuration is invalid
|
||
|
*
|
||
|
* @param config Configuration to validate
|
||
|
* @param schema Validation schema
|
||
|
* @returns Validated configuration with defaults
|
||
|
* @throws ValidationError if validation fails
|
||
|
*/
|
||
|
public static validateOrThrow<T>(config: T, schema: ValidationSchema): T {
|
||
|
const result = this.validate(config, schema);
|
||
|
|
||
|
if (!result.valid) {
|
||
|
throw new ValidationError(
|
||
|
`Configuration validation failed: ${result.errors.join(', ')}`,
|
||
|
'CONFIG_VALIDATION_ERROR',
|
||
|
{ data: { errors: result.errors } }
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return result.config;
|
||
|
}
|
||
|
}
|