/
validation.js
268 lines (246 loc) · 8 KB
/
validation.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
256
257
258
259
260
261
262
263
264
265
266
267
268
import omit from 'lodash/omit';
import { validate } from '@talend/json-schema-form-core';
import { getValue } from './properties';
import shouldValidate from './condition';
import { getArrayElementItems } from './array';
/**
* Adapt merged schema from jsfc with additional rules
* @param mergedSchema The jsfc merged schema
* @returns The adapted merged schema
*/
export function adaptAdditionalRules(mergedSchema) {
// skip enum validation if explicitly not restricted
const { schema } = mergedSchema;
if (mergedSchema.restricted === false) {
if (schema.type === 'array' && (schema.items && schema.items.enum)) {
return {
...mergedSchema,
schema: {
...schema,
items: {
...schema.items,
enum: undefined,
},
},
};
} else if (schema.enum) {
return {
...mergedSchema,
schema: {
...schema,
enum: undefined,
},
};
}
}
return mergedSchema;
}
/**
* Validate a value.
* @param schema The merged schema
* @param value The value
* @param properties The values
* @param customValidationFn A custom validation function
* that is applied on schema.customValidation = true
* @returns {object} The validation result.
*/
export function validateValue(schema, value, properties, customValidationFn) {
const validationSchema = adaptAdditionalRules(schema);
const staticResult = validate(validationSchema, value);
if (staticResult.valid && schema.customValidation && customValidationFn) {
return customValidationFn(schema, value, properties);
}
return staticResult.valid ? null : staticResult.error.message;
}
/**
* Validate an array.
* @param mergedSchema The array schema
* @param value The value
* @param properties All the values
* @param customValidationFn A custom validation function
* that is applied on schema.customValidation = true
* @param deepValidation Validate the array values if set to true
* @returns {object} The validation result.
*/
export function validateArray(mergedSchema, value, properties, customValidationFn, deepValidation) {
const results = {};
const { key } = mergedSchema;
// validate array definition, not its sub-items here
const schemaWithoutItems = {
...mergedSchema,
schema: {
...mergedSchema.schema,
items: [],
},
};
const error = validateValue(schemaWithoutItems, value, properties, customValidationFn);
if (error) {
results[key] = error;
}
// validate each value of the array
if (deepValidation && value) {
for (let valueIndex = 0; valueIndex < value.length; valueIndex += 1) {
// adapt items schema with value index
const indexedItems = getArrayElementItems(mergedSchema, valueIndex);
// eslint-disable-next-line no-use-before-define
const subResults = validateAll(indexedItems, properties, customValidationFn);
Object.assign(results, subResults);
}
}
return results;
}
/**
* Validate a simple value.
* @param mergedSchema The schema to validate
* @param value The value
* @param properties All the values
* @param customValidationFn A custom validation function
* that is applied on schema.customValidation = true
* @param deepValidation Validate subItems if true
* @returns {object} The validation result.
*/
export function validateSimple(
mergedSchema,
value,
properties,
customValidationFn,
deepValidation,
) {
const results = {};
const { key, items } = mergedSchema;
const error = validateValue(mergedSchema, value, properties, customValidationFn);
if (error) {
results[key] = error;
}
if (deepValidation && items) {
// eslint-disable-next-line no-use-before-define
const subResults = validateAll(items, properties, customValidationFn);
Object.assign(results, subResults);
}
return results;
}
/**
* Execute the right validation depending on the schema type.
* @param mergedSchema The merged schema
* @param value The value
* @param properties All the values
* @param customValidationFn A custom validation function
* that is applied on schema.customValidation = true
* @param deepValidation Validate subItems if true
* @returns {Object} The validation result by field.
*/
export function validateSingle(
mergedSchema,
value,
properties,
customValidationFn,
deepValidation,
) {
if (mergedSchema.type === 'array') {
return validateArray(mergedSchema, value, properties, customValidationFn, deepValidation);
}
return validateSimple(mergedSchema, value, properties, customValidationFn, deepValidation);
}
/**
* Validate all values in the schema.
* @param mergedSchema The merged schema array
* @param properties The values
* @param customValidationFn A custom validation function
* that is applied on schema.customValidation = true
* @returns {object} The validation result by field.
*/
export function validateAll(mergedSchema, properties, customValidationFn) {
const results = {};
mergedSchema.forEach(schema => {
const value = getValue(properties, schema);
const subResults = !shouldValidate(schema.condition, properties)
? true
: validateSingle(schema, value, properties, customValidationFn, true); // deep validation
Object.assign(results, subResults);
});
return results;
}
/**
* Check if a schema value is valid.
* It is invalid if :
* - the schema is an invalid field (errors[key] is falsy)
* - the schema has items (ex: fieldset, tabs, ...), and at least one of them is invalid
* @param schema The schema
* @param errors The errors
* @returns {boolean} true if it is invalid, false otherwise.
*/
export function isValid(schema, errors) {
const { key, items } = schema;
if (key && errors[key]) {
return false;
}
if (items) {
for (const itemSchema of items) {
if (!isValid(itemSchema, errors)) {
return false;
}
}
}
return true;
}
/**
* Filter the errors on array which items indexes are between a range
* This returns only the errors keys.
* @param errors The errors map
* @param arrayKey The array key
* @param minIndex The min item index (INCLUDED)
* @param maxIndex The max item index (EXCLUDED)
*/
export function filterArrayErrorsKeys(errors, arrayKey, minIndex, maxIndex) {
const minArrayIndexKey = Number.isInteger(minIndex) && arrayKey.concat(minIndex).toString();
const maxArrayIndexKey = Number.isInteger(maxIndex) && arrayKey.concat(maxIndex).toString();
return Object.keys(errors).filter(
errorKey =>
errorKey.startsWith(arrayKey) && // is on target array
(!minArrayIndexKey || errorKey >= minArrayIndexKey) && // is after min
(!maxArrayIndexKey || errorKey < maxArrayIndexKey), // is before max
);
}
/**
* Given an error map:
* Remove errors on array items if shouldRemoveIndex(index) is true
* Shift the index of array items, where new index is getNextIndex(index)
* @param oldErrors The errorMap
* @param arrayKey The array key
* @param minIndex The first index to manipulate
* @param maxIndex The last (EXCLUDED) index to manipulate
* @param shouldRemoveIndex Predicate to determine if this item errors should be removed
* @param getNextIndex New index provider
*/
export function shiftArrayErrorsKeys(
oldErrors,
{ arrayKey, minIndex, maxIndex, shouldRemoveIndex, getNextIndex },
) {
// extract the errors included between the range
const arrayErrorsToShiftOrRemove = filterArrayErrorsKeys(oldErrors, arrayKey, minIndex, maxIndex);
// get all errors except those to remove or shift
const errors = omit(oldErrors, arrayErrorsToShiftOrRemove);
const indexPositionInKey = arrayKey.length;
arrayErrorsToShiftOrRemove
.map(errorKey => errorKey.split(','))
// filter the index we want to remove (shouldRemoveIndex)
.filter(errorKey => {
if (!shouldRemoveIndex) {
return true;
}
const itemIndex = Number(errorKey[indexPositionInKey]);
return !shouldRemoveIndex(itemIndex);
})
// shift the item index (getNextIndex)
.map(oldErrorKey => {
const oldIndex = Number(oldErrorKey[indexPositionInKey]);
const newErrorKey = oldErrorKey.slice(0);
newErrorKey[indexPositionInKey] = getNextIndex(oldIndex);
return [oldErrorKey, newErrorKey];
})
// populate the final error map
.forEach(([oldErrorKey, newErrorKey]) => {
errors[newErrorKey] = oldErrors[oldErrorKey];
});
return errors;
}