-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy pathvalidate.js
255 lines (229 loc) · 6.86 KB
/
validate.js
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import toPath from "lodash.topath";
import Ajv from "ajv";
let ajv = createAjvInstance();
import { deepEquals } from "./utils";
let formerMetaSchema = null;
import { isObject, mergeObjects } from "./utils";
function createAjvInstance() {
const ajv = new Ajv({
errorDataPath: "property",
allErrors: true,
multipleOfPrecision: 8,
schemaId: "auto",
});
// add custom formats
ajv.addFormat(
"data-url",
/^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/
);
ajv.addFormat(
"color",
/^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/
);
return ajv;
}
function toErrorSchema(errors) {
// Transforms a ajv validation errors list:
// [
// {property: ".level1.level2[2].level3", message: "err a"},
// {property: ".level1.level2[2].level3", message: "err b"},
// {property: ".level1.level2[4].level3", message: "err b"},
// ]
// Into an error tree:
// {
// level1: {
// level2: {
// 2: {level3: {errors: ["err a", "err b"]}},
// 4: {level3: {errors: ["err b"]}},
// }
// }
// };
if (!errors.length) {
return {};
}
return errors.reduce((errorSchema, error) => {
const { property, message } = error;
const path = toPath(property);
let parent = errorSchema;
// If the property is at the root (.level1) then toPath creates
// an empty array element at the first index. Remove it.
if (path.length > 0 && path[0] === "") {
path.splice(0, 1);
}
for (const segment of path.slice(0)) {
if (!(segment in parent)) {
parent[segment] = {};
}
parent = parent[segment];
}
if (Array.isArray(parent.__errors)) {
// We store the list of errors for this node in a property named __errors
// to avoid name collision with a possible sub schema field named
// "errors" (see `validate.createErrorHandler`).
parent.__errors = parent.__errors.concat(message);
} else {
if (message) {
parent.__errors = [message];
}
}
return errorSchema;
}, {});
}
export function toErrorList(errorSchema, fieldName = "root") {
// XXX: We should transform fieldName as a full field path string.
let errorList = [];
if ("__errors" in errorSchema) {
errorList = errorList.concat(
errorSchema.__errors.map(stack => {
return {
stack: `${fieldName}: ${stack}`,
};
})
);
}
return Object.keys(errorSchema).reduce((acc, key) => {
if (key !== "__errors") {
acc = acc.concat(toErrorList(errorSchema[key], key));
}
return acc;
}, errorList);
}
function createErrorHandler(formData) {
const handler = {
// We store the list of errors for this node in a property named __errors
// to avoid name collision with a possible sub schema field named
// "errors" (see `utils.toErrorSchema`).
__errors: [],
addError(message) {
this.__errors.push(message);
},
};
if (isObject(formData)) {
return Object.keys(formData).reduce((acc, key) => {
return { ...acc, [key]: createErrorHandler(formData[key]) };
}, handler);
}
if (Array.isArray(formData)) {
return formData.reduce((acc, value, key) => {
return { ...acc, [key]: createErrorHandler(value) };
}, handler);
}
return handler;
}
function unwrapErrorHandler(errorHandler) {
return Object.keys(errorHandler).reduce((acc, key) => {
if (key === "addError") {
return acc;
} else if (key === "__errors") {
return { ...acc, [key]: errorHandler[key] };
}
return { ...acc, [key]: unwrapErrorHandler(errorHandler[key]) };
}, {});
}
/**
* Transforming the error output from ajv to format used by jsonschema.
* At some point, components should be updated to support ajv.
*/
function transformAjvErrors(errors = []) {
if (errors === null) {
return [];
}
return errors.map(e => {
const { dataPath, keyword, message, params, schemaPath } = e;
let property = `${dataPath}`;
// put data in expected format
return {
name: keyword,
property,
message,
params, // specific to ajv
stack: `${property} ${message}`.trim(),
schemaPath,
};
});
}
/**
* This function processes the formData with a user `validate` contributed
* function, which receives the form data and an `errorHandler` object that
* will be used to add custom validation errors for each field.
*/
export default function validateFormData(
formData,
schema,
customValidate,
transformErrors,
additionalMetaSchemas = []
) {
// add more schemas to validate against
if (
additionalMetaSchemas &&
!deepEquals(formerMetaSchema, additionalMetaSchemas) &&
Array.isArray(additionalMetaSchemas)
) {
ajv = createAjvInstance();
ajv.addMetaSchema(additionalMetaSchemas);
formerMetaSchema = additionalMetaSchemas;
}
let validationError = null;
try {
ajv.validate(schema, formData);
} catch (err) {
validationError = err;
}
let errors = transformAjvErrors(ajv.errors);
// Clear errors to prevent persistent errors, see #1104
ajv.errors = null;
const noProperMetaSchema =
validationError &&
validationError.message &&
typeof validationError.message === "string" &&
validationError.message.includes("no schema with key or ref ");
if (noProperMetaSchema) {
errors = [
...errors,
{
stack: validationError.message,
},
];
}
if (typeof transformErrors === "function") {
errors = transformErrors(errors);
}
let errorSchema = toErrorSchema(errors);
if (noProperMetaSchema) {
errorSchema = {
...errorSchema,
...{
$schema: {
__errors: [validationError.message],
},
},
};
}
if (typeof customValidate !== "function") {
return { errors, errorSchema };
}
const errorHandler = customValidate(formData, createErrorHandler(formData));
const userErrorSchema = unwrapErrorHandler(errorHandler);
const newErrorSchema = mergeObjects(errorSchema, userErrorSchema, true);
// XXX: The errors list produced is not fully compliant with the format
// exposed by the jsonschema lib, which contains full field paths and other
// properties.
const newErrors = toErrorList(newErrorSchema);
return {
errors: newErrors,
errorSchema: newErrorSchema,
};
}
/**
* Validates data against a schema, returning true if the data is valid, or
* false otherwise. If the schema is invalid, then this function will return
* false.
*/
export function isValid(schema, data) {
try {
return ajv.validate(schema, data);
} catch (e) {
return false;
}
}