@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 CHANGED
@@ -1,5 +1,10 @@
1
- // Structured validation failure. `details` is an array of per-field messages
2
- // so the caller can render field-level UI errors, not just one blob.
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) throw new ValidationError(errors.join('; '), errors);
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/schema-handler",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Field-schema primitive: define, validate, infer, map, and template values across schemas",
5
5
  "main": "index.js",
6
6
  "files": [