-
-
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 #25 from medishen/dev/v2
feat(validator): implement comprehensive validation system with decor…
- Loading branch information
Showing
22 changed files
with
734 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { Constructor, RuleOperator, RuleType, VALIDATOR_METADATA } from '@gland/common'; | ||
import Reflector from '@gland/metadata'; | ||
import { ValidationField } from '../interface/validator.interface'; | ||
import { ValidationRules } from '../rules/validation.rules'; | ||
import { DependencyValidators } from '../rules/dependency-validators.rules'; | ||
|
||
export class RuleAction { | ||
private static coreValidator = new ValidationRules(); | ||
private static depValidator = new DependencyValidators(); | ||
static applyRule({ ruleName, param }: { ruleName: RuleType; param?: string }, value: unknown): boolean { | ||
switch (ruleName) { | ||
case 'min': | ||
case 'max': | ||
if (!param) throw new Error(`Parameter required for ${ruleName} rule`); | ||
return RuleAction.coreValidator[ruleName](value, param); | ||
default: | ||
return RuleAction.coreValidator[ruleName](value); | ||
} | ||
} | ||
static applyDependencyRule({ operator, value }: { operator: RuleOperator; value?: unknown }, dependentValue: unknown): boolean { | ||
switch (operator) { | ||
// Handle array-based operations | ||
case 'in': | ||
case 'notIn': | ||
if (!Array.isArray(value)) return false; | ||
return RuleAction.depValidator[operator](dependentValue, value); | ||
// Handle existence checks | ||
case 'exists': | ||
case 'notExists': | ||
return RuleAction.depValidator[operator](dependentValue); | ||
|
||
// Handle string operations | ||
case 'startsWith': | ||
case 'endsWith': | ||
case 'contains': | ||
case 'notContains': | ||
return RuleAction.validateStringOperation(operator, dependentValue, value); | ||
|
||
// Handle regex operations | ||
case 'matches': | ||
case 'notMatches': | ||
return RuleAction.validateRegexOperation(operator, dependentValue, value); | ||
|
||
// Handle comparison operations | ||
case 'greaterThan': | ||
case 'lessThan': | ||
case 'greaterOrEqual': | ||
case 'lessOrEqual': | ||
return RuleAction.validateComparison(operator, dependentValue, value); | ||
|
||
// Default equality checks | ||
default: | ||
return RuleAction.depValidator[operator](dependentValue, value); | ||
} | ||
} | ||
private static validateStringOperation(operator: RuleOperator, value: unknown, search: unknown): boolean { | ||
return typeof value === 'string' && typeof search === 'string' ? (RuleAction.depValidator[operator] as (value: string, search: string) => boolean)(value, search) : false; | ||
} | ||
|
||
private static validateRegexOperation(operator: RuleOperator, value: unknown, pattern: unknown): boolean { | ||
return typeof value === 'string' && typeof pattern === 'string' ? (RuleAction.depValidator[operator] as (value: string, pattern: string) => boolean)(value, pattern) : false; | ||
} | ||
|
||
private static validateComparison(operator: RuleOperator, a: unknown, b: unknown): boolean { | ||
const validTypes = (x: unknown) => typeof x === 'number' || x instanceof Date; | ||
return validTypes(a) && validTypes(b) ? (RuleAction.depValidator[operator] as <T extends number | Date>(a: T, b: T) => boolean)(a as number | Date, b as number | Date) : false; | ||
} | ||
|
||
static filter<T>(schemaClass: Constructor<T>, pick?: (keyof T)[], omit?: (keyof T)[]): Record<string, ValidationField> { | ||
const rules: Record<string, ValidationField> = Reflector.getMetadata(VALIDATOR_METADATA.RULES_METADATA, schemaClass) ?? {}; | ||
const filteredRules: Record<string, ValidationField> = {}; | ||
|
||
if (pick) { | ||
for (const key of pick) { | ||
if (key in rules) { | ||
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,8 @@ | ||
import { Context, VALIDATOR_METADATA } from '@gland/common'; | ||
import Reflector from '@gland/metadata'; | ||
|
||
export function Guard(...guards: ((ctx: Context) => void | Promise<void>)[]): MethodDecorator { | ||
return (target: any, propertyKey: string | symbol) => { | ||
Reflector.defineMetadata(VALIDATOR_METADATA.GUARD_FUNCTION_METADATA, guards, target.constructor, propertyKey); | ||
}; | ||
} |
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,5 @@ | ||
export * from './guard.decorator'; | ||
export * from './rule.decorator'; | ||
export * from './schema-reference.decorator'; | ||
export * from './schema.decorator'; | ||
export * from './validate.decorator'; |
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,17 @@ | ||
import { RuleMessages, VALIDATOR_METADATA } from '@gland/common'; | ||
import { ValidationField } from '../interface/validator.interface'; | ||
import Reflector from '@gland/metadata'; | ||
|
||
export function Rule(options?: { messages?: RuleMessages; 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.getMetadata(VALIDATOR_METADATA.RULES_METADATA, target.constructor) ?? {}; | ||
existingRules[propertyKey as string] = { | ||
rules: Array.isArray(rules) ? rules : [rules], | ||
messages: options?.messages ?? {}, | ||
options: options?.options ?? {}, | ||
}; | ||
Reflector.defineMetadata(VALIDATOR_METADATA.RULES_METADATA, existingRules, target.constructor); | ||
}; | ||
} |
23 changes: 23 additions & 0 deletions
23
packages/validators/decorators/schema-reference.decorator.ts
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,23 @@ | ||
import { Constructor, VALIDATOR_METADATA } from '@gland/common'; | ||
import { ValidationOptions } from '../interface/validator.interface'; | ||
import Reflector from '@gland/metadata'; | ||
|
||
/** | ||
* `@SchemaRef` decorator is used to reference nested schema classes and apply optional validation options. | ||
* It dynamically infers the schema class type and attaches it to the metadata. | ||
*/ | ||
export function SchemaRef<T>(options?: ValidationOptions<T>): PropertyDecorator { | ||
return function (target, propertyKey) { | ||
const instance = new (target.constructor as any)(); | ||
const schemaClass: Constructor<T> = instance[propertyKey]; | ||
// Ensure schema class is assigned correctly if not set | ||
if (typeof schemaClass !== 'function' || !Reflector.getMetadata(VALIDATOR_METADATA.SCHEMA_METADATA_WATERMARK, schemaClass)) { | ||
throw new Error(`The property '${String(propertyKey)}' must reference a valid schema class.`); | ||
} | ||
// Retrieve existing nested schemas or initialize a new object | ||
const nestedSchemas = Reflector.getMetadata(VALIDATOR_METADATA.NESTED_SCHEMA_METADATA, target.constructor) ?? {}; | ||
|
||
nestedSchemas[propertyKey] = { schemaClass: schemaClass, options: options ?? {} }; | ||
Reflector.defineMetadata(VALIDATOR_METADATA.NESTED_SCHEMA_METADATA, 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { VALIDATOR_METADATA } from '@gland/common'; | ||
import { SchemaOptions } from '../interface/validator.interface'; | ||
import Reflector from '@gland/metadata'; | ||
|
||
export function Schema(options: SchemaOptions): ClassDecorator { | ||
const { section = 'body', defaultRules } = options; | ||
|
||
if (defaultRules?.includes('inherit')) { | ||
throw new Error("The 'inherit' rule cannot be used as a default rule in the schema. Please remove it from the defaultRules array."); | ||
} | ||
return (target) => { | ||
Reflector.defineMetadata(VALIDATOR_METADATA.SCHEMA_METADATA_WATERMARK, true, target); | ||
Reflector.defineMetadata(VALIDATOR_METADATA.SCHEMA_SECTION_METADATA, section, target); | ||
|
||
if (defaultRules) { | ||
Reflector.defineMetadata(VALIDATOR_METADATA.RULES_DEFAULTS_METADATA, defaultRules, target); | ||
} | ||
}; | ||
} |
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,18 @@ | ||
import { Constructor } from '@gland/common/interfaces'; | ||
import { ValidatorEngine } from '../validators/validator-engine'; | ||
export function Validate<T>(schemaClass: Constructor<T>, options: { returnFirstError?: boolean } = {}): MethodDecorator { | ||
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) { | ||
const originalMethod = descriptor.value; | ||
|
||
descriptor.value = function (...args: any[]) { | ||
const data = args[0]; | ||
|
||
const validationErrors = ValidatorEngine.validate<T>(schemaClass, data, options.returnFirstError ?? false); | ||
if (validationErrors && Object.keys(validationErrors).length > 0) { | ||
return validationErrors; | ||
} | ||
|
||
return originalMethod.apply(this, args); | ||
}; | ||
}; | ||
} |
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 @@ | ||
export * from './validators'; |
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,66 @@ | ||
import { RulesList, RuleMessages, RuleOperator, Constructor, RequestSchema } from '@gland/common'; | ||
export interface ValidationField { | ||
/** | ||
* The validation rules as a string or a custom validation function. | ||
*/ | ||
rules: RulesList; | ||
|
||
/** | ||
* Custom error messages for specific rule types. | ||
*/ | ||
messages?: RuleMessages; | ||
/** | ||
* Additional options for the validation field. | ||
*/ | ||
options?: { | ||
/** | ||
* A custom validation function for advanced scenarios. | ||
*/ | ||
custom?: (value: any) => 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: RuleOperator; | ||
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. | ||
*/ | ||
dependsOn?: { | ||
/** | ||
* The schema class to validate if the condition is met. | ||
*/ | ||
schema: Constructor<any>; | ||
}[]; | ||
} | ||
export interface SchemaOptions { | ||
/** | ||
* Specifies a name or section for the schema (e.g., "body", "query"). | ||
*/ | ||
section?: RequestSchema; | ||
|
||
/** | ||
* Applies a default set of rules to all fields in the schema. | ||
*/ | ||
defaultRules?: RulesList; | ||
} |
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,16 @@ | ||
{ | ||
"name": "@gland/validator", | ||
"version": "1.0.0", | ||
"author": "Mahdi", | ||
"license": "MIT", | ||
"scripts": { | ||
"build": "tsc" | ||
}, | ||
"dependencies": { | ||
"@gland/common": "workspace:*", | ||
"@gland/metadata": "workspace:*" | ||
}, | ||
"devDependencies": { | ||
"typescript": "^5.5.4" | ||
} | ||
} |
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,78 @@ | ||
import { DependencyRuleValidation } from "../types/rules-validation.types"; | ||
|
||
// Dependency validation rules implementation | ||
export class DependencyValidators implements DependencyRuleValidation { | ||
equal<T>(a: T, b: T): boolean { | ||
return a === b; | ||
} | ||
|
||
notEqual<T>(a: T, b: T): boolean { | ||
return a !== b; | ||
} | ||
|
||
greaterThan<T extends number | Date>(a: T, b: T): boolean { | ||
return a > b; | ||
} | ||
|
||
lessThan<T extends number | Date>(a: T, b: T): boolean { | ||
return a < b; | ||
} | ||
|
||
greaterOrEqual<T extends number | Date>(a: T, b: T): boolean { | ||
return a >= b; | ||
} | ||
|
||
lessOrEqual<T extends number | Date>(a: T, b: T): boolean { | ||
return a <= b; | ||
} | ||
|
||
in<T>(value: T, list: T[]): boolean { | ||
return list.includes(value); | ||
} | ||
|
||
notIn<T>(value: T, list: T[]): boolean { | ||
return !list.includes(value); | ||
} | ||
|
||
exists<T>(value: T | null | undefined): value is T { | ||
return value !== null && value !== undefined; | ||
} | ||
|
||
notExists<T>(value: T | null | undefined): boolean { | ||
return value === null || value === undefined; | ||
} | ||
|
||
startsWith(value: string, search: string): boolean { | ||
return value.startsWith(search); | ||
} | ||
|
||
endsWith(value: string, search: string): boolean { | ||
return value.endsWith(search); | ||
} | ||
|
||
contains(value: string, search: string): boolean { | ||
return value.includes(search); | ||
} | ||
|
||
notContains(value: string, search: string): boolean { | ||
return !value.includes(search); | ||
} | ||
|
||
matches(value: string, pattern: string): boolean { | ||
try { | ||
const regex = new RegExp(pattern); | ||
return regex.test(value); | ||
} catch { | ||
return false; | ||
} | ||
} | ||
|
||
notMatches(value: string, pattern: string): boolean { | ||
try { | ||
const regex = new RegExp(pattern); | ||
return !regex.test(value); | ||
} catch { | ||
return false; | ||
} | ||
} | ||
} |
Oops, something went wrong.