Entity(Class) level validator based on class-validator interfaces
npm install @astahmer/entity-validator --save
This library doesn't provide any validator, decorator or entities. It only provides an easy way to make custom validators/decorators that will fit your needs.
const validator = new EntityValidator();
const errors = validator.execute(entity, options);
The execute
function always returns an array of errors, can be empty.
The options parameter is an object that accepts a groups to pass custom validation groups & a context key that can be literally anything.
The context key should be used to pass dynamic data at the moment of validation.
Learn about typescript decorators here.
Registering an entity-validator decorator is done by passing a config to registerEntityDecorator(args: RegisterEntityDecoratorArgs)
.
RegisterEntityDecoratorArgs
shares most keys as EntityValidatorConfig
.
You can make both Class & Property decorators using function overloads.
registerEntityDecorator({
name: "IsUnique",
target,
options,
validator: new IsUniqueValidator(),
data: { fields },
defaultMessage: "Entity is not unique",
});
-
The option
data
can be anything and is meant to be used to pass decorator arguments to validator. -
The option
always
is set to true when nogroups
(undefined | groups.length === 0) are passed &always
is not explicitly false. That is mostly useful for validation with default groups per request. (ex: groups: ["user", "user_create"] on POST /api/users/) -
If you need to retrieve all metadata attached to an entity, you can use the
getEntityValidatorMetadata(entity)
function. -
Metadatas are stored using the reflect-metadata library under an unique (using a Symbol) key
VALIDATION_METAKEY
.
Create your class and put some validation decorators on the properties you want to validate:
Complete code example available here
import { EntityValidator, EntityValidatorFunctionOptions } from "@astahmer/entity-validator";
import { IsUnique } from "./validators/IsUnique";
@IsUnique<User>(["firstName", "lastName"])
export class User {
id: number;
firstName: string;
lastName: string;
}
const validator = new EntityValidator();
const store: User[] = [];
// User 1 should pass
const user = new User();
user.id = 1;
user.firstName = "Alexandre";
user.lastName = "Stahmer";
store.push(user);
const emptyErrors = await validator.execute(user, { context: { store } });
console.log(emptyErrors); // []
// User 2 should fail
const user2 = new User();
user.id = 2;
user2.firstName = "Alexandre";
user2.lastName = "Stahmer";
const errors = await validator.execute(user2, { context: { store } });
console.log(errors);
/* errors = [
{
constraints: {
IsUnique: 'Another <User> entity already exists with unique constraints on : <firstName, lastName>'
},
children: [],
property: 'firstName, lastName'
}
];
*/
TypeORM Entities
Complete code example available here or with typeorm
import { IsUnique } from "./validators/IsUnique";
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
export abstract class AbstractEntity {
@PrimaryGeneratedColumn()
id: number;
}
@IsUnique<User>(["firstName", "lastName"])
@Entity()
export class User extends AbstractEntity {
@Column()
firstName: string;
@Column()
lastName: string;
}
@Entity()
export class Category extends AbstractEntity {
@Column()
name: string;
}
@IsUnique<Article>(["title", "category"], { message: "Title should be unique among a category" })
@Entity()
export class Article extends AbstractEntity {
@Column()
title: string;
@ManyToOne(() => Category)
category: Category;
}
Complete code example available here or with typeorm
A simple implementation of aIsUniqueValidator
could look like this :
class IsUniqueValidator<T extends AbstractEntity> implements EntityValidatorConstraintInterface<T> {
async validate(item: T, args: EntityValidationArguments<T, IsUniqueData<T>>) {
const fields = args.data.fields;
const store = args.context?.store as T[];
if (!store) return;
const entity = store.find((entity) => fields.every((field) => entity[field] === item[field]));
return !entity;
}
}
That new validator would then be registered by passing a config to registerEntityDecorator
export function IsUnique(
fieldsOrOptions: string[] | EntityValidatorOptions,
options?: EntityValidatorOptions
): PropertyDecorator | ClassDecorator {
return (target, propName: string) => {
// If propName is defined => PropertyDecorator, else it's a ClassDecorator
const isPropDecorator = !!propName;
target = isPropDecorator ? target.constructor : target;
options = isPropDecorator ? (fieldsOrOptions as EntityValidatorOptions) : options;
const fields = isPropDecorator ? [propName] : fieldsOrOptions;
const className = (target as any)?.name;
const defaultProperty = isPropDecorator ? propName : fields.join(", ");
const defaultMessage = `Another <${className}> entity already exists with unique constraints on : <${defaultProperty}>`;
const property = options?.property || defaultProperty;
registerEntityDecorator({
name: "IsUnique",
target,
options,
validator: new IsUniqueValidator(),
data: { fields },
defaultMessage,
property,
});
};
}
As container service (with type-di)
import Container from "typedi";
import { EntityValidator, EntityValidatorFunctionOptions } from "@astahmer/entity-validator";
import { AbstractEntity } from "@/entity/AbstractEntity";
/** Call EntityValidator.execute on entity with given options */
export async function validateEntity<T extends AbstractEntity>(entity: T, options?: EntityValidatorFunctionOptions) {
const validator = Container.get(EntityValidator);
return validator.execute(entity, options);
}