Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typescript"
],
"scripts": {
"benchmark": "ts-node --transpile-only test/benchmark.ts",
"build": "npm run build:cjs",
"build:clean": "rimraf build",
"build:es2015": "tsc --project tsconfig.prod.esm2015.json",
Expand Down
10 changes: 5 additions & 5 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ export interface UseContainerOptions {
* container simply creates a new instance of the given class.
*/
const defaultContainer: { get<T>(someClass: { new (...args: any[]): T } | Function): T } = new (class {
private instances: { type: Function; object: any }[] = [];
private instances = new Map<Function, any>();
get<T>(someClass: { new (...args: any[]): T }): T {
let instance = this.instances.find(instance => instance.type === someClass);
let instance = this.instances.get(someClass);
if (!instance) {
instance = { type: someClass, object: new someClass() };
this.instances.push(instance);
instance = new someClass();
this.instances.set(someClass, instance);
}

return instance.object;
return instance;
}
})();

Expand Down
3 changes: 2 additions & 1 deletion src/decorator/array/ArrayContains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const ARRAY_CONTAINS = 'arrayContains';
export function arrayContains(array: unknown, values: any[]): boolean {
if (!Array.isArray(array)) return false;

return values.every(value => array.indexOf(value) !== -1);
const arraySet = new Set(array);
return values.every(value => arraySet.has(value));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/decorator/array/ArrayNotContains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const ARRAY_NOT_CONTAINS = 'arrayNotContains';
export function arrayNotContains(array: unknown, values: any[]): boolean {
if (!Array.isArray(array)) return false;

return values.every(value => array.indexOf(value) === -1);
const arraySet = new Set(array);
return values.every(value => !arraySet.has(value));
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/decorator/array/ArrayUnique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export function arrayUnique(array: unknown[], identifier?: ArrayUniqueIdentifier
array = array.map(o => (o != null ? identifier(o) : o));
}

const uniqueItems = array.filter((a, b, c) => c.indexOf(a) === b);
return array.length === uniqueItems.length;
return new Set(array).size === array.length;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/decorator/common/IsIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const IS_IN = 'isIn';
* Checks if given value is in a array of allowed values.
*/
export function isIn(value: unknown, possibleValues: readonly unknown[]): boolean {
return Array.isArray(possibleValues) && possibleValues.some(possibleValue => possibleValue === value);
return Array.isArray(possibleValues) && possibleValues.includes(value);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/decorator/common/IsNotIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const IS_NOT_IN = 'isNotIn';
* Checks if given value not in a array of allowed values.
*/
export function isNotIn(value: unknown, possibleValues: readonly unknown[]): boolean {
return !Array.isArray(possibleValues) || !possibleValues.some(possibleValue => possibleValue === value);
return !Array.isArray(possibleValues) || !possibleValues.includes(value);
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/decorator/object/IsNotEmptyObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ export function isNotEmptyObject(value: unknown, options?: { nullable?: boolean
}

if (options?.nullable === false) {
return !Object.values(value).every(propertyValue => propertyValue === null || propertyValue === undefined);
for (const key in value) {
if (value.hasOwnProperty(key)) {
const propertyValue = (value as any)[key];
if (propertyValue !== null && propertyValue !== undefined) {
return true;
}
}
}
return false;
}

for (const key in value) {
Expand Down
5 changes: 3 additions & 2 deletions src/decorator/typechecker/IsEnum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const IS_ENUM = 'isEnum';
* Checks if a given value is the member of the provided enum.
*/
export function isEnum(value: unknown, entity: any): boolean {
const enumValues = Object.keys(entity).map(k => entity[k]);
const enumValues = Object.values(entity);
return enumValues.includes(value);
}

Expand All @@ -24,12 +24,13 @@ function validEnumValues(entity: any): string[] {
* Checks if a given value is the member of the provided enum.
*/
export function IsEnum(entity: object, validationOptions?: ValidationOptions): PropertyDecorator {
const enumValuesSet = new Set(Object.values(entity));
return ValidateBy(
{
name: IS_ENUM,
constraints: [entity, validEnumValues(entity)],
validator: {
validate: (value, args): boolean => isEnum(value, args?.constraints[0]),
validate: (value): boolean => enumValuesSet.has(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be one of the following values: $constraint2',
validationOptions
Expand Down
7 changes: 6 additions & 1 deletion src/metadata/ConstraintMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class ConstraintMetadata {
// Constructor
// -------------------------------------------------------------------------

private _instance!: ValidatorConstraintInterface;

constructor(target: Function, name?: string, async: boolean = false) {
this.target = target;
this.name = name;
Expand All @@ -42,6 +44,9 @@ export class ConstraintMetadata {
* Instance of the target custom validation class which performs validation.
*/
get instance(): ValidatorConstraintInterface {
return getFromContainer<ValidatorConstraintInterface>(this.target);
if (!this._instance) {
this._instance = getFromContainer<ValidatorConstraintInterface>(this.target);
}
return this._instance;
}
}
107 changes: 104 additions & 3 deletions src/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ import { ConstraintMetadata } from './ConstraintMetadata';
import { ValidationSchema } from '../validation-schema/ValidationSchema';
import { ValidationSchemaToMetadataTransformer } from '../validation-schema/ValidationSchemaToMetadataTransformer';
import { getGlobal } from '../utils';
import { ValidationTypes } from '../validation/ValidationTypes';

export interface PartitionedPropertyMetadata {
defined: ValidationMetadata[];
custom: ValidationMetadata[];
nested: ValidationMetadata[];
conditional: ValidationMetadata[];
all: ValidationMetadata[];
hasPromiseValidation: boolean;
/** True when only custom validators exist (no defined/nested/conditional) — enables fast path */
customOnly: boolean;
}

export type PartitionedMetadata = Record<string, PartitionedPropertyMetadata>;

/**
* Storage all metadatas.
Expand All @@ -12,8 +26,13 @@ export class MetadataStorage {
// Private properties
// -------------------------------------------------------------------------

private static _nextId = 0;

private validationMetadatas: Map<any, ValidationMetadata[]> = new Map();
private constraintMetadatas: Map<any, ConstraintMetadata[]> = new Map();
private targetMetadataCache: Map<string, ValidationMetadata[]> = new Map();
private groupedMetadataCache: Map<string, Record<string, ValidationMetadata[]>> = new Map();
private partitionedMetadataCache: Map<string, PartitionedMetadata> = new Map();

get hasValidationMetaData(): boolean {
return !!this.validationMetadatas.size;
Expand All @@ -35,6 +54,10 @@ export class MetadataStorage {
* Adds a new validation metadata.
*/
addValidationMetadata(metadata: ValidationMetadata): void {
this.targetMetadataCache.clear();
this.groupedMetadataCache.clear();
this.partitionedMetadataCache.clear();

const existingMetadata = this.validationMetadatas.get(metadata.target);

if (existingMetadata) {
Expand All @@ -60,25 +83,101 @@ export class MetadataStorage {
/**
* Groups metadata by their property names.
*/
groupByPropertyName(metadata: ValidationMetadata[]): { [propertyName: string]: ValidationMetadata[] } {
groupByPropertyName(
metadata: ValidationMetadata[],
cacheKey?: string
): { [propertyName: string]: ValidationMetadata[] } {
if (cacheKey) {
const cached = this.groupedMetadataCache.get(cacheKey);
if (cached) return cached;
}

const grouped: { [propertyName: string]: ValidationMetadata[] } = {};
metadata.forEach(metadata => {
if (!grouped[metadata.propertyName]) grouped[metadata.propertyName] = [];
grouped[metadata.propertyName].push(metadata);
});

if (cacheKey) {
this.groupedMetadataCache.set(cacheKey, grouped);
}

return grouped;
}

/**
* Returns pre-partitioned metadata grouped by property name, with each property's
* metadata split by type. Cached for repeated validations of the same class.
*/
getPartitionedMetadata(
groupedMetadatas: Record<string, ValidationMetadata[]>,
cacheKey: string
): PartitionedMetadata {
const cached = this.partitionedMetadataCache.get(cacheKey);
if (cached) return cached;

const result: PartitionedMetadata = {};
for (const propertyName in groupedMetadatas) {
const allMetadatas = groupedMetadatas[propertyName];
const defined: ValidationMetadata[] = [];
const custom: ValidationMetadata[] = [];
const nested: ValidationMetadata[] = [];
const conditional: ValidationMetadata[] = [];
const all: ValidationMetadata[] = [];
let hasPromiseValidation = false;

for (const metadata of allMetadatas) {
if (metadata.type === ValidationTypes.IS_DEFINED) {
defined.push(metadata);
} else if (metadata.type !== ValidationTypes.WHITELIST) {
all.push(metadata);
switch (metadata.type) {
case ValidationTypes.CUSTOM_VALIDATION:
custom.push(metadata);
break;
case ValidationTypes.NESTED_VALIDATION:
nested.push(metadata);
break;
case ValidationTypes.CONDITIONAL_VALIDATION:
conditional.push(metadata);
break;
case ValidationTypes.PROMISE_VALIDATION:
hasPromiseValidation = true;
break;
}
}
}

const customOnly =
defined.length === 0 && nested.length === 0 && conditional.length === 0 && !hasPromiseValidation;
result[propertyName] = { defined, custom, nested, conditional, all, hasPromiseValidation, customOnly };
}

this.partitionedMetadataCache.set(cacheKey, result);
return result;
}

/**
* Gets all validation metadatas for the given object with the given groups.
*/
buildCacheKey(target: Function, schema: string, always: boolean, strictGroups: boolean, groups?: string[]): string {
const targetId = (target as any).__cv_id ?? ((target as any).__cv_id = ++MetadataStorage._nextId);
const groupKey = groups?.length ? groups.slice().sort().join(',') : '';
return `${targetId}|${schema || ''}|${always ? 1 : 0}|${strictGroups ? 1 : 0}|${groupKey}`;
}

getTargetValidationMetadatas(
targetConstructor: Function,
targetSchema: string,
always: boolean,
strictGroups: boolean,
groups?: string[]
groups?: string[],
cacheKey?: string
): ValidationMetadata[] {
const key = cacheKey ?? this.buildCacheKey(targetConstructor, targetSchema, always, strictGroups, groups);
const cached = this.targetMetadataCache.get(key);
if (cached) return cached;

const includeMetadataBecauseOfAlwaysOption = (metadata: ValidationMetadata): boolean => {
// `metadata.always` overrides global default.
if (typeof metadata.always !== 'undefined') return metadata.always;
Expand Down Expand Up @@ -145,7 +244,9 @@ export class MetadataStorage {
});
});

return originalMetadatas.concat(uniqueInheritedMetadatas);
const result = originalMetadatas.concat(uniqueInheritedMetadatas);
this.targetMetadataCache.set(key, result);
return result;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions src/metadata/ValidationMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ export class ValidationMetadata {
*/
validationTypeOptions: any;

/**
* Cached resolved constraint metadatas for this validation.
* Populated on first access to avoid repeated Map lookups.
*/
resolvedConstraints: any[] | undefined = undefined;

/**
* Inline validate function for built-in validators, bypassing constraint metadata dispatch.
*/
inlineValidate?: (value: any, args?: any) => Promise<boolean> | boolean;

/**
* Inline defaultMessage function for built-in validators.
*/
inlineDefaultMessage?: (args?: any) => string;

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down
13 changes: 12 additions & 1 deletion src/register-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,16 @@ export function registerDecorator(options: ValidationDecoratorOptions): void {
constraintCls: constraintCls,
constraints: options.constraints,
};
getMetadataStorage().addValidationMetadata(new ValidationMetadata(validationMetadataArgs));
const validationMetadata = new ValidationMetadata(validationMetadataArgs);

// For inline object validators (all built-in decorators), store the validate/defaultMessage
// functions directly to bypass constraint metadata lookup and wrapper class dispatch.
if (!(options.validator instanceof Function)) {
validationMetadata.inlineValidate = options.validator.validate.bind(options.validator);
if (options.validator.defaultMessage) {
validationMetadata.inlineDefaultMessage = options.validator.defaultMessage.bind(options.validator);
}
}

getMetadataStorage().addValidationMetadata(validationMetadata);
}
Loading