-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from medishen/dev/v2
Added schema validation error handling nested schema support and validation field options
- Loading branch information
Showing
17 changed files
with
537 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export enum ValidationMetadataKey { | ||
SCHEMA = 'validation:schema', | ||
DEFAULT_RULES = 'validation:schema:default_rules', | ||
VALIDATION_STATUS = 'validation:schema:status', | ||
RULES = 'validation:rules', | ||
VALIDATOR = 'validation:validator', | ||
NESTED = 'validation:nested', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { ValidationSchema } from '../types'; | ||
import { RuleString, RuleType, ValidationOperator } from '../types/validator.type'; | ||
import { Constructor } from './module.interface'; | ||
export interface ValidationField { | ||
/** | ||
* The validation rules as a string or a custom validation function. | ||
*/ | ||
rules: RuleString; | ||
|
||
/** | ||
* Custom error messages for specific rule types. | ||
*/ | ||
messages?: Partial<Record<RuleType, string>>; | ||
/** | ||
* Additional options for the validation field. | ||
*/ | ||
options?: { | ||
/** | ||
* A custom validation function for advanced scenarios. | ||
*/ | ||
custom?: (value: any) => boolean | Promise<boolean>; | ||
|
||
/** | ||
* Specifies dependencies between fields within the same schema. | ||
* For example, a "passwordConfirm" field that depends on the "password" field. | ||
*/ | ||
dependsOn?: { | ||
field: string; // The field this depends on. | ||
operator: ValidationOperator; | ||
value?: any; // Value to compare against, if applicable. | ||
}; | ||
}; | ||
} | ||
export interface ValidationOptions<T> { | ||
/** | ||
* Specifies which fields of the schema to validate. | ||
* Only the fields in this array will be validated. | ||
*/ | ||
pick?: (keyof T)[]; | ||
|
||
/** | ||
* Specifies which fields of the schema to exclude from validation. | ||
* All other fields will be validated. | ||
*/ | ||
omit?: (keyof T)[]; | ||
|
||
/** | ||
* Defines conditional relationships between schemas. | ||
* If the condition is valid, it validates the specified schema. | ||
*/ | ||
conditions?: { | ||
/** | ||
* The schema class to validate if the condition is met. | ||
*/ | ||
schema: Constructor<any>; | ||
|
||
/** | ||
* Custom error message to show if the condition is not met. | ||
*/ | ||
message: string; | ||
}[]; | ||
} | ||
export interface SchemaOptions { | ||
/** | ||
* Specifies a name or section for the schema (e.g., "body", "query"). | ||
*/ | ||
section?: ValidationSchema; | ||
|
||
/** | ||
* Applies a default set of rules to all fields in the schema. | ||
*/ | ||
defaultRules?: RuleString; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import { ServerRequest } from '../interfaces'; | ||
|
||
export type MiddlewareFn = (ctx: ServerRequest, next: Function) => void | Promise<void>; | ||
export type NextFunction = () => void | Promise<void>; | ||
export type MiddlewareFn = (ctx: ServerRequest, next: NextFunction) => void | Promise<void>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Constructor, ValidationOptions } from '../interfaces'; | ||
|
||
export type RuleType = 'required' | 'string' | 'boolean' | 'array' | 'email' | 'url' | 'optional' | 'integer' | 'date' | 'min' | 'max' | 'regex' | 'inherit'; | ||
export type RuleParameter = RuleType | `min:${number}` | `max:${number}`; | ||
export type RuleString = RuleParameter | Array<RuleParameter>; | ||
export type ValidationOperator = | ||
| 'equal' // Value must equal a specific value | ||
| 'notEqual' // Value must not equal a specific value | ||
| 'greaterThan' // Value must be greater than a specific value | ||
| 'lessThan' // Value must be less than a specific value | ||
| 'greaterOrEqual' // Value must be greater than or equal to a specific value | ||
| 'lessOrEqual' // Value must be less than or equal to a specific value | ||
| 'in' // Value must exist in a list of allowed values | ||
| 'notIn' // Value must not exist in a list of disallowed values | ||
| 'exists' // Dependent field must exist | ||
| 'notExists' // Dependent field must not exist | ||
| 'startsWith' // Value must start with a specific string | ||
| 'endsWith' // Value must end with a specific string | ||
| 'contains' // Value must contain a specific string | ||
| 'notContains' // Value must not contain a specific string | ||
| 'matches' // Value must match a specific regex | ||
| 'notMatches'; // Value must not match a specific regex | ||
export type NestedSchemas<T> = Record<string, { schemaClass: Constructor<T>; options: ValidationOptions<T> }>; | ||
export type ValidationSchema = 'body' | 'query' | 'headers' | 'params'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import Reflector from '../metadata'; | ||
import { ValidationMetadataKey } from '../common/enums'; | ||
import { SchemaOptions, ValidationField, ValidationOptions } from '../common/interfaces'; | ||
import { ValidationMessages } from '../validator/config'; | ||
export function Rule(options?: { messages?: ValidationMessages; options?: ValidationField['options'] }): PropertyDecorator { | ||
return function (target, propertyKey) { | ||
const instance = new (target.constructor as any)(); | ||
const rules = instance[propertyKey]; | ||
const existingRules: Record<string, ValidationField> = Reflector.get(ValidationMetadataKey.RULES, target.constructor) ?? {}; | ||
existingRules[propertyKey as string] = { | ||
rules: Array.isArray(rules) ? rules : [rules], | ||
messages: options?.messages ?? {}, | ||
options: options?.options ?? {}, | ||
}; | ||
Reflector.define(ValidationMetadataKey.RULES, existingRules, target.constructor); | ||
}; | ||
} | ||
|
||
export function Schema({ section = 'body', defaultRules }: SchemaOptions): ClassDecorator { | ||
return (target) => { | ||
Reflector.define(ValidationMetadataKey.SCHEMA, { section, defaultRules: defaultRules }, target); | ||
}; | ||
} | ||
export function NestedSchema<T = any>(options?: ValidationOptions<T>): PropertyDecorator { | ||
return function (target, propertyKey) { | ||
const instance = new (target.constructor as any)(); | ||
let schemaClass: any = instance[propertyKey].constructor; | ||
if (schemaClass.toString().includes('[native code]')) { | ||
schemaClass = instance[propertyKey]; | ||
} | ||
const nestedSchemas = Reflector.get(ValidationMetadataKey.NESTED, target.constructor) ?? {}; | ||
nestedSchemas[propertyKey] = { schemaClass: schemaClass, options: options ?? {} }; | ||
Reflector.define(ValidationMetadataKey.NESTED, nestedSchemas, target.constructor); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { ValidationField } from '../common/interfaces'; | ||
import { DefaultMessages, renderMessage } from './config'; | ||
import { RuleString } from '../common/types'; | ||
import { RuleAction } from './RuleAction'; | ||
|
||
export class FieldValidator { | ||
static async validateField(value: any, fieldRules: ValidationField, fieldKey: string, returnFirstError: boolean, allData: Record<string, any>, defaultRules: RuleString = []): Promise<string[]> { | ||
const errors: string[] = []; | ||
const rules = Array.isArray(fieldRules.rules) | ||
? [ | ||
...new Set(fieldRules.rules.includes('inherit') || defaultRules ? [...defaultRules, ...fieldRules.rules.filter((rule) => rule !== 'inherit')] : fieldRules.rules), // Avoid duplicates | ||
] | ||
: fieldRules.rules; | ||
|
||
/// Handle `dependsOn` validation | ||
if (fieldRules.options?.dependsOn) { | ||
const { field, operator, value: dependencyValue } = fieldRules.options.dependsOn; | ||
const dependentFieldValue = allData[field]; | ||
if (!RuleAction.applyDependencyRule({ operator, dependencyValue }, dependentFieldValue)) { | ||
const template = (fieldRules.messages as any)?.dependsOn || DefaultMessages.dependsOn; | ||
const message = renderMessage(template, { field: fieldKey, dependentField: field }); | ||
errors.push(message); | ||
|
||
if (returnFirstError) { | ||
return errors; | ||
} | ||
} | ||
} | ||
// Handle `custom` validation | ||
if (fieldRules.options?.custom) { | ||
const isValid = await fieldRules.options.custom(value); | ||
if (!isValid) { | ||
const template = (fieldRules.messages as any)?.custom || DefaultMessages.custom; | ||
const message = renderMessage(template, { field: fieldKey }); | ||
errors.push(message); | ||
|
||
if (returnFirstError) { | ||
return errors; | ||
} | ||
} | ||
} | ||
for (const rule of rules) { | ||
const [ruleName, param] = rule.split(':'); | ||
if (!RuleAction.applyRule({ param, ruleName }, value)) { | ||
const template = (fieldRules.messages as any)?.[ruleName] || (DefaultMessages as any)[ruleName]; | ||
const message = renderMessage(template, { field: fieldKey, value: param }); | ||
errors.push(message); | ||
if (returnFirstError) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return errors; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { ValidationMetadataKey } from '../common/enums'; | ||
import { Constructor, ValidationField } from '../common/interfaces'; | ||
import Reflector from '../metadata'; | ||
|
||
export class RuleAction { | ||
static applyRule({ param, ruleName }: { ruleName: string; param: string }, value: any): boolean { | ||
switch (ruleName) { | ||
case 'required': | ||
return value !== undefined && value !== null && value !== ''; | ||
case 'string': | ||
return typeof value === 'string'; | ||
case 'integer': | ||
return Number.isInteger(value); | ||
case 'boolean': | ||
return typeof value === 'boolean'; | ||
case 'email': | ||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); | ||
case 'min': | ||
return typeof value === 'string' && value.length >= parseInt(param); | ||
case 'max': | ||
return typeof value === 'string' && value.length <= parseInt(param); | ||
case 'array': | ||
return Array.isArray(value); | ||
case 'regex': | ||
return new RegExp(param).test(value); | ||
case 'url': | ||
return /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?$/.test(value); | ||
default: | ||
return true; | ||
} | ||
} | ||
static applyDependencyRule({ operator, dependencyValue }: { operator: string; dependencyValue?: any }, dependentFieldValue: any): boolean { | ||
switch (operator) { | ||
case 'equal': | ||
return dependentFieldValue === dependencyValue; | ||
case 'notEqual': | ||
return dependentFieldValue !== dependencyValue; | ||
case 'greaterThan': | ||
return dependentFieldValue > dependencyValue; | ||
case 'lessThan': | ||
return dependentFieldValue < dependencyValue; | ||
case 'greaterOrEqual': | ||
return dependentFieldValue >= dependencyValue; | ||
case 'lessOrEqual': | ||
return dependentFieldValue <= dependencyValue; | ||
case 'in': | ||
return Array.isArray(dependencyValue) && dependencyValue.includes(dependentFieldValue); | ||
case 'notIn': | ||
return Array.isArray(dependencyValue) && !dependencyValue.includes(dependentFieldValue); | ||
case 'exists': | ||
return dependentFieldValue !== undefined && dependentFieldValue !== null; | ||
case 'notExists': | ||
return dependentFieldValue === undefined || dependentFieldValue === null; | ||
case 'startsWith': | ||
return typeof dependentFieldValue === 'string' && dependentFieldValue.startsWith(dependencyValue); | ||
case 'endsWith': | ||
return typeof dependentFieldValue === 'string' && dependentFieldValue.endsWith(dependencyValue); | ||
case 'contains': | ||
return typeof dependentFieldValue === 'string' && dependentFieldValue.includes(dependencyValue); | ||
case 'notContains': | ||
return typeof dependentFieldValue === 'string' && !dependentFieldValue.includes(dependencyValue); | ||
case 'matches': | ||
return typeof dependentFieldValue === 'string' && new RegExp(dependencyValue).test(dependentFieldValue); | ||
case 'notMatches': | ||
return typeof dependentFieldValue === 'string' && !new RegExp(dependencyValue).test(dependentFieldValue); | ||
default: | ||
return true; // Default to valid for unknown rules | ||
} | ||
} | ||
static filter<T>(schemaClass: Constructor<T>, pick?: (keyof T)[], omit?: (keyof T)[]): Record<string, ValidationField> { | ||
const rules: Record<string, ValidationField> = Reflector.get(ValidationMetadataKey.RULES, schemaClass) ?? {}; | ||
const filteredRules: Record<string, ValidationField> = {}; | ||
if (pick) { | ||
for (const key of pick) { | ||
if (rules[key as string]) { | ||
filteredRules[key as string] = rules[key as string]; | ||
} | ||
} | ||
} else if (omit) { | ||
for (const key in rules) { | ||
if (!omit.includes(key as keyof T)) { | ||
filteredRules[key] = rules[key]; | ||
} | ||
} | ||
} else { | ||
return rules; // No filtering if neither `pick` nor `omit` is provided | ||
} | ||
|
||
return filteredRules; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export class ValidationError { | ||
errors: Record<string, Record<string, string[]>> = {}; | ||
|
||
addError(section: string, field: string, message: string[]): void { | ||
if (!this.errors[section]) this.errors[section] = {}; | ||
if (!this.errors[section][field]) this.errors[section][field] = []; | ||
this.errors[section][field].push(...message); | ||
} | ||
} |
Oops, something went wrong.