-
-
Notifications
You must be signed in to change notification settings - Fork 288
/
Copy pathAjvService.ts
112 lines (88 loc) · 3.23 KB
/
AjvService.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import {deepClone, getValue, nameOf, prototypeOf, setValue, Type} from "@tsed/core";
import {Constant, Inject, Injectable} from "@tsed/di";
import {getJsonSchema, JsonEntityStore, JsonSchema, JsonSchemaObject} from "@tsed/schema";
import Ajv, {ErrorObject} from "ajv";
import {AjvValidationError} from "../errors/AjvValidationError";
import {AjvErrorObject, ErrorFormatter} from "../interfaces/IAjvSettings";
import {defaultErrorFormatter} from "../utils/defaultErrorFormatter";
import {getPath} from "../utils/getPath";
import "./Ajv";
export interface AjvValidateOptions extends Record<string, any> {
schema?: JsonSchema | Partial<JsonSchemaObject>;
type?: Type<any> | any;
collectionType?: Type<any> | any;
}
@Injectable({
type: "validator:service"
})
export class AjvService {
@Constant("ajv.errorFormatter", defaultErrorFormatter)
protected errorFormatter: ErrorFormatter;
@Inject()
protected ajv: Ajv;
async validate(value: any, options: AjvValidateOptions | JsonSchema): Promise<any> {
let {schema: defaultSchema, type, collectionType, ...additionalOptions} = this.mapOptions(options);
const schema = defaultSchema || getJsonSchema(type, {...additionalOptions, customKeys: true});
if (schema) {
const localValue = deepClone(value);
const validate = this.ajv.compile(schema);
const valid = await validate(localValue);
const {errors} = validate;
if (!valid && errors) {
throw this.mapErrors(errors, {
type,
collectionType,
async: true,
value: localValue
});
}
}
return value;
}
protected mapOptions(options: AjvValidateOptions | JsonSchema): AjvValidateOptions {
if (options instanceof JsonSchema) {
return {
schema: options.toJSON({customKeys: true})
};
}
return options;
}
protected mapErrors(errors: ErrorObject[], options: any) {
const {type, collectionType, value} = options;
const message = errors
.map((error: AjvErrorObject) => {
if (collectionType) {
error.collectionName = nameOf(collectionType);
}
const dataPath = getPath(error);
if (!error.data) {
if (dataPath) {
error.data = getValue(value, dataPath.replace(/^\./, ""));
} else if (error.schemaPath !== "#/required") {
error.data = value;
}
}
if (dataPath && dataPath.match(/pwd|password|mdp|secret/)) {
error.data = "[REDACTED]";
}
if (type) {
error.modelName = nameOf(type);
error.message = this.mapClassError(error, type);
}
return this.errorFormatter.call(this, error, {});
})
.join("\n");
return new AjvValidationError(message, errors);
}
protected mapClassError(error: AjvErrorObject, targetType: Type<any>) {
const propertyKey = getValue(error, "params.missingProperty");
if (propertyKey) {
const store = JsonEntityStore.from<JsonEntityStore>(prototypeOf(targetType), propertyKey);
if (store) {
setValue(error, "params.missingProperty", store.name || propertyKey);
return error.message!.replace(`'${propertyKey}'`, `'${store.name || propertyKey}'`);
}
}
return error.message;
}
}