@ordojs/forms
Version:
Comprehensive form handling system for OrdoJS
457 lines • 14.7 kB
JavaScript
/**
* Comprehensive validation system with schema integration
*/
/**
* Built-in validation rules
*/
export class ValidationRules {
static required(message = 'This field is required') {
return {
type: 'required',
message,
validate: (value) => {
if (value === null || value === undefined)
return false;
if (typeof value === 'string')
return value.trim().length > 0;
if (typeof value === 'number')
return !isNaN(value);
if (typeof value === 'boolean')
return true;
if (value instanceof Date)
return !isNaN(value.getTime());
if (value instanceof File)
return value.size > 0;
return Boolean(value);
}
};
}
static minLength(min, message) {
return {
type: 'minLength',
message: message || `Must be at least ${min} characters`,
validate: (value) => {
if (value === null || value === undefined)
return true;
const str = String(value);
return str.length >= min;
}
};
}
static maxLength(max, message) {
return {
type: 'maxLength',
message: message || `Must be no more than ${max} characters`,
validate: (value) => {
if (value === null || value === undefined)
return true;
const str = String(value);
return str.length <= max;
}
};
}
static min(min, message) {
return {
type: 'min',
message: message || `Must be at least ${min}`,
validate: (value) => {
if (value === null || value === undefined)
return true;
const num = Number(value);
return !isNaN(num) && num >= min;
}
};
}
static max(max, message) {
return {
type: 'max',
message: message || `Must be no more than ${max}`,
validate: (value) => {
if (value === null || value === undefined)
return true;
const num = Number(value);
return !isNaN(num) && num <= max;
}
};
}
static email(message = 'Must be a valid email address') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return {
type: 'email',
message,
validate: (value) => {
if (value === null || value === undefined || value === '')
return true;
return emailRegex.test(String(value));
}
};
}
static url(message = 'Must be a valid URL') {
return {
type: 'url',
message,
validate: (value) => {
if (value === null || value === undefined || value === '')
return true;
try {
new URL(String(value));
return true;
}
catch {
return false;
}
}
};
}
static pattern(regex, message = 'Invalid format') {
return {
type: 'pattern',
message,
validate: (value) => {
if (value === null || value === undefined || value === '')
return true;
return regex.test(String(value));
}
};
}
static oneOf(options, message) {
return {
type: 'oneOf',
message: message || `Must be one of: ${options.join(', ')}`,
validate: (value) => {
if (value === null || value === undefined)
return true;
return options.includes(value);
}
};
}
static custom(validator, message = 'Invalid value') {
return {
type: 'custom',
message,
validate: validator
};
}
static async asyncCustom(validator, message = 'Invalid value') {
return {
type: 'asyncCustom',
message,
validate: validator
};
}
static equalTo(fieldName, message) {
return {
type: 'equalTo',
message: message || `Must match ${fieldName}`,
validate: (value, formValues) => {
if (!formValues || value === null || value === undefined)
return true;
return value === formValues[fieldName];
}
};
}
static date(message = 'Must be a valid date') {
return {
type: 'date',
message,
validate: (value) => {
if (value === null || value === undefined || value === '')
return true;
if (value instanceof Date)
return !isNaN(value.getTime());
const date = new Date(String(value));
return !isNaN(date.getTime());
}
};
}
static file(options = {}) {
return {
type: 'file',
message: options.message || 'Invalid file',
validate: (value) => {
if (value === null || value === undefined)
return true;
if (!(value instanceof File))
return false;
// Check file size
if (options.maxSize && value.size > options.maxSize) {
return false;
}
// Check file type
if (options.allowedTypes && !options.allowedTypes.includes(value.type)) {
return false;
}
return true;
}
};
}
}
/**
* Schema adapter for Zod integration
*/
export class ZodSchemaAdapter {
constructor(schema) {
this.schema = schema;
}
async validate(values) {
try {
this.schema.parse(values);
return {};
}
catch (error) {
const errors = {};
if (error.issues) {
for (const issue of error.issues) {
const fieldName = issue.path.join('.');
errors[fieldName] = {
type: issue.code || 'validation_error',
message: issue.message,
field: fieldName
};
}
}
return errors;
}
}
async validateField(name, value, values) {
try {
// Create a partial object with just this field
const testValues = { ...values, [name]: value };
const result = this.schema.safeParse(testValues);
if (result.success) {
return null;
}
// Find error for this specific field
const fieldError = result.error.issues.find((issue) => issue.path.join('.') === name);
if (fieldError) {
return {
type: fieldError.code || 'validation_error',
message: fieldError.message,
field: name
};
}
return null;
}
catch (error) {
return {
type: 'validation_error',
message: error.message || 'Validation failed',
field: name
};
}
}
}
/**
* Schema adapter for Yup integration
*/
export class YupSchemaAdapter {
constructor(schema) {
this.schema = schema;
}
async validate(values) {
try {
await this.schema.validate(values);
return {};
}
catch (error) {
const errors = {};
if (error.inner) {
for (const validationError of error.inner) {
const fieldName = validationError.path || 'unknown';
errors[fieldName] = {
type: validationError.type || 'validation_error',
message: validationError.message,
field: fieldName
};
}
}
else if (error.path) {
errors[error.path] = {
type: error.type || 'validation_error',
message: error.message,
field: error.path
};
}
return errors;
}
}
async validateField(name, value, values) {
try {
const testValues = { ...values, [name]: value };
await this.schema.validateAt(name, testValues);
return null;
}
catch (error) {
return {
type: error.type || 'validation_error',
message: error.message || 'Validation failed',
field: name
};
}
}
}
/**
* Schema adapter for Joi integration
*/
export class JoiSchemaAdapter {
constructor(schema) {
this.schema = schema;
}
async validate(values) {
const result = this.schema.validate(values, { abortEarly: false });
if (!result.error) {
return {};
}
const errors = {};
if (result.error.details) {
for (const detail of result.error.details) {
const fieldName = detail.path.join('.');
errors[fieldName] = {
type: detail.type || 'validation_error',
message: detail.message,
field: fieldName
};
}
}
return errors;
}
async validateField(name, value, values) {
try {
const testValues = { ...values, [name]: value };
const result = this.schema.validate(testValues);
if (!result.error) {
return null;
}
// Find error for this specific field
const fieldError = result.error.details?.find(detail => detail.path.join('.') === name);
if (fieldError) {
return {
type: fieldError.type || 'validation_error',
message: fieldError.message,
field: name
};
}
return null;
}
catch (error) {
return {
type: 'validation_error',
message: error.message || 'Validation failed',
field: name
};
}
}
}
/**
* Validation manager for handling different validation scenarios
*/
export class ValidationManager {
constructor() {
this.customValidators = new Map();
}
static getInstance() {
if (!ValidationManager.instance) {
ValidationManager.instance = new ValidationManager();
}
return ValidationManager.instance;
}
/**
* Register a custom validator globally
*/
registerValidator(name, rule) {
this.customValidators.set(name, rule);
}
/**
* Get a registered validator
*/
getValidator(name) {
return this.customValidators.get(name);
}
/**
* Create schema adapter from different schema types
*/
createSchemaAdapter(schema) {
// Try to detect schema type
if (schema && typeof schema.parse === 'function' && typeof schema.safeParse === 'function') {
return new ZodSchemaAdapter(schema);
}
if (schema && typeof schema.validate === 'function' && typeof schema.validateAt === 'function') {
return new YupSchemaAdapter(schema);
}
if (schema && typeof schema.validate === 'function' && typeof schema.validateAsync === 'function') {
return new JoiSchemaAdapter(schema);
}
throw new Error('Unsupported schema type. Please use Zod, Yup, or Joi schemas.');
}
/**
* Validate a single field with multiple rules
*/
async validateField(value, rules, formValues) {
for (const rule of rules) {
try {
const result = rule.validate(value, formValues);
const isValid = result instanceof Promise ? await result : result;
if (!isValid) {
return {
type: rule.type,
message: rule.message,
field: 'unknown'
};
}
}
catch (error) {
return {
type: 'validation_error',
message: error instanceof Error ? error.message : 'Validation failed',
field: 'unknown'
};
}
}
return null;
}
/**
* Combine multiple validation rules
*/
combineRules(...rules) {
return rules;
}
/**
* Create conditional validation rule
*/
when(condition, thenRule, elseRule) {
return {
type: 'conditional',
message: thenRule.message,
validate: (value, formValues) => {
if (!formValues)
return true;
const shouldApplyThenRule = condition(formValues);
const ruleToApply = shouldApplyThenRule ? thenRule : elseRule;
if (!ruleToApply)
return true;
return ruleToApply.validate(value, formValues);
}
};
}
}
ValidationManager.instance = null;
/**
* Utility functions for validation
*/
export const validation = {
rules: ValidationRules,
manager: ValidationManager.getInstance(),
// Helper functions
createZodAdapter: (schema) => new ZodSchemaAdapter(schema),
createYupAdapter: (schema) => new YupSchemaAdapter(schema),
createJoiAdapter: (schema) => new JoiSchemaAdapter(schema),
// Common validation patterns
email: () => ValidationRules.email(),
required: () => ValidationRules.required(),
minLength: (min) => ValidationRules.minLength(min),
maxLength: (max) => ValidationRules.maxLength(max),
pattern: (regex, message) => ValidationRules.pattern(regex, message),
// Async validation helper
async: (validator, message) => ValidationRules.custom(validator, message)
};
//# sourceMappingURL=validation.js.map