joi 8.4.2 → 9.0.0-0

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/README.md CHANGED
@@ -95,4 +95,4 @@ When validating a schema:
95
95
  * Rules are defined in an additive fashion and evaluated in order after whitelist and blacklist checks.
96
96
 
97
97
  # API
98
- See the [API Reference](https://github.com/hapijs/joi/blob/v8.4.2/API.md).
98
+ See the [API Reference](https://github.com/hapijs/joi/blob/v9.0.0-0/API.md).
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // Load modules
4
+
5
+ const Joi = require('../');
6
+
7
+ // Delcare internals
8
+
9
+ const internals = {};
10
+
11
+
12
+ // This is an example of a survey to obtain the reputation of Parisians
13
+ // It contains examples of how to conditionally require keys based on values of other keys
14
+
15
+ // This is a valid value for integer rating 1 - 5
16
+ const intRating = Joi.number().integer().min(1).max(5);
17
+
18
+ const schema = Joi.object().keys({
19
+ // Do you know any French people? yes or no (required)
20
+ q1: Joi.boolean().required(),
21
+ // Do you know any Parisians? yes or no (required if answered yes in q1)
22
+ q2: Joi.boolean()
23
+ .when('q1', { is: true, then: Joi.required() }),
24
+ // How many french in paris do you know? 1-6, 6-10, 11-50 or 50+ (required if answered yes in q2)
25
+ q3: Joi.string()
26
+ .when('q2', { is: true, then: Joi.valid('1-5', '6-10', '11-50', '50+').required() }),
27
+ // Rate 20% of most friendly Parisians, from how many people you know answered in q3, individually on 1-5 rating
28
+ q4: Joi.array()
29
+ .when('q3', {is: '1-5', then: Joi.array().min(0).max(1).items(intRating).required() })
30
+ .when('q3', {is: '6-10', then: Joi.array().min(1).max(2).items(intRating).required() })
31
+ .when('q3', {is: '11-50', then: Joi.array().min(2).max(10).items(intRating).required() })
32
+ .when('q3', {is: '50+', then: Joi.array().min(10).items(intRating).required() }),
33
+ // Rate remaining 80% of Parisians, from how many people you know answered in q3, individually on 1-5 rating
34
+ q5: Joi.array()
35
+ .when('q3', {is: '1-5', then: Joi.array().min(1).max(4).items(intRating).required() })
36
+ .when('q3', {is: '6-10', then: Joi.array().min(4).max(8).items(intRating).required() })
37
+ .when('q3', {is: '11-50', then: Joi.array().min(8).max(40).items(intRating).required() })
38
+ .when('q3', {is: '50+', then: Joi.array().min(40).items(intRating).required().required() }),
39
+ // Rate the reputation of Parisians in general, 1-5 rating
40
+ q6: intRating.required()
41
+ });
42
+
43
+ const response = {
44
+ q1: true,
45
+ q2: true,
46
+ q3: '1-5',
47
+ q4: [5],
48
+ q5: [1],
49
+ q6: 2
50
+ };
51
+
52
+ Joi.assert(response, schema);
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // Load modules
4
+
5
+ const Joi = require('../');
6
+
7
+
8
+ // Declare internals
9
+
10
+ const internals = {};
11
+
12
+
13
+ const schema = Joi.object().options({ abortEarly: false }).keys({
14
+ email: Joi.string().email().required().label('User Email'),
15
+ password: Joi.string().min(8).required(),
16
+ password_confirmation: Joi.any().valid(Joi.ref('password')).required().options({ language: { any: { allowOnly: 'must match password' }, label: 'Password Confirmation' } }).label('This label is not used because language.label takes precedence'),
17
+ first_name: Joi.string().required(),
18
+ last_name: Joi.string().required(),
19
+ company: Joi.string().optional()
20
+ });
21
+
22
+
23
+ const data = {
24
+ email: 'not_a_valid_email_to_show_custom_label',
25
+ password: 'abcd1234',
26
+ password_confirmation: 'abc1',
27
+ first_name: 'Joe',
28
+ last_name: 'Doe'
29
+ };
30
+
31
+ Joi.assert(data, schema);
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ // Load modules
4
+
5
+ const Joi = require('../');
6
+
7
+
8
+ // Declare internals
9
+
10
+ const internals = {};
11
+
12
+
13
+ const schema = {
14
+ type: Joi.string().required(),
15
+ subtype: Joi.alternatives()
16
+ .when('type', {is: 'video', then: Joi.valid('mp4', 'wav')})
17
+ .when('type', {is: 'audio', then: Joi.valid('mp3')})
18
+ .when('type', {is: 'image', then: Joi.valid('jpg', 'png')})
19
+ .when('type', {is: 'pdf' , then: Joi.valid('document')})
20
+ };
21
+
22
+
23
+ Joi.assert({ type: 'video', subtype: 'mp4' }, schema); // Pass
24
+ Joi.assert({ type: 'video', subtype: 'wav' }, schema); // Pass
25
+ Joi.assert({ type: 'other', subtype: 'something' }, schema); // Fail
26
+ Joi.assert({ type: 'audio', subtype: 'mp4' }, schema); // Fail
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ // Load modules
4
+ const Joi = require('../');
5
+
6
+ const now = new Date();
7
+ const javascriptTimestamp = now.getTime();
8
+ const unixTimestamp = now.getTime() / 1000;
9
+
10
+ const schema = Joi.object().options({ abortEarly: false }).keys({
11
+ javascript1: Joi.date().timestamp(),
12
+ javascript2: Joi.date().timestamp('javascript'),
13
+ unix: Joi.date().timestamp('unix')
14
+ });
15
+
16
+ const data = {
17
+ javascript1: javascriptTimestamp,
18
+ javascript2: javascriptTimestamp,
19
+ unix: unixTimestamp
20
+ };
21
+
22
+ Joi.assert(data, schema);
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // Load modules
4
+
5
+ const Toc = require('markdown-toc');
6
+ const Fs = require('fs');
7
+ const Package = require('./package.json');
8
+
9
+ // Declare internals
10
+
11
+ const internals = {
12
+ api: {
13
+ filename: './API.md',
14
+ contents: Fs.readFileSync('./API.md', 'utf8')
15
+ },
16
+ readme: {
17
+ filename: './README.md',
18
+ contents: Fs.readFileSync('./README.md', 'utf8')
19
+ }
20
+ };
21
+
22
+ internals.generateToc = function () {
23
+
24
+ const tocOptions = {
25
+ bullets: '-',
26
+ slugify: function (text) {
27
+
28
+ return text.toLowerCase()
29
+ .replace(/\s/g, '-')
30
+ .replace(/[^\w-]/g, '');
31
+ }
32
+ };
33
+
34
+ const api = Toc.insert(internals.api.contents, tocOptions)
35
+ .replace(/<!-- version -->(.|\n)*<!-- versionstop -->/, `<!-- version -->\n# ${Package.version} API Reference\n<!-- versionstop -->`);
36
+
37
+ Fs.writeFileSync(internals.api.filename, api);
38
+ };
39
+
40
+ internals.generateLink = function () {
41
+ // create absolute URL for versioned docs
42
+ const readme = internals.readme.contents
43
+ .replace(/\[API Reference\]\(.*\)/gi, `[API Reference](${Package.homepage || ''}/blob/v${Package.version}/${internals.api.filename.substr(2)})`);
44
+
45
+ Fs.writeFileSync(internals.readme.filename, readme);
46
+ };
47
+
48
+ internals.generateToc();
49
+ internals.generateLink();
package/images/joi.png ADDED
Binary file
Binary file
package/lib/any.js CHANGED
@@ -32,10 +32,37 @@ internals.defaults = {
32
32
 
33
33
  internals.checkOptions = function (options) {
34
34
 
35
- const Schemas = require('./schemas');
36
- const result = Schemas.options.validate(options);
37
- if (result.error) {
38
- throw new Error(result.error.details[0].message);
35
+ const optionType = {
36
+ abortEarly: 'boolean',
37
+ convert: 'boolean',
38
+ allowUnknown: 'boolean',
39
+ skipFunctions: 'boolean',
40
+ stripUnknown: 'boolean',
41
+ language: 'object',
42
+ presence: ['string', 'required', 'optional', 'forbidden', 'ignore'],
43
+ raw: 'boolean',
44
+ context: 'object',
45
+ strip: 'boolean',
46
+ noDefaults: 'boolean'
47
+ };
48
+
49
+ const keys = Object.keys(options);
50
+ for (let i = 0; i < keys.length; ++i) {
51
+ const key = keys[i];
52
+ const opt = optionType[key];
53
+ let type = opt;
54
+ let values = null;
55
+
56
+ if (Array.isArray(opt)) {
57
+ type = opt[0];
58
+ values = opt.slice(1);
59
+ }
60
+
61
+ Hoek.assert(type, 'unknown key ' + key);
62
+ Hoek.assert(typeof options[key] === type, key + ' should be of type ' + type);
63
+ if (values) {
64
+ Hoek.assert(values.indexOf(options[key]) >= 0, key + ' should be one of ' + values.join(', '));
65
+ }
39
66
  }
40
67
  };
41
68
 
@@ -51,8 +78,7 @@ module.exports = internals.Any = function () {
51
78
  this._invalids = new internals.Set();
52
79
  this._tests = [];
53
80
  this._refs = [];
54
- this._flags = {
55
- /*
81
+ this._flags = { /*
56
82
  presence: 'optional', // optional, required, forbidden, ignore
57
83
  allowOnly: false,
58
84
  allowUnknown: undefined,
@@ -64,8 +90,7 @@ module.exports = internals.Any = function () {
64
90
  case: undefined, // upper, lower
65
91
  empty: undefined,
66
92
  func: false
67
- */
68
- };
93
+ */ };
69
94
 
70
95
  this._description = null;
71
96
  this._unit = null;
@@ -192,10 +217,10 @@ internals.Any.prototype.concat = function (schema) {
192
217
  };
193
218
 
194
219
 
195
- internals.Any.prototype._test = function (name, arg, func) {
220
+ internals.Any.prototype._test = function (name, arg, func, options) {
196
221
 
197
222
  const obj = this.clone();
198
- obj._tests.push({ func, name, arg });
223
+ obj._tests.push({ func, name, arg, options });
199
224
  return obj;
200
225
  };
201
226
 
@@ -229,17 +254,6 @@ internals.Any.prototype.raw = function (isRaw) {
229
254
  };
230
255
 
231
256
 
232
- internals.Any.prototype.error = function (err) {
233
-
234
- Hoek.assert(err && err instanceof Error, 'Must provide a valid Error object');
235
-
236
- const obj = this.clone();
237
- obj._settings = obj._settings || {};
238
- obj._settings.error = err;
239
- return obj;
240
- };
241
-
242
-
243
257
  internals.Any.prototype._allow = function () {
244
258
 
245
259
  const values = Hoek.flatten(Array.prototype.slice.call(arguments));
@@ -378,7 +392,7 @@ internals.Any.prototype.when = function (ref, options) {
378
392
  Alternatives = Alternatives || require('./alternatives');
379
393
  const obj = Alternatives.when(ref, { is: options.is, then, otherwise });
380
394
  obj._flags.presence = 'ignore';
381
- obj._settings = internals.concatSettings(obj._settings, { baseType: this });
395
+ obj._settings = internals.concatSettings({ baseType: this }, obj._settings);
382
396
 
383
397
  return obj;
384
398
  };
@@ -609,13 +623,16 @@ internals.Any.prototype._validate = function (value, state, options, reference)
609
623
 
610
624
  for (let i = 0; i < this._tests.length; ++i) {
611
625
  const test = this._tests[i];
612
- const err = test.func.call(this, value, state, options);
613
- if (err) {
614
- errors.push(err);
626
+ const ret = test.func.call(this, value, state, options);
627
+ if (ret && ret.isJoi) {
628
+ errors.push(ret);
615
629
  if (options.abortEarly) {
616
630
  return finish();
617
631
  }
618
632
  }
633
+ else if (ret !== null) {
634
+ value = ret;
635
+ }
619
636
  }
620
637
 
621
638
  return finish();
@@ -640,13 +657,16 @@ internals.Any.prototype._validateWithOptions = function (value, options, callbac
640
657
  };
641
658
 
642
659
 
643
- internals.Any.prototype.validate = function (value, options, callback) {
660
+ internals.Any.prototype.validate = function (value, callback) {
661
+
662
+ const result = this._validate(value, null, internals.defaults);
663
+ const errors = Errors.process(result.errors, value);
644
664
 
645
- if (typeof options === 'function') {
646
- return this._validateWithOptions(value, null, options);
665
+ if (callback) {
666
+ return callback(errors, result.value);
647
667
  }
648
668
 
649
- return this._validateWithOptions(value, options, callback);
669
+ return { error: errors, value: result.value };
650
670
  };
651
671
 
652
672
 
@@ -658,7 +678,7 @@ internals.Any.prototype.describe = function () {
658
678
 
659
679
  const flags = Object.keys(this._flags);
660
680
  if (flags.length) {
661
- if (this._flags.hasOwnProperty('empty') || this._flags.hasOwnProperty('default') || this._flags.lazy) {
681
+ if (this._flags.hasOwnProperty('empty') || this._flags.hasOwnProperty('default')) {
662
682
  description.flags = {};
663
683
  for (let i = 0; i < flags.length; ++i) {
664
684
  const flag = flags[i];
@@ -676,9 +696,6 @@ internals.Any.prototype.describe = function () {
676
696
  description.flags[flag] = this._flags[flag];
677
697
  }
678
698
  }
679
- else if (flag === 'lazy') {
680
- // We don't want it in the description
681
- }
682
699
  else {
683
700
  description.flags[flag] = this._flags[flag];
684
701
  }
@@ -734,9 +751,31 @@ internals.Any.prototype.describe = function () {
734
751
  for (let i = 0; i < this._tests.length; ++i) {
735
752
  const validator = this._tests[i];
736
753
  const item = { name: validator.name };
754
+
737
755
  if (validator.arg !== void 0) {
738
756
  item.arg = Ref.isRef(validator.arg) ? validator.arg.toString() : validator.arg;
739
757
  }
758
+
759
+ const options = validator.options;
760
+ if (options) {
761
+ if (options.hasRef) {
762
+ item.arg = {};
763
+ const keys = Object.keys(validator.arg);
764
+ for (let j = 0; j < keys.length; ++j) {
765
+ const key = keys[j];
766
+ const value = validator.arg[key];
767
+ item.arg[key] = Ref.isRef(value) ? value.toString() : value;
768
+ }
769
+ }
770
+
771
+ if (typeof options.description === 'string') {
772
+ item.description = options.description;
773
+ }
774
+ else if (typeof options.description === 'function') {
775
+ item.description = options.description(item.arg);
776
+ }
777
+ }
778
+
740
779
  description.rules.push(item);
741
780
  }
742
781
 
@@ -752,7 +791,6 @@ internals.Any.prototype.describe = function () {
752
791
  return description;
753
792
  };
754
793
 
755
-
756
794
  internals.Any.prototype.label = function (name) {
757
795
 
758
796
  Hoek.assert(name && typeof name === 'string', 'Label name must be a non-empty string');
@@ -760,7 +798,8 @@ internals.Any.prototype.label = function (name) {
760
798
  const obj = this.clone();
761
799
  const options = { language: { label: name } };
762
800
 
763
- obj._settings = internals.concatSettings(obj._settings, options);
801
+ // If language.label is set, it should override this label
802
+ obj._settings = internals.concatSettings(options, obj._settings);
764
803
  return obj;
765
804
  };
766
805
 
package/lib/array.js CHANGED
@@ -38,17 +38,6 @@ internals.Array = function () {
38
38
  Hoek.inherits(internals.Array, Any);
39
39
 
40
40
 
41
- internals.safeParse = function (value, result) {
42
-
43
- try {
44
- const converted = JSON.parse(value);
45
- if (Array.isArray(converted)) {
46
- result.value = converted;
47
- }
48
- }
49
- catch (e) { }
50
- };
51
-
52
41
  internals.Array.prototype._base = function (value, state, options) {
53
42
 
54
43
  const result = {
@@ -58,7 +47,13 @@ internals.Array.prototype._base = function (value, state, options) {
58
47
  if (typeof value === 'string' &&
59
48
  options.convert) {
60
49
 
61
- internals.safeParse(value, result);
50
+ try {
51
+ const converted = JSON.parse(value);
52
+ if (Array.isArray(converted)) {
53
+ result.value = converted;
54
+ }
55
+ }
56
+ catch (e) { }
62
57
  }
63
58
 
64
59
  let isArray = Array.isArray(result.value);
@@ -75,8 +70,6 @@ internals.Array.prototype._base = function (value, state, options) {
75
70
 
76
71
  if (this._inner.inclusions.length ||
77
72
  this._inner.exclusions.length ||
78
- this._inner.requireds.length ||
79
- this._inner.ordereds.length ||
80
73
  !this._flags.sparse) {
81
74
 
82
75
  // Clone the array so that we don't modify the original
@@ -129,7 +122,7 @@ internals.checkItems = function (items, wasArray, state, options) {
129
122
  // Sparse
130
123
 
131
124
  if (!this._flags.sparse && item === undefined) {
132
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
125
+ errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path }, options));
133
126
 
134
127
  if (options.abortEarly) {
135
128
  return errors;
@@ -170,15 +163,6 @@ internals.checkItems = function (items, wasArray, state, options) {
170
163
  --i;
171
164
  --il;
172
165
  }
173
- else if (!this._flags.sparse && res.value === undefined) {
174
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
175
-
176
- if (options.abortEarly) {
177
- return errors;
178
- }
179
-
180
- continue;
181
- }
182
166
  else {
183
167
  items[i] = res.value;
184
168
  }
@@ -212,15 +196,6 @@ internals.checkItems = function (items, wasArray, state, options) {
212
196
  internals.fastSplice(requireds, j);
213
197
  --j;
214
198
  --jl;
215
-
216
- if (!this._flags.sparse && res.value === undefined) {
217
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
218
-
219
- if (options.abortEarly) {
220
- return errors;
221
- }
222
- }
223
-
224
199
  break;
225
200
  }
226
201
  }
@@ -231,10 +206,6 @@ internals.checkItems = function (items, wasArray, state, options) {
231
206
 
232
207
  // Inclusions
233
208
 
234
- const stripUnknown = options.stripUnknown
235
- ? (options.stripUnknown === true ? true : !!options.stripUnknown.arrays)
236
- : false;
237
-
238
209
  jl = inclusions.length;
239
210
  for (let j = 0; j < jl; ++j) {
240
211
  const inclusion = inclusions[j];
@@ -253,10 +224,6 @@ internals.checkItems = function (items, wasArray, state, options) {
253
224
  --i;
254
225
  --il;
255
226
  }
256
- else if (!this._flags.sparse && res.value === undefined) {
257
- errors.push(this.createError('array.sparse', null, { key: state.key, path: localState.path, pos: i }, options));
258
- errored = true;
259
- }
260
227
  else {
261
228
  items[i] = res.value;
262
229
  }
@@ -267,7 +234,7 @@ internals.checkItems = function (items, wasArray, state, options) {
267
234
 
268
235
  // Return the actual error if only one inclusion defined
269
236
  if (jl === 1) {
270
- if (stripUnknown) {
237
+ if (options.stripUnknown) {
271
238
  internals.fastSplice(items, i);
272
239
  --i;
273
240
  --il;
@@ -291,7 +258,7 @@ internals.checkItems = function (items, wasArray, state, options) {
291
258
  }
292
259
 
293
260
  if (this._inner.inclusions.length && !isValid) {
294
- if (stripUnknown) {
261
+ if (options.stripUnknown) {
295
262
  internals.fastSplice(items, i);
296
263
  --i;
297
264
  --il;
@@ -334,7 +301,7 @@ internals.fillMissedErrors = function (errors, requireds, state, options) {
334
301
 
335
302
  if (knownMisses.length) {
336
303
  if (unknownMisses) {
337
- errors.push(this.createError('array.includesRequiredBoth', { knownMisses: knownMisses, unknownMisses: unknownMisses }, { key: state.key, path: state.path }, options));
304
+ errors.push(this.createError('array.includesRequiredBoth', { knownMisses: knownMisses, unknownMisses: unknownMisses }, { key: state.key, path: state.patk }, options));
338
305
  }
339
306
  else {
340
307
  errors.push(this.createError('array.includesRequiredKnowns', { knownMisses: knownMisses }, { key: state.key, path: state.path }, options));
package/lib/boolean.js CHANGED
@@ -30,8 +30,15 @@ internals.Boolean.prototype._base = function (value, state, options) {
30
30
  options.convert) {
31
31
 
32
32
  const lower = value.toLowerCase();
33
- result.value = (lower === 'true' || lower === 'yes' || lower === 'on' ? true
34
- : (lower === 'false' || lower === 'no' || lower === 'off' ? false : value));
33
+ result.value = (lower === 'true' || lower === 'yes' || lower === 'on' || lower === '1' ? true
34
+ : (lower === 'false' || lower === 'no' || lower === 'off' || lower === '0' ? false : value));
35
+ }
36
+
37
+ if (typeof value === 'number' &&
38
+ options.convert) {
39
+
40
+ result.value = (value === 1 ? true
41
+ : (value === 0 ? false : value));
35
42
  }
36
43
 
37
44
  result.errors = (typeof result.value === 'boolean') ? null : this.createError('boolean.base', null, state, options);
package/lib/date.js CHANGED
@@ -41,9 +41,6 @@ internals.Date.prototype._base = function (value, state, options) {
41
41
  if (result.value instanceof Date && !isNaN(result.value.getTime())) {
42
42
  result.errors = null;
43
43
  }
44
- else if (!options.convert) {
45
- result.errors = this.createError('date.strict', null, state, options);
46
- }
47
44
  else {
48
45
  let type;
49
46
  if (internals.isIsoDate(this._flags.format)) {
package/lib/errors.js CHANGED
@@ -115,39 +115,28 @@ exports.process = function (errors, object) {
115
115
  for (let i = 0; i < localErrors.length; ++i) {
116
116
  const item = localErrors[i];
117
117
 
118
- if (item.options.error) {
119
- return item.options.error;
120
- }
118
+ const detail = {
119
+ message: item.toString(),
120
+ path: internals.getPath(item),
121
+ type: item.type,
122
+ context: item.context
123
+ };
121
124
 
122
- let itemMessage;
123
125
  if (parent === undefined) {
124
- itemMessage = item.toString();
125
- message = message + (message ? '. ' : '') + itemMessage;
126
+ message = message + (message ? '. ' : '') + detail.message;
126
127
  }
127
128
 
128
129
  // Do not push intermediate errors, we're only interested in leafs
129
-
130
130
  if (item.context.reason && item.context.reason.length) {
131
- const override = processErrors(item.context.reason, item.path);
132
- if (override) {
133
- return override;
134
- }
131
+ processErrors(item.context.reason, item.path);
135
132
  }
136
133
  else {
137
- details.push({
138
- message: itemMessage || item.toString(),
139
- path: internals.getPath(item),
140
- type: item.type,
141
- context: item.context
142
- });
134
+ details.push(detail);
143
135
  }
144
136
  }
145
137
  };
146
138
 
147
- const override = processErrors(errors);
148
- if (override) {
149
- return override;
150
- }
139
+ processErrors(errors);
151
140
 
152
141
  const error = new Error(message);
153
142
  error.isJoi = true;
@@ -161,7 +150,17 @@ exports.process = function (errors, object) {
161
150
 
162
151
  internals.getPath = function (item) {
163
152
 
164
- return item.path || item.context.key;
153
+ const recursePath = (it) => {
154
+
155
+ const reachedItem = Hoek.reach(it, 'context.reason.0');
156
+ if (reachedItem && reachedItem.context) {
157
+ return recursePath(reachedItem);
158
+ }
159
+
160
+ return it.path;
161
+ };
162
+
163
+ return recursePath(item) || item.context.key;
165
164
  };
166
165
 
167
166