@xeplr/schema-handler 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/errors.js +7 -2
- package/lib/validate.js +112 -5
- package/package.json +1 -1
package/lib/errors.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
// Structured validation failure. `details` is an array of
|
|
2
|
-
//
|
|
1
|
+
// Structured validation failure. `details` is an array of { field, message }
|
|
2
|
+
// — one entry per problem, keyed by which field it belongs to — so a caller
|
|
3
|
+
// can render field-level UI errors (e.g. feed straight into
|
|
4
|
+
// @xeplr/ui-schema-handler's DynamicForm `errors` prop, which wants
|
|
5
|
+
// { [name]: message }) rather than having to parse a field name back out of
|
|
6
|
+
// a sentence. `message` (from the Error base) stays a single joined string
|
|
7
|
+
// for anything that just wants to log or throw it.
|
|
3
8
|
class ValidationError extends Error {
|
|
4
9
|
constructor(message, details) {
|
|
5
10
|
super(message);
|
package/lib/validate.js
CHANGED
|
@@ -4,9 +4,42 @@ var { ValidationError } = require('./errors');
|
|
|
4
4
|
/**
|
|
5
5
|
* Apply a schema (array of field definitions) to a values object.
|
|
6
6
|
* Returns a copy of `values` with defaults filled in.
|
|
7
|
-
* Throws ValidationError listing every problem.
|
|
7
|
+
* Throws ValidationError listing every problem (details: [{ field, message }]).
|
|
8
8
|
*
|
|
9
|
-
* Field: { name, type, required, default, description, order
|
|
9
|
+
* Field: { name, type, required, default, description, order,
|
|
10
|
+
* group?, // FORM LAYOUT ONLY — ignored here, and deliberately
|
|
11
|
+
* // so. Fields naming the same group are drawn
|
|
12
|
+
* // together in one collapsible section, which is
|
|
13
|
+
* // how a 13-input action stops reading as a tax
|
|
14
|
+
* // return. It says nothing about validity: a
|
|
15
|
+
* // collapsed group's values are submitted and
|
|
16
|
+
* // checked exactly like any other, because "did the
|
|
17
|
+
* // user happen to have this section open" must never
|
|
18
|
+
* // be part of what the server accepts.
|
|
19
|
+
* //
|
|
20
|
+
* // Distinct from `showWhen` (also layout-only, also
|
|
21
|
+
* // ignored here), which decides whether a field is
|
|
22
|
+
* // ASKED AT ALL. Rule of thumb: showWhen for a
|
|
23
|
+
* // branch — picking a template means there is no
|
|
24
|
+
* // subject to write — and group for the tail of
|
|
25
|
+
* // always-legitimate options nobody sets most days.
|
|
26
|
+
* options?, // [value, ...] or [{value, label}, ...] — if
|
|
27
|
+
* // present, the value MUST be one of them. Not
|
|
28
|
+
* // declared separately from a radio/select
|
|
29
|
+
* // widget's own option list — that list IS the
|
|
30
|
+
* // constraint, one source of truth rather than two
|
|
31
|
+
* // that can drift apart.
|
|
32
|
+
* validation? } // per-type constraints, only checked once the
|
|
33
|
+
* // base type already passed `check()`:
|
|
34
|
+
* // string: { minLength, maxLength, pattern }
|
|
35
|
+
* // number: { min, max, integer }
|
|
36
|
+
* // date: { min, max } (ISO strings)
|
|
37
|
+
* // array: { minItems, maxItems, itemType }
|
|
38
|
+
* // object: { fields: [...] } (nested schema,
|
|
39
|
+
* // validated recursively — this is the
|
|
40
|
+
* // "nested sub-forms" DynamicForm's own
|
|
41
|
+
* // comment flags as not built yet; the
|
|
42
|
+
* // data layer doesn't need to wait on it)
|
|
10
43
|
* Values: { [name]: value }
|
|
11
44
|
* Label: free-form string used in error messages, e.g. 'config' | 'input' | 'params'
|
|
12
45
|
*/
|
|
@@ -27,18 +60,92 @@ function applySchema(schema, values, label) {
|
|
|
27
60
|
|
|
28
61
|
if (missing && field.default !== undefined) { out[field.name] = field.default; continue; }
|
|
29
62
|
if (missing) {
|
|
30
|
-
if (field.required) errors.push('Missing required ' + label + ' field: ' + field.name);
|
|
63
|
+
if (field.required) errors.push({ field: field.name, message: 'Missing required ' + label + ' field: ' + field.name });
|
|
31
64
|
continue;
|
|
32
65
|
}
|
|
33
66
|
if (field.type && !check(field.type, val)) {
|
|
34
|
-
errors.push(label + ' field "' + field.name + '" expected type ' + field.type + ', got ' + typeof val);
|
|
67
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" expected type ' + field.type + ', got ' + typeof val });
|
|
35
68
|
continue;
|
|
36
69
|
}
|
|
70
|
+
|
|
71
|
+
if (field.options && field.options.length) {
|
|
72
|
+
var allowed = optionValues(field.options);
|
|
73
|
+
if (allowed.indexOf(val) === -1) {
|
|
74
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be one of: ' + allowed.join(', ') });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (field.type === 'object' && field.validation && Array.isArray(field.validation.fields)) {
|
|
80
|
+
applyNestedObject(field, val, label, errors, out);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
checkConstraints(field, val, label, errors);
|
|
37
85
|
out[field.name] = val;
|
|
38
86
|
}
|
|
39
87
|
|
|
40
|
-
if (errors.length)
|
|
88
|
+
if (errors.length) {
|
|
89
|
+
throw new ValidationError(errors.map(function(e) { return e.message; }).join('; '), errors);
|
|
90
|
+
}
|
|
41
91
|
return out;
|
|
42
92
|
}
|
|
43
93
|
|
|
94
|
+
function optionValues(options) {
|
|
95
|
+
return options.map(function(o) {
|
|
96
|
+
return (o && typeof o === 'object' && Object.prototype.hasOwnProperty.call(o, 'value')) ? o.value : o;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkConstraints(field, val, label, errors) {
|
|
101
|
+
var v = field.validation;
|
|
102
|
+
if (!v) return;
|
|
103
|
+
|
|
104
|
+
if (field.type === 'string') {
|
|
105
|
+
if (v.minLength != null && val.length < v.minLength) {
|
|
106
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be at least ' + v.minLength + ' characters' });
|
|
107
|
+
}
|
|
108
|
+
if (v.maxLength != null && val.length > v.maxLength) {
|
|
109
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be at most ' + v.maxLength + ' characters' });
|
|
110
|
+
}
|
|
111
|
+
if (v.pattern && !(new RegExp(v.pattern)).test(val)) {
|
|
112
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" does not match the required pattern' });
|
|
113
|
+
}
|
|
114
|
+
} else if (field.type === 'number') {
|
|
115
|
+
if (v.min != null && val < v.min) errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be >= ' + v.min });
|
|
116
|
+
if (v.max != null && val > v.max) errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be <= ' + v.max });
|
|
117
|
+
if (v.integer && !Number.isInteger(val)) errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be an integer' });
|
|
118
|
+
} else if (field.type === 'date') {
|
|
119
|
+
var d = val instanceof Date ? val : new Date(val);
|
|
120
|
+
if (v.min && d < new Date(v.min)) errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be on or after ' + v.min });
|
|
121
|
+
if (v.max && d > new Date(v.max)) errors.push({ field: field.name, message: label + ' field "' + field.name + '" must be on or before ' + v.max });
|
|
122
|
+
} else if (field.type === 'array') {
|
|
123
|
+
if (v.minItems != null && val.length < v.minItems) {
|
|
124
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" must have at least ' + v.minItems + ' items' });
|
|
125
|
+
}
|
|
126
|
+
if (v.maxItems != null && val.length > v.maxItems) {
|
|
127
|
+
errors.push({ field: field.name, message: label + ' field "' + field.name + '" must have at most ' + v.maxItems + ' items' });
|
|
128
|
+
}
|
|
129
|
+
if (v.itemType) {
|
|
130
|
+
for (var j = 0; j < val.length; j++) {
|
|
131
|
+
if (!check(v.itemType, val[j])) {
|
|
132
|
+
errors.push({ field: field.name + '[' + j + ']', message: label + ' field "' + field.name + '[' + j + ']" expected type ' + v.itemType });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Recursive: nested errors are re-keyed as "<parent>.<child>" so a flat
|
|
140
|
+
// details array still says exactly which leaf failed.
|
|
141
|
+
function applyNestedObject(field, val, label, errors, out) {
|
|
142
|
+
try {
|
|
143
|
+
out[field.name] = applySchema(field.validation.fields, val, label + '.' + field.name);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
(err.details || []).forEach(function(d) {
|
|
146
|
+
errors.push({ field: field.name + '.' + d.field, message: d.message });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
44
151
|
module.exports = { applySchema: applySchema };
|