Skip to content

Commit

Permalink
Merge pull request #25 from medishen/dev/v2
Browse files Browse the repository at this point in the history
feat(validator): implement comprehensive validation system with decor…
  • Loading branch information
0xii00 authored Jan 27, 2025
2 parents 7e5a383 + d551734 commit 619b92c
Show file tree
Hide file tree
Showing 22 changed files with 734 additions and 0 deletions.
91 changes: 91 additions & 0 deletions packages/validators/actions/rule-action.ts
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;
}
}
8 changes: 8 additions & 0 deletions packages/validators/decorators/guard.decorator.ts
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);
};
}
5 changes: 5 additions & 0 deletions packages/validators/decorators/index.ts
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';
17 changes: 17 additions & 0 deletions packages/validators/decorators/rule.decorator.ts
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 packages/validators/decorators/schema-reference.decorator.ts
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);
};
}
19 changes: 19 additions & 0 deletions packages/validators/decorators/schema.decorator.ts
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);
}
};
}
18 changes: 18 additions & 0 deletions packages/validators/decorators/validate.decorator.ts
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);
};
};
}
1 change: 1 addition & 0 deletions packages/validators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './validators';
66 changes: 66 additions & 0 deletions packages/validators/interface/validator.interface.ts
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;
}
16 changes: 16 additions & 0 deletions packages/validators/package.json
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"
}
}
78 changes: 78 additions & 0 deletions packages/validators/rules/dependency-validators.rules.ts
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;
}
}
}
Loading

0 comments on commit 619b92c

Please sign in to comment.