packages/core/src/entity/EntityValidator.ts
import type { Dictionary, EntityData, EntityMetadata, EntityProperty, FilterQuery } from '../typings';
import { ReferenceKind } from '../enums';
import { Utils } from '../utils/Utils';
import { ValidationError } from '../errors';
import { helper } from './wrap';
export class EntityValidator {
KNOWN_TYPES = new Set(['string', 'number', 'boolean', 'bigint', 'Uint8Array', 'Date', 'Buffer', 'RegExp']);
constructor(private strict: boolean) { }
validate<T extends object>(entity: T, payload: any, meta: EntityMetadata<T>): void {
meta.props.forEach(prop => {
if (prop.inherited) {
return;
}
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
this.validateCollection(entity, prop);
}
const SCALAR_TYPES = ['string', 'number', 'boolean', 'Date'];
if (prop.kind !== ReferenceKind.SCALAR || !SCALAR_TYPES.includes(prop.type)) {
return;
}
const newValue = this.validateProperty(prop, this.getValue(payload, prop), entity);
if (this.getValue(payload, prop) === newValue) {
return;
}
this.setValue(payload, prop, newValue);
/* istanbul ignore else */
if (entity[prop.name]) {
entity[prop.name] = payload[prop.name];
}
});
}
validateRequired<T extends object>(entity: T): void {
const wrapped = helper(entity);
for (const prop of wrapped.__meta.props) {
if (
!prop.nullable &&
!prop.autoincrement &&
!prop.default &&
!prop.defaultRaw &&
!prop.onCreate &&
!prop.generated &&
!prop.embedded &&
![ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) &&
prop.name !== wrapped.__meta.root.discriminatorColumn &&
prop.type.toLowerCase() !== 'objectid' &&
prop.persist !== false &&
entity[prop.name] == null
) {
throw ValidationError.propertyRequired(entity, prop);
}
}
}
validateProperty<T extends object>(prop: EntityProperty, givenValue: any, entity: T) {
if (givenValue === null || givenValue === undefined) {
return givenValue;
}
const expectedType = prop.runtimeType;
let givenType = Utils.getObjectType(givenValue);
let ret = givenValue;
if (!this.strict) {
ret = this.fixTypes(expectedType, givenType, givenValue);
givenType = Utils.getObjectType(ret);
}
if (prop.enum && prop.items) {
if (!prop.items.some(it => it === givenValue)) {
throw ValidationError.fromWrongPropertyType(entity, prop.name, expectedType, givenType, givenValue);
}
} else {
if (givenType !== expectedType && this.KNOWN_TYPES.has(expectedType)) {
throw ValidationError.fromWrongPropertyType(entity, prop.name, expectedType, givenType, givenValue);
}
}
return ret;
}
validateParams(params: any, type = 'search condition', field?: string): void {
if (Utils.isPrimaryKey(params) || Utils.isEntity(params)) {
return;
}
if (Array.isArray(params)) {
return (params as unknown[]).forEach(item => this.validateParams(item, type, field));
}
if (Utils.isPlainObject(params)) {
Object.keys(params).forEach(k => {
this.validateParams(params[k], type, k);
});
}
}
validatePrimaryKey<T>(entity: EntityData<T>, meta: EntityMetadata<T>): void {
const pkExists = meta.primaryKeys.every(pk => entity[pk] != null) || entity[meta.serializedPrimaryKey] != null;
if (!entity || !pkExists) {
throw ValidationError.fromMergeWithoutPK(meta);
}
}
validateEmptyWhere<T>(where: FilterQuery<T>): void {
if (Utils.isEmpty(where)) {
throw new Error(`You cannot call 'EntityManager.findOne()' with empty 'where' parameter`);
}
}
private getValue(o: Dictionary, prop: EntityProperty) {
if (prop.embedded && prop.embedded[0] in o) {
return o[prop.embedded[0]]?.[prop.embedded[1]];
}
return o[prop.name];
}
private setValue(o: Dictionary, prop: EntityProperty, v: any) {
/* istanbul ignore next */
if (prop.embedded && prop.embedded[0] in o) {
return o[prop.embedded[0]][prop.embedded[1]] = v;
}
o[prop.name] = v;
}
private validateCollection<T extends object>(entity: T, prop: EntityProperty): void {
if (prop.hydrate !== false && helper(entity).__initialized && !entity[prop.name as keyof T]) {
throw ValidationError.fromCollectionNotInitialized(entity, prop);
}
}
private fixTypes(expectedType: string, givenType: string, givenValue: any): any {
if (expectedType === 'Date' && ['string', 'number'].includes(givenType)) {
givenValue = this.fixDateType(givenValue);
}
if (expectedType === 'number' && givenType === 'string') {
givenValue = this.fixNumberType(givenValue);
}
if (expectedType === 'boolean' && givenType === 'number') {
givenValue = this.fixBooleanType(givenValue);
}
return givenValue;
}
private fixDateType(givenValue: string): Date | string {
let date: Date;
if (Utils.isString(givenValue) && givenValue.match(/^-?\d+(\.\d+)?$/)) {
date = new Date(+givenValue);
} else {
date = new Date(givenValue);
}
return date.toString() !== 'Invalid Date' ? date : givenValue;
}
private fixNumberType(givenValue: string): number | string {
const num = +givenValue;
return '' + num === givenValue ? num : givenValue;
}
private fixBooleanType(givenValue: number): boolean | number {
const bool = !!givenValue;
return +bool === givenValue ? bool : givenValue;
}
}