Skip to content

Commit

Permalink
Merge pull request #28 from medishen/dev/v2
Browse files Browse the repository at this point in the history
Fix Validator Engine and Improve Rule Action Logic with Enhanced Validation Handling
  • Loading branch information
0xii00 authored Jan 31, 2025
2 parents db2d1fc + 157a033 commit 6726e03
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 221 deletions.
11 changes: 11 additions & 0 deletions packages/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Qiks } from '@medishn/qiks';
import { CacheConfigQiks } from '@medishn/qiks/dist/types/CacheTypes';

/**
* A caching system built on top of @medishn/qiks.
*/
export class MemoryCacheStore<K, V> extends Qiks<K, V> {
constructor(options?: CacheConfigQiks<K>) {
super(options);
}
}
12 changes: 12 additions & 0 deletions packages/cache/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@gland/cache",
"version": "1.0.0",
"author": "Mahdi",
"license": "MIT",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.5.4"
}
}
11 changes: 11 additions & 0 deletions packages/cache/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "."
},
"files": [],
"include": ["**/*.ts"],
"exclude": ["node_modules"],
"references": []
}
2 changes: 1 addition & 1 deletion packages/metadata/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@gland/metadata",
"version": "1.0.0",
"author": "Mahdi",
"description": "A metadata management library for the Gland framework, offering decorators and tools to handle metadata for routes, controllers, and schemas.",
"license": "MIT",
"scripts": {
"build": "tsc"
Expand Down
13 changes: 10 additions & 3 deletions packages/validators/actions/rule-action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Constructor, RuleOperator, RuleType, VALIDATOR_METADATA } from '@gland/common';
import { Constructor, isNumber, RuleOperator, RuleType, VALIDATOR_METADATA } from '@gland/common';
import Reflector from '@gland/metadata';
import { ValidationField } from '../interface/validator.interface';
import { ValidationRules } from '../rules/validation.rules';
Expand Down Expand Up @@ -62,12 +62,19 @@ export class RuleAction {
}

private static validateComparison(operator: RuleOperator, a: unknown, b: unknown): boolean {
const validTypes = (x: unknown) => typeof x === 'number' || x instanceof Date;
const validTypes = (x: unknown) => {
// Convert string numbers to numbers and check if the value is a valid number or Date
if (typeof x === 'string' && !isNaN(Number(x))) {
x = Number(x);
}
return typeof x === 'number' || Number.isSafeInteger(x) || 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 rules = Reflector.getMetadata<Record<string, ValidationField>>(VALIDATOR_METADATA.RULES_METADATA, schemaClass) ?? {};

const filteredRules: Record<string, ValidationField> = {};

if (pick) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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.`);
Expand Down
2 changes: 1 addition & 1 deletion packages/validators/rules/validation.rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RuleValidation } from '../types/rules-validation.types';

export class ValidationRules implements Partial<RuleValidation> {
required(value: unknown): value is NonNullable<unknown> {
return !isNil(value);
return !isNil(value) && value !== '';
}

string(value: unknown): value is string {
Expand Down
19 changes: 14 additions & 5 deletions packages/validators/validators/validator-engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SectionErrors, Constructor, VALIDATOR_METADATA, RulesList, isNil } from '@gland/common';
import { SectionErrors, Constructor, VALIDATOR_METADATA, RulesList, isNil, RuleCondition } from '@gland/common';
import { FieldValidator } from './field-validator';
import { RuleAction } from '../actions/rule-action';
import { ValidationContainer } from './container';
Expand Down Expand Up @@ -45,8 +45,17 @@ export class ValidatorEngine {
/** Validate fields and collect errors */
static validateSchemaFields<T>(schemaClass: Constructor<T>, data: Record<string, any>, contianer: ValidationContainer, returnFirstError: boolean) {
const rules = ValidatorEngine.getValidationRules(schemaClass);
const defaultRules = ValidatorEngine.getDefaultRules(schemaClass);
const section = ValidatorEngine.getSchemaSection(schemaClass);

if (!rules) {
throw Error(`No validation rules found for schema: ${schemaClass.name}`);
}

if (!section) {
throw Error(`No section found for schema: ${schemaClass.name}`);
}
const defaultRules = ValidatorEngine.getDefaultRules(schemaClass);

const sectionData = data[section] ?? {};

for (const [field, fieldRules] of Object.entries(rules)) {
Expand All @@ -68,15 +77,15 @@ export class ValidatorEngine {
}

private static getValidationRules<T>(schemaClass: Constructor<T>): Record<string, ValidationField> {
return Reflector.getMetadata(VALIDATOR_METADATA.RULES_METADATA, schemaClass);
return Reflector.getMetadata(VALIDATOR_METADATA.RULES_METADATA, schemaClass)!;
}

private static getDefaultRules<T>(schemaClass: Constructor<T>): RulesList {
return Reflector.getMetadata(VALIDATOR_METADATA.RULES_DEFAULTS_METADATA, schemaClass);
return Reflector.getMetadata(VALIDATOR_METADATA.RULES_DEFAULTS_METADATA, schemaClass)!;
}

private static getSchemaSection<T>(schemaClass: Constructor<T>): string {
return Reflector.getMetadata(VALIDATOR_METADATA.SCHEMA_SECTION_METADATA, schemaClass);
return Reflector.getMetadata<string>(VALIDATOR_METADATA.SCHEMA_SECTION_METADATA, schemaClass)!;
}
private static mergeRules(fieldRules: ValidationField): ValidationField {
return {
Expand Down
48 changes: 48 additions & 0 deletions test/mocks/base-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SinonStubbedInstance } from 'sinon';
import sinon from 'sinon';

export interface Mock<T> {
setup(): void;
restore(): void;
reset(): void;
get stub(): SinonStubbedInstance<T>;
}

export abstract class MockBase<T extends object> implements Mock<T> {
protected original: Partial<T> = {};
protected stubs: SinonStubbedInstance<T> = {} as SinonStubbedInstance<T>;
protected target: any;

constructor(protected mockTarget: T) {
this.target = mockTarget;
}

abstract setup(): void;
abstract restore(): void;

reset(): void {
Object.values(this.stubs).forEach((stub: any) => {
if (typeof stub.reset === 'function') stub.reset();
});
}

get stub(): SinonStubbedInstance<T> {
return this.stubs;
}

protected createStubs(methods: Array<keyof T>): void {
methods.forEach((method) => {
if (typeof this.mockTarget[method] === 'function') {
this.original[method] = this.mockTarget[method];
(this.stubs as any)[method] = sinon.stub(this.target, method as string);
}
});
}
protected restoreMethods(methods: Array<keyof T>): void {
methods.forEach((method) => {
if (this.original[method]) {
this.target[method] = this.original[method];
}
});
}
}
1 change: 1 addition & 0 deletions test/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './reflector.mock';
59 changes: 20 additions & 39 deletions test/mocks/reflector.mock.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,22 @@
import sinon from 'sinon';
import Reflector from '../../dist/metadata';

export const createMockReflector = () => {
const mockReflector = {
define: sinon.stub(),
has: sinon.stub().returns(false),
get: sinon.stub().returns(undefined),
delete: sinon.stub().returns(false),
clear: sinon.stub(),
keys: sinon.stub().returns([]),
list: sinon.stub().returns(null),
allList: sinon.stub().returns(new Map()),
getRoutes: sinon.stub().returns([]),
update: sinon.stub(),
};

// Save original methods
const originalMethods = {
define: Reflector.define,
has: Reflector.has,
get: Reflector.get,
delete: Reflector.delete,
clear: Reflector.clear,
keys: Reflector.keys,
list: Reflector.list,
allList: Reflector.allList,
getRoutes: Reflector.getRoutes,
update: Reflector.update,
};

// Override Reflector
Object.assign(Reflector, mockReflector);

const restore = () => {
Object.assign(Reflector, originalMethods);
import Reflector from '../../packages/metadata';
import { MockBase } from './base-mock';
export class ReflectorMock extends MockBase<typeof Reflector> {
private methods: (keyof typeof Reflector)[] = ['clearMetadata', 'defineMetadata', 'deleteMetadata', 'getMetadata', 'getMetadataKeys', 'hasMetadata', 'listMetadata', 'metadata'];
constructor() {
super(Reflector);
}
setup(): void {
this.createStubs(this.methods);
// Default stubs
this.stub.hasMetadata.returns(false);
this.stub.getMetadata.returns(undefined);
this.stub.deleteMetadata.returns(false);
this.stub.getMetadataKeys.returns([]);
this.stub.listMetadata.returns(null);
}
restore(): void {
this.restoreMethods(this.methods);
sinon.restore();
};

return { mockReflector, restore };
};
}
}
Loading

0 comments on commit 6726e03

Please sign in to comment.