diff --git a/package.json b/package.json index ba1be68bbcc6..b8f3a52c4883 100644 --- a/package.json +++ b/package.json @@ -259,7 +259,7 @@ "io-ts": "^2.0.5", "ipaddr.js": "2.0.0", "isbinaryfile": "4.0.2", - "joi": "^13.5.2", + "joi": "^17.4.0", "jquery": "^3.5.0", "js-levenshtein": "^1.1.6", "js-search": "^1.4.3", @@ -546,7 +546,7 @@ "@types/jest": "^26.0.22", "@types/jest-specific-snapshot": "^0.5.5", "@types/jest-when": "^2.7.2", - "@types/joi": "^13.4.2", + "@types/joi": "^17.2.3", "@types/jquery": "^3.3.31", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts index 56e855c6c1b7..5ab5bd6a9ef0 100644 --- a/packages/kbn-config-schema/src/internals/index.ts +++ b/packages/kbn-config-schema/src/internals/index.ts @@ -7,75 +7,30 @@ */ import Joi from 'joi'; -import { - AnySchema, - JoiRoot, - Reference, - Rules, - SchemaLike, - State, - ValidationErrorItem, - ValidationOptions, -} from 'joi'; +import type { JoiRoot, CustomHelpers } from 'joi'; import { isPlainObject } from 'lodash'; import { isDuration } from 'moment'; import { Stream } from 'stream'; import { ByteSizeValue, ensureByteSizeValue } from '../byte_size_value'; import { ensureDuration } from '../duration'; -export { AnySchema, Reference, SchemaLike, ValidationErrorItem }; - function isMap(o: any): o is Map { return o instanceof Map; } -const anyCustomRule: Rules = { - name: 'custom', - params: { - validator: Joi.func().maxArity(1).required(), - }, - validate(params, value, state, options) { - let validationResultMessage; - try { - validationResultMessage = params.validator(value); - } catch (e) { - validationResultMessage = e.message || e; - } - - if (typeof validationResultMessage === 'string') { - return this.createError( - 'any.custom', - { value, message: validationResultMessage }, - state, - options - ); - } - - return value; - }, -}; - -/** - * @internal - */ -export const internals = Joi.extend([ +export const internals: JoiRoot = Joi.extend( { - name: 'any', - - rules: [anyCustomRule], - }, - { - name: 'boolean', - + type: 'boolean', base: Joi.boolean(), - coerce(value: any, state: State, options: ValidationOptions) { + coerce(value, { error }: CustomHelpers) { // If value isn't defined, let Joi handle default value if it's defined. if (value === undefined) { - return value; + return { + value, + }; } // Allow strings 'true' and 'false' to be coerced to booleans (case-insensitive). - // From Joi docs on `Joi.boolean`: // > Generates a schema object that matches a boolean data type. Can also // > be called via bool(). If the validation convert option is on @@ -87,135 +42,125 @@ export const internals = Joi.extend([ } if (typeof value !== 'boolean') { - return this.createError('boolean.base', { value }, state, options); + return { + errors: [error('boolean.base')], + }; } - return value; + return { + value, + }; }, - rules: [anyCustomRule], }, { - name: 'binary', - - base: Joi.binary(), - coerce(value: any, state: State, options: ValidationOptions) { - // If value isn't defined, let Joi handle default value if it's defined. - if (value !== undefined && !(typeof value === 'object' && Buffer.isBuffer(value))) { - return this.createError('binary.base', { value }, state, options); - } - - return value; - }, - rules: [anyCustomRule], - }, - { - name: 'stream', - - pre(value: any, state: State, options: ValidationOptions) { - // If value isn't defined, let Joi handle default value if it's defined. + type: 'stream', + prepare(value, { error }) { if (value instanceof Stream) { - return value as any; + return { value }; } - - return this.createError('stream.base', { value }, state, options); + return { + errors: [error('stream.base')], + }; }, - rules: [anyCustomRule], }, { - name: 'string', - - base: Joi.string(), - rules: [anyCustomRule], - }, - { - name: 'bytes', - - coerce(value: any, state: State, options: ValidationOptions) { + type: 'bytes', + coerce(value: any, { error }) { try { if (typeof value === 'string') { - return ByteSizeValue.parse(value); + return { value: ByteSizeValue.parse(value) }; } if (typeof value === 'number') { - return new ByteSizeValue(value); + return { value: new ByteSizeValue(value) }; } } catch (e) { - return this.createError('bytes.parse', { value, message: e.message }, state, options); + return { + errors: [error('bytes.parse', { message: e.message })], + }; } - - return value; + return { value }; }, - pre(value: any, state: State, options: ValidationOptions) { + validate(value, { error }) { // If value isn't defined, let Joi handle default value if it's defined. if (value instanceof ByteSizeValue) { - return value as any; + return { value }; } - - return this.createError('bytes.base', { value }, state, options); + return { + errors: [error('bytes.base')], + }; }, - rules: [ - anyCustomRule, - { - name: 'min', - params: { - limit: Joi.alternatives([Joi.number(), Joi.string()]).required(), + rules: { + min: { + args: [ + { + name: 'limit', + assert: Joi.alternatives([Joi.number(), Joi.string()]).required(), + }, + ], + method(limit) { + return this.$_addRule({ name: 'min', args: { limit } }); }, - validate(params, value, state, options) { - const limit = ensureByteSizeValue(params.limit); + validate(value, { error }, args) { + const limit = ensureByteSizeValue(args.limit); if (value.isLessThan(limit)) { - return this.createError('bytes.min', { value, limit }, state, options); + return error('bytes.min', { value, limit }); } return value; }, }, - { - name: 'max', - params: { - limit: Joi.alternatives([Joi.number(), Joi.string()]).required(), + max: { + args: [ + { + name: 'limit', + assert: Joi.alternatives([Joi.number(), Joi.string()]).required(), + }, + ], + method(limit) { + return this.$_addRule({ name: 'max', args: { limit } }); }, - validate(params, value, state, options) { - const limit = ensureByteSizeValue(params.limit); + validate(value, { error }, args) { + const limit = ensureByteSizeValue(args.limit); if (value.isGreaterThan(limit)) { - return this.createError('bytes.max', { value, limit }, state, options); + return error('bytes.max', { value, limit }); } return value; }, }, - ], + }, }, { - name: 'duration', - - coerce(value: any, state: State, options: ValidationOptions) { + type: 'duration', + coerce(value, { error }) { try { if (typeof value === 'string' || typeof value === 'number') { - return ensureDuration(value); + return { value: ensureDuration(value) }; } } catch (e) { - return this.createError('duration.parse', { value, message: e.message }, state, options); + return { + errors: [error('duration.parse', { message: e.message })], + }; } - - return value; + return { value }; }, - pre(value: any, state: State, options: ValidationOptions) { + validate(value, { error }) { if (!isDuration(value)) { - return this.createError('duration.base', { value }, state, options); + return { + errors: [error('duration.base')], + }; } - - return value; + return { value }; }, - rules: [anyCustomRule], }, { - name: 'number', - + type: 'number', base: Joi.number(), - coerce(value: any, state: State, options: ValidationOptions) { + coerce(value, { error }) { // If value isn't defined, let Joi handle default value if it's defined. if (value === undefined) { - return value; + return { value }; } // Do we want to allow strings that can be converted, e.g. "2"? (Joi does) @@ -226,198 +171,197 @@ export const internals = Joi.extend([ // > strings that can be converted to numbers) const coercedValue: any = typeof value === 'string' ? Number(value) : value; if (typeof coercedValue !== 'number' || isNaN(coercedValue)) { - return this.createError('number.base', { value }, state, options); + return { + errors: [error('number.base')], + }; } - return value; + return { value }; }, - rules: [anyCustomRule], }, { - name: 'object', - + type: 'object', base: Joi.object(), - coerce(value: any, state: State, options: ValidationOptions) { + coerce(value: any, { error, prefs }) { if (value === undefined || isPlainObject(value)) { - return value; + return { value }; } - if (options.convert && typeof value === 'string') { + if (prefs.convert && typeof value === 'string') { try { const parsed = JSON.parse(value); - if (isPlainObject(parsed)) { - return parsed; - } - return this.createError('object.base', { value: parsed }, state, options); + return { value: parsed }; } catch (e) { - return this.createError('object.parse', { value }, state, options); + return { errors: [error('object.parse')] }; } } - return this.createError('object.base', { value }, state, options); + return { errors: [error('object.base')] }; }, - rules: [anyCustomRule], }, { - name: 'map', - - coerce(value: any, state: State, options: ValidationOptions) { + type: 'array', + base: Joi.array(), + coerce(value: any, { error, prefs }) { + if (value === undefined || Array.isArray(value)) { + return { value }; + } + if (prefs.convert && typeof value === 'string') { + try { + // ensuring that the parsed object is an array is done by the base's validation + return { value: JSON.parse(value) }; + } catch (e) { + return { + errors: [error('array.parse')], + }; + } + } + return { + errors: [error('array.base')], + }; + }, + }, + { + type: 'map', + coerce(value, { error, prefs }) { if (value === undefined) { - return value; + return { value }; } if (isPlainObject(value)) { - return new Map(Object.entries(value)); + return { value: new Map(Object.entries(value)) }; } - if (options.convert && typeof value === 'string') { + if (prefs.convert && typeof value === 'string') { try { const parsed = JSON.parse(value); if (isPlainObject(parsed)) { - return new Map(Object.entries(parsed)); + return { value: new Map(Object.entries(parsed)) }; } - return this.createError('map.base', { value: parsed }, state, options); + return { + value: parsed, + }; } catch (e) { - return this.createError('map.parse', { value }, state, options); + return { + errors: [error('map.parse')], + }; } } - - return value; + return { value }; }, - pre(value: any, state: State, options: ValidationOptions) { + validate(value, { error }) { if (!isMap(value)) { - return this.createError('map.base', { value }, state, options); + return { + errors: [error('map.base')], + }; } - return value as any; + return { value }; }, - rules: [ - anyCustomRule, - { - name: 'entries', - params: { - key: Joi.object().schema(), - value: Joi.object().schema(), + rules: { + entries: { + args: [ + { + name: 'key', + assert: Joi.object().schema(), + }, + { + name: 'value', + assert: Joi.object().schema(), + }, + ], + method(key, value) { + return this.$_addRule({ name: 'entries', args: { key, value } }); }, - validate(params, value, state, options) { + validate(value, { error }, args, options) { const result = new Map(); for (const [entryKey, entryValue] of value) { - const { value: validatedEntryKey, error: keyError } = Joi.validate( - entryKey, - params.key, - { presence: 'required' } - ); - - if (keyError) { - return this.createError('map.key', { entryKey, reason: keyError }, state, options); + let validatedEntryKey: any; + try { + validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' }); + } catch (e) { + return error('map.key', { entryKey, reason: e }); } - const { value: validatedEntryValue, error: valueError } = Joi.validate( - entryValue, - params.value, - { presence: 'required' } - ); - - if (valueError) { - return this.createError( - 'map.value', - { entryKey, reason: valueError }, - state, - options - ); + let validatedEntryValue: any; + try { + validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' }); + } catch (e) { + return error('map.value', { entryKey, reason: e }); } result.set(validatedEntryKey, validatedEntryValue); } - - return result as any; + return result; }, }, - ], + }, }, { - name: 'record', - pre(value: any, state: State, options: ValidationOptions) { + type: 'record', + coerce(value, { error, prefs }) { if (value === undefined || isPlainObject(value)) { - return value; + return { value }; } - if (options.convert && typeof value === 'string') { + if (prefs.convert && typeof value === 'string') { try { const parsed = JSON.parse(value); - if (isPlainObject(parsed)) { - return parsed; - } - return this.createError('record.base', { value: parsed }, state, options); + return { value: parsed }; } catch (e) { - return this.createError('record.parse', { value }, state, options); + return { + errors: [error('record.parse')], + }; } } - - return this.createError('record.base', { value }, state, options); + return { + errors: [error('record.base')], + }; }, - rules: [ - anyCustomRule, - { - name: 'entries', - params: { key: Joi.object().schema(), value: Joi.object().schema() }, - validate(params, value, state, options) { + validate(value, { error }) { + if (!isPlainObject(value)) { + return { + errors: [error('record.base')], + }; + } + + return { value }; + }, + rules: { + entries: { + args: [ + { + name: 'key', + assert: Joi.object().schema(), + }, + { + name: 'value', + assert: Joi.object().schema(), + }, + ], + method(key, value) { + return this.$_addRule({ name: 'entries', args: { key, value } }); + }, + validate(value, { error }, args) { const result = {} as Record; for (const [entryKey, entryValue] of Object.entries(value)) { - const { value: validatedEntryKey, error: keyError } = Joi.validate( - entryKey, - params.key, - { presence: 'required' } - ); - - if (keyError) { - return this.createError('record.key', { entryKey, reason: keyError }, state, options); + let validatedEntryKey: any; + try { + validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' }); + } catch (e) { + return error('record.key', { entryKey, reason: e }); } - const { value: validatedEntryValue, error: valueError } = Joi.validate( - entryValue, - params.value, - { presence: 'required' } - ); - - if (valueError) { - return this.createError( - 'record.value', - { entryKey, reason: valueError }, - state, - options - ); + let validatedEntryValue: any; + try { + validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' }); + } catch (e) { + return error('record.value', { entryKey, reason: e }); } result[validatedEntryKey] = validatedEntryValue; } - - return result as any; + return result; }, }, - ], - }, - { - name: 'array', - - base: Joi.array(), - coerce(value: any, state: State, options: ValidationOptions) { - if (value === undefined || Array.isArray(value)) { - return value; - } - - if (options.convert && typeof value === 'string') { - try { - const parsed = JSON.parse(value); - if (Array.isArray(parsed)) { - return parsed; - } - return this.createError('array.base', { value: parsed }, state, options); - } catch (e) { - return this.createError('array.parse', { value }, state, options); - } - } - - return this.createError('array.base', { value }, state, options); }, - rules: [anyCustomRule], - }, -]) as JoiRoot; + } +); diff --git a/packages/kbn-config-schema/src/references/reference.ts b/packages/kbn-config-schema/src/references/reference.ts index e5731325ef7e..888d6c17704a 100644 --- a/packages/kbn-config-schema/src/references/reference.ts +++ b/packages/kbn-config-schema/src/references/reference.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { internals, Reference as InternalReference } from '../internals'; +import type { Reference as InternalReference } from 'joi'; +import { internals } from '../internals'; export class Reference { public static isReference(value: V | Reference | undefined): value is Reference { diff --git a/packages/kbn-config-schema/src/types/buffer_type.test.ts b/packages/kbn-config-schema/src/types/buffer_type.test.ts index 52805aa0452d..6300c5009d08 100644 --- a/packages/kbn-config-schema/src/types/buffer_type.test.ts +++ b/packages/kbn-config-schema/src/types/buffer_type.test.ts @@ -27,6 +27,10 @@ test('includes namespace in failure', () => { ); }); +test('coerces strings to buffer', () => { + expect(schema.buffer().validate('abc')).toStrictEqual(Buffer.from('abc')); +}); + describe('#defaultValue', () => { test('returns default when undefined', () => { const value = Buffer.from('Hi!'); @@ -49,8 +53,4 @@ test('returns error when not a buffer', () => { expect(() => schema.buffer().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( `"expected value of type [Buffer] but got [Array]"` ); - - expect(() => schema.buffer().validate('abc')).toThrowErrorMatchingInlineSnapshot( - `"expected value of type [Buffer] but got [string]"` - ); }); diff --git a/packages/kbn-config-schema/src/types/conditional_type.test.ts b/packages/kbn-config-schema/src/types/conditional_type.test.ts index b9fe5c94b6d3..0c211cc22661 100644 --- a/packages/kbn-config-schema/src/types/conditional_type.test.ts +++ b/packages/kbn-config-schema/src/types/conditional_type.test.ts @@ -348,15 +348,15 @@ test('works within `oneOf`', () => { expect(type.validate(['a', 'b'], { type: 'array' })).toEqual(['a', 'b']); expect(() => type.validate(1, { type: 'string' })).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value of type [string] but got [number] -- [1]: expected value of type [array] but got [number]" -`); + "types that failed validation: + - [0]: expected value of type [string] but got [number] + - [1]: expected value of type [array] but got [number]" + `); expect(() => type.validate(true, { type: 'string' })).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value of type [string] but got [boolean] -- [1]: expected value of type [array] but got [boolean]" -`); + "types that failed validation: + - [0]: expected value of type [string] but got [boolean] + - [1]: expected value of type [array] but got [boolean]" + `); }); describe('#validate', () => { diff --git a/packages/kbn-config-schema/src/types/literal_type.ts b/packages/kbn-config-schema/src/types/literal_type.ts index 0737070acaf8..d0e1d954c725 100644 --- a/packages/kbn-config-schema/src/types/literal_type.ts +++ b/packages/kbn-config-schema/src/types/literal_type.ts @@ -17,7 +17,7 @@ export class LiteralType extends Type { protected handleError(type: string, { value, valids: [expectedValue] }: Record) { switch (type) { case 'any.required': - case 'any.allowOnly': + case 'any.only': return `expected value to equal [${expectedValue}]`; } } diff --git a/packages/kbn-config-schema/src/types/maybe_type.ts b/packages/kbn-config-schema/src/types/maybe_type.ts index ef901dd7a637..a6712160a8e5 100644 --- a/packages/kbn-config-schema/src/types/maybe_type.ts +++ b/packages/kbn-config-schema/src/types/maybe_type.ts @@ -14,7 +14,7 @@ export class MaybeType extends Type { type .getSchema() .optional() - .default(() => undefined, 'undefined') + .default(() => undefined) ); } } diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index e101f05e02f2..67f0963fefdd 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -181,14 +181,15 @@ test('called with wrong type', () => { test('handles oneOf', () => { const type = schema.object({ - key: schema.oneOf([schema.string()]), + key: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), }); expect(type.validate({ key: 'foo' })).toEqual({ key: 'foo' }); expect(() => type.validate({ key: 123 })).toThrowErrorMatchingInlineSnapshot(` -"[key]: types that failed validation: -- [key.0]: expected value of type [string] but got [number]" -`); + "[key]: types that failed validation: + - [key.0]: expected value of type [string] but got [number] + - [key.1]: expected value of type [array] but got [number]" + `); }); test('handles references', () => { diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index 6cce8df60e8c..284ea6fddb35 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ +import type { AnySchema } from 'joi'; import typeDetect from 'type-detect'; -import { AnySchema, internals } from '../internals'; +import { internals } from '../internals'; import { Type, TypeOptions } from './type'; import { ValidationError } from '../errors'; @@ -185,7 +186,7 @@ export class ObjectType

extends Type> return `expected a plain object value, but found [${typeDetect(value)}] instead.`; case 'object.parse': return `could not parse object value from json input`; - case 'object.allowUnknown': + case 'object.unknown': return `definition for this key is missing`; case 'object.child': return reason[0]; diff --git a/packages/kbn-config-schema/src/types/record_of_type.test.ts b/packages/kbn-config-schema/src/types/record_type.test.ts similarity index 100% rename from packages/kbn-config-schema/src/types/record_of_type.test.ts rename to packages/kbn-config-schema/src/types/record_type.test.ts diff --git a/packages/kbn-config-schema/src/types/stream_type.test.ts b/packages/kbn-config-schema/src/types/stream_type.test.ts index 34323176e943..0f3e1d42dc3a 100644 --- a/packages/kbn-config-schema/src/types/stream_type.test.ts +++ b/packages/kbn-config-schema/src/types/stream_type.test.ts @@ -46,13 +46,7 @@ test('includes namespace in failure', () => { describe('#defaultValue', () => { test('returns default when undefined', () => { const value = new Stream(); - expect(schema.stream({ defaultValue: value }).validate(undefined)).toMatchInlineSnapshot(` - Stream { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - } - `); + expect(schema.stream({ defaultValue: value }).validate(undefined)).toBeInstanceOf(Stream); }); test('returns value when specified', () => { diff --git a/packages/kbn-config-schema/src/types/string_type.test.ts b/packages/kbn-config-schema/src/types/string_type.test.ts index f08634ba28b0..7eb6f386fcea 100644 --- a/packages/kbn-config-schema/src/types/string_type.test.ts +++ b/packages/kbn-config-schema/src/types/string_type.test.ts @@ -30,6 +30,20 @@ test('includes namespace in failure', () => { ); }); +test('returns error when not string', () => { + expect(() => schema.string().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + + expect(() => schema.string().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [Array]"` + ); + + expect(() => schema.string().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [RegExp]"` + ); +}); + describe('#minLength', () => { test('returns value when longer string', () => { expect(schema.string({ minLength: 2 }).validate('foo')).toBe('foo'); @@ -71,14 +85,13 @@ describe('#hostname', () => { expect(hostNameSchema.validate('www.example.com')).toBe('www.example.com'); expect(hostNameSchema.validate('3domain.local')).toBe('3domain.local'); expect(hostNameSchema.validate('hostname')).toBe('hostname'); - expect(hostNameSchema.validate('2387628')).toBe('2387628'); expect(hostNameSchema.validate('::1')).toBe('::1'); expect(hostNameSchema.validate('0:0:0:0:0:0:0:1')).toBe('0:0:0:0:0:0:0:1'); expect(hostNameSchema.validate('xn----ascii-7gg5ei7b1i.xn--90a3a')).toBe( 'xn----ascii-7gg5ei7b1i.xn--90a3a' ); - const hostNameWithMaxAllowedLength = 'a'.repeat(255); + const hostNameWithMaxAllowedLength = Array(4).fill('a'.repeat(63)).join('.'); expect(hostNameSchema.validate(hostNameWithMaxAllowedLength)).toBe( hostNameWithMaxAllowedLength ); @@ -87,6 +100,12 @@ describe('#hostname', () => { test('returns error when value is not a valid hostname', () => { const hostNameSchema = schema.string({ hostname: true }); + expect(() => hostNameSchema.validate('2387628')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid hostname (see RFC 1123)."` + ); + expect(() => + hostNameSchema.validate(Array(4).fill('a'.repeat(64)).join('.')) + ).toThrowErrorMatchingInlineSnapshot(`"value must be a valid hostname (see RFC 1123)."`); expect(() => hostNameSchema.validate('host:name')).toThrowErrorMatchingInlineSnapshot( `"value must be a valid hostname (see RFC 1123)."` ); @@ -99,16 +118,14 @@ describe('#hostname', () => { expect(() => hostNameSchema.validate('0:?:0:0:0:0:0:1')).toThrowErrorMatchingInlineSnapshot( `"value must be a valid hostname (see RFC 1123)."` ); - - const tooLongHostName = 'a'.repeat(256); - expect(() => hostNameSchema.validate(tooLongHostName)).toThrowErrorMatchingInlineSnapshot( + expect(() => hostNameSchema.validate('a'.repeat(256))).toThrowErrorMatchingInlineSnapshot( `"value must be a valid hostname (see RFC 1123)."` ); }); test('returns error when empty string', () => { expect(() => schema.string({ hostname: true }).validate('')).toThrowErrorMatchingInlineSnapshot( - `"any.empty"` + `"\\"value\\" is not allowed to be empty"` ); }); @@ -176,17 +193,3 @@ describe('#validate', () => { ); }); }); - -test('returns error when not string', () => { - expect(() => schema.string().validate(123)).toThrowErrorMatchingInlineSnapshot( - `"expected value of type [string] but got [number]"` - ); - - expect(() => schema.string().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( - `"expected value of type [string] but got [Array]"` - ); - - expect(() => schema.string().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( - `"expected value of type [string] but got [RegExp]"` - ); -}); diff --git a/packages/kbn-config-schema/src/types/string_type.ts b/packages/kbn-config-schema/src/types/string_type.ts index 2772af6b9bab..1442c5b9b4ef 100644 --- a/packages/kbn-config-schema/src/types/string_type.ts +++ b/packages/kbn-config-schema/src/types/string_type.ts @@ -8,7 +8,7 @@ import typeDetect from 'type-detect'; import { internals } from '../internals'; -import { Type, TypeOptions } from './type'; +import { Type, TypeOptions, convertValidationFunction } from './type'; export type StringOptions = TypeOptions & { minLength?: number; @@ -25,26 +25,32 @@ export class StringType extends Type { let schema = options.hostname === true ? internals.string().hostname() - : internals.any().custom((value) => { - if (typeof value !== 'string') { - return `expected value of type [string] but got [${typeDetect(value)}]`; - } - }); + : internals.any().custom( + convertValidationFunction((value) => { + if (typeof value !== 'string') { + return `expected value of type [string] but got [${typeDetect(value)}]`; + } + }) + ); if (options.minLength !== undefined) { - schema = schema.custom((value) => { - if (value.length < options.minLength!) { - return `value has length [${value.length}] but it must have a minimum length of [${options.minLength}].`; - } - }); + schema = schema.custom( + convertValidationFunction((value) => { + if (value.length < options.minLength!) { + return `value has length [${value.length}] but it must have a minimum length of [${options.minLength}].`; + } + }) + ); } if (options.maxLength !== undefined) { - schema = schema.custom((value) => { - if (value.length > options.maxLength!) { - return `value has length [${value.length}] but it must have a maximum length of [${options.maxLength}].`; - } - }); + schema = schema.custom( + convertValidationFunction((value) => { + if (value.length > options.maxLength!) { + return `value has length [${value.length}] but it must have a maximum length of [${options.maxLength}].`; + } + }) + ); } super(schema, options); diff --git a/packages/kbn-config-schema/src/types/type.ts b/packages/kbn-config-schema/src/types/type.ts index aa98dbffb6de..696101fb2c22 100644 --- a/packages/kbn-config-schema/src/types/type.ts +++ b/packages/kbn-config-schema/src/types/type.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import type { AnySchema, CustomValidator, ErrorReport } from 'joi'; import { SchemaTypeError, ValidationError } from '../errors'; -import { AnySchema, internals, ValidationErrorItem } from '../internals'; import { Reference } from '../references'; export interface TypeOptions { @@ -15,6 +15,25 @@ export interface TypeOptions { validate?: (value: T) => string | void; } +export const convertValidationFunction = ( + validate: (value: T) => string | void +): CustomValidator => { + return (value, { error }) => { + let validationResultMessage; + try { + validationResultMessage = validate(value); + } catch (e) { + validationResultMessage = e.message || e; + } + + if (typeof validationResultMessage === 'string') { + return error('any.custom', { message: validationResultMessage }); + } + + return value; + }; +}; + export abstract class Type { // This is just to enable the `TypeOf` helper, and because TypeScript would // fail if it wasn't initialized we use a "trick" to which basically just @@ -36,24 +55,23 @@ export abstract class Type { // If default value is a function, then we must provide description for it. if (typeof options.defaultValue === 'function') { - schema = schema.default(options.defaultValue, 'Type default value'); + schema = schema.default(options.defaultValue); } else { schema = schema.default( Reference.isReference(options.defaultValue) ? options.defaultValue.getSchema() - : options.defaultValue + : (options.defaultValue as any) ); } } if (options.validate) { - schema = schema.custom(options.validate); + schema = schema.custom(convertValidationFunction(options.validate)); } // Attach generic error handler only if it hasn't been attached yet since // only the last error handler is counted. - const schemaFlags = (schema.describe().flags as Record) || {}; - if (schemaFlags.error === undefined) { + if (schema.$_getFlag('error') === undefined) { schema = schema.error(([error]) => this.onError(error)); } @@ -61,7 +79,7 @@ export abstract class Type { } public validate(value: any, context: Record = {}, namespace?: string): V { - const { value: validatedValue, error } = internals.validate(value, this.internalSchema, { + const { value: validatedValue, error } = this.internalSchema.validate(value, { context, presence: 'required', }); @@ -88,14 +106,19 @@ export abstract class Type { return undefined; } - private onError(error: SchemaTypeError | ValidationErrorItem): SchemaTypeError { + private onError(error: SchemaTypeError | ErrorReport): SchemaTypeError { if (error instanceof SchemaTypeError) { return error; } - const { context = {}, type, path, message } = error; + const { local, code, path, value } = error; + const convertedPath = path.map((entry) => entry.toString()); + const context: Record = { + ...local, + value, + }; - const errorHandleResult = this.handleError(type, context, path); + const errorHandleResult = this.handleError(code, context, convertedPath); if (errorHandleResult instanceof SchemaTypeError) { return errorHandleResult; } @@ -103,15 +126,18 @@ export abstract class Type { // If error handler just defines error message, then wrap it into proper // `SchemaTypeError` instance. if (typeof errorHandleResult === 'string') { - return new SchemaTypeError(errorHandleResult, path); + return new SchemaTypeError(errorHandleResult, convertedPath); } // If error is produced by the custom validator, just extract source message // from context and wrap it into `SchemaTypeError` instance. - if (type === 'any.custom') { - return new SchemaTypeError(context.message, path); + if (code === 'any.custom' && context.message) { + return new SchemaTypeError(context.message, convertedPath); } - return new SchemaTypeError(message || type, path); + // `message` is only initialized once `toString` has been called (...) + // see https://github.com/sideway/joi/blob/master/lib/errors.js + const message = error.toString(); + return new SchemaTypeError(message || code, convertedPath); } } diff --git a/packages/kbn-config-schema/src/types/one_of_type.test.ts b/packages/kbn-config-schema/src/types/union_type.test.ts similarity index 66% rename from packages/kbn-config-schema/src/types/one_of_type.test.ts rename to packages/kbn-config-schema/src/types/union_type.test.ts index 7e2077db5412..c18e516ecd3e 100644 --- a/packages/kbn-config-schema/src/types/one_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/union_type.test.ts @@ -62,22 +62,42 @@ test('handles object', () => { }); test('handles object with wrong type', () => { - const type = schema.oneOf([schema.object({ age: schema.number() })]); + const type = schema.oneOf([schema.object({ age: schema.number() }), schema.string()]); expect(() => type.validate({ age: 'foo' })).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0.age]: expected value of type [number] but got [string]" -`); + "types that failed validation: + - [0.age]: expected value of type [number] but got [string] + - [1]: expected value of type [string] but got [Object]" + `); +}); + +test('use shorter error messages when defining only one type', () => { + const type = schema.oneOf([schema.object({ age: schema.number() })]); + + expect(() => type.validate({ age: 'foo' })).toThrowErrorMatchingInlineSnapshot( + `"[age]: expected value of type [number] but got [string]"` + ); }); test('includes namespace in failure', () => { - const type = schema.oneOf([schema.object({ age: schema.number() })]); + const type = schema.oneOf([schema.object({ age: schema.number() }), schema.string()]); expect(() => type.validate({ age: 'foo' }, {}, 'foo-namespace')) .toThrowErrorMatchingInlineSnapshot(` -"[foo-namespace]: types that failed validation: -- [foo-namespace.0.age]: expected value of type [number] but got [string]" -`); + "[foo-namespace]: types that failed validation: + - [foo-namespace.0.age]: expected value of type [number] but got [string] + - [foo-namespace.1]: expected value of type [string] but got [Object]" + `); +}); + +test('includes namespace in failure in shorthand mode', () => { + const type = schema.oneOf([schema.object({ age: schema.number() })]); + + expect(() => + type.validate({ age: 'foo' }, {}, 'foo-namespace') + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace.age]: expected value of type [number] but got [string]"` + ); }); test('handles multiple objects with same key', () => { @@ -106,33 +126,32 @@ test('handles maybe', () => { test('fails if not matching type', () => { const type = schema.oneOf([schema.string()]); - expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value of type [string] but got [boolean]" -`); - expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value of type [string] but got [number]" -`); + expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [boolean]"` + ); + expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); }); test('fails if not matching multiple types', () => { const type = schema.oneOf([schema.string(), schema.number()]); expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value of type [string] but got [boolean] -- [1]: expected value of type [number] but got [boolean]" -`); + "types that failed validation: + - [0]: expected value of type [string] but got [boolean] + - [1]: expected value of type [number] but got [boolean]" + `); }); test('fails if not matching literal', () => { - const type = schema.oneOf([schema.literal('foo')]); + const type = schema.oneOf([schema.literal('foo'), schema.literal('dolly')]); expect(() => type.validate('bar')).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value to equal [foo]" -`); + "types that failed validation: + - [0]: expected value to equal [foo] + - [1]: expected value to equal [dolly]" + `); }); test('fails if nested union type fail', () => { @@ -142,12 +161,10 @@ test('fails if nested union type fail', () => { ]); expect(() => type.validate('aaa')).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: types that failed validation: - - [0]: expected value of type [boolean] but got [string] -- [1]: types that failed validation: - - [0]: types that failed validation: - - [0]: could not parse object value from json input - - [1]: expected value of type [number] but got [string]" -`); + "types that failed validation: + - [0]: expected value of type [boolean] but got [string] + - [1]: types that failed validation: + - [0]: could not parse object value from json input + - [1]: expected value of type [number] but got [string]" + `); }); diff --git a/packages/kbn-config-schema/src/types/union_type.ts b/packages/kbn-config-schema/src/types/union_type.ts index c67362daea71..ef233d9d4a64 100644 --- a/packages/kbn-config-schema/src/types/union_type.ts +++ b/packages/kbn-config-schema/src/types/union_type.ts @@ -13,20 +13,21 @@ import { Type, TypeOptions } from './type'; export class UnionType>, T> extends Type { constructor(types: RTS, options?: TypeOptions) { - const schema = internals.alternatives(types.map((type) => type.getSchema())); + const schema = internals.alternatives(types.map((type) => type.getSchema())).match('any'); super(schema, options); } - protected handleError(type: string, { reason, value }: Record, path: string[]) { + protected handleError(type: string, { value, details }: Record, path: string[]) { switch (type) { case 'any.required': return `expected at least one defined value but got [${typeDetect(value)}]`; - case 'alternatives.child': + case 'alternatives.match': return new SchemaTypesError( 'types that failed validation:', path, - reason.map((e: SchemaTypeError, index: number) => { + details.map((detail: AlternativeErrorDetail, index: number) => { + const e = detail.context.error; const childPathWithIndex = e.path.slice(); childPathWithIndex.splice(path.length, 0, index.toString()); @@ -38,3 +39,9 @@ export class UnionType>, T> extends Type { } } } + +interface AlternativeErrorDetail { + context: { + error: SchemaTypeError; + }; +} diff --git a/packages/kbn-config-schema/types/joi.d.ts b/packages/kbn-config-schema/types/joi.d.ts index 88bdffe8f77b..5dd695cb05e8 100644 --- a/packages/kbn-config-schema/types/joi.d.ts +++ b/packages/kbn-config-schema/types/joi.d.ts @@ -12,6 +12,7 @@ import { ByteSizeValue } from '../src/byte_size_value'; declare module 'joi' { interface BytesSchema extends AnySchema { min(limit: number | string | ByteSizeValue): this; + max(limit: number | string | ByteSizeValue): this; } @@ -23,6 +24,14 @@ declare module 'joi' { entries(key: AnySchema, value: AnySchema): this; } + interface ErrorReport { + // missing from the typedef + // see https://github.com/sideway/joi/blob/master/lib/errors.js + local?: Record; + + toString(): string; + } + export type JoiRoot = Joi.Root & { bytes: () => BytesSchema; duration: () => AnySchema; @@ -30,13 +39,4 @@ declare module 'joi' { record: () => RecordSchema; stream: () => AnySchema; }; - - interface AnySchema { - custom(validator: (value: any) => string | void): this; - } - - // Joi types don't include `schema` function even though it's supported. - interface ObjectSchema { - schema(): this; - } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts new file mode 100644 index 000000000000..88c1fd99f001 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Config } from './config'; + +describe('Config', () => { + it('recognizes keys under `object().pattern`', () => { + const config = new Config({ + settings: { + services: { + foo: () => 42, + }, + }, + primary: true, + path: process.cwd(), + }); + + expect(config.has('services.foo')).toEqual(true); + expect(config.get('services.foo')()).toEqual(42); + }); +}); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts index 42da2572f885..1d4af9c33fb7 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts @@ -50,20 +50,20 @@ export class Config { values: Record, childSchema: any ): boolean { - if (!childSchema._inner) { + if (!childSchema.$_terms.keys && !childSchema.$_terms.patterns) { return false; } // normalize child and pattern checks so we can iterate the checks in a single loop const checks: Array<{ test: (k: string) => boolean; schema: Schema }> = [ // match children first, they have priority - ...(childSchema._inner.children || []).map((child: { key: string; schema: Schema }) => ({ + ...(childSchema.$_terms.keys || []).map((child: { key: string; schema: Schema }) => ({ test: (k: string) => child.key === k, schema: child.schema, })), // match patterns on any key that doesn't match an explicit child - ...(childSchema._inner.patterns || []).map((pattern: { regex: RegExp; rule: Schema }) => ({ + ...(childSchema.$_terms.patterns || []).map((pattern: { regex: RegExp; rule: Schema }) => ({ test: (k: string) => pattern.regex.test(k) && has(values, k), schema: pattern.rule, })), diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 65573fdbd664..e5d0fdc122a1 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -9,6 +9,7 @@ import { dirname, resolve } from 'path'; import Joi from 'joi'; +import type { CustomHelpers } from 'joi'; // valid pattern for ID // enforced camel-case identifiers for consistency @@ -54,15 +55,17 @@ const dockerServerSchema = () => image: requiredWhenEnabled(Joi.string()), port: requiredWhenEnabled(Joi.number()), portInContainer: requiredWhenEnabled(Joi.number()), - waitForLogLine: Joi.alternatives(Joi.object().type(RegExp), Joi.string()).optional(), + waitForLogLine: Joi.alternatives(Joi.object().instance(RegExp), Joi.string()).optional(), waitFor: Joi.func().optional(), args: Joi.array().items(Joi.string()).optional(), }) .default(); const defaultRelativeToConfigPath = (path: string) => { - const makeDefault: any = (_: any, options: any) => resolve(dirname(options.context.path), path); - makeDefault.description = `/${path}`; + const makeDefault = (parent: any, helpers: CustomHelpers) => { + helpers.schema.description(`/${path}`); + return resolve(dirname(helpers.prefs.context!.path), path); + }; return makeDefault; }; diff --git a/src/core/server/csp/config.test.ts b/src/core/server/csp/config.test.ts index c7f6c4a214fa..8036ebeeaad3 100644 --- a/src/core/server/csp/config.test.ts +++ b/src/core/server/csp/config.test.ts @@ -13,7 +13,7 @@ describe('config.validate()', () => { // This is intentionally not editable in the raw CSP config. // Users should set `server.securityResponseHeaders.disableEmbedding` to control this config property. expect(() => config.schema.validate({ disableEmbedding: true })).toThrowError( - '[disableEmbedding.0]: expected value to equal [false]' + '[disableEmbedding]: expected value to equal [false]' ); }); }); diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 42710aad40ac..65ac08f6ce5f 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -119,7 +119,7 @@ Object { exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; -exports[`throws if invalid hostname 2`] = `"[host]: value 0 is not a valid hostname (use \\"0.0.0.0\\" to bind to all interfaces)"`; +exports[`throws if invalid hostname 2`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 1f8fd95d6905..a6a133753c3f 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -87,11 +87,6 @@ const configSchema = schema.object( host: schema.string({ defaultValue: 'localhost', hostname: true, - validate(value) { - if (value === '0') { - return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; - } - }, }), maxPayload: schema.byteSize({ defaultValue: '1048576b', diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index e299db240512..0998a8010313 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -302,7 +302,7 @@ describe('Reporting Config Schema', () => { // kibanaServer const throwValidationErr = () => ConfigSchema.validate({ kibanaServer: { hostname: '0' } }); expect(throwValidationErr).toThrowError( - `[kibanaServer.hostname]: must not be "0" for the headless browser to correctly resolve the host` + `[kibanaServer.hostname]: value must be a valid hostname (see RFC 1123).` ); }); }); diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index f56bf5520072..d616a18289df 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -9,16 +9,7 @@ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import moment from 'moment'; const KibanaServerSchema = schema.object({ - hostname: schema.maybe( - schema.string({ - validate(value) { - if (value === '0') { - return 'must not be "0" for the headless browser to correctly resolve the host'; - } - }, - hostname: true, - }) - ), + hostname: schema.maybe(schema.string({ hostname: true })), port: schema.maybe(schema.number()), protocol: schema.maybe( schema.string({ diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 54d0becd2446..d6f1307b5d1b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -87,29 +87,7 @@ const MacEntrySchema = schema.object({ ...CommonEntrySchema, }); -/* - * Entry Schema depending on Os type using schema.conditional. - * If OS === WINDOWS then use Windows schema, - * else if OS === LINUX then use Linux schema, - * else use Mac schema - */ -const EntrySchemaDependingOnOS = schema.conditional( - schema.siblingRef('os'), - OperatingSystem.WINDOWS, - WindowsEntrySchema, - schema.conditional( - schema.siblingRef('os'), - OperatingSystem.LINUX, - LinuxEntrySchema, - MacEntrySchema - ) -); - -/* - * Entities array schema. - * The validate function checks there is no duplicated entry inside the array - */ -const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, { +const entriesSchemaOptions = { minSize: 1, validate(entries: ConditionEntry[]) { return ( @@ -118,7 +96,27 @@ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, { .join(', ') || undefined ); }, -}); +}; + +/* + * Entities array schema depending on Os type using schema.conditional. + * If OS === WINDOWS then use Windows schema, + * else if OS === LINUX then use Linux schema, + * else use Mac schema + * + * The validate function checks there is no duplicated entry inside the array + */ +const EntriesSchema = schema.conditional( + schema.siblingRef('os'), + OperatingSystem.WINDOWS, + schema.arrayOf(WindowsEntrySchema, entriesSchemaOptions), + schema.conditional( + schema.siblingRef('os'), + OperatingSystem.LINUX, + schema.arrayOf(LinuxEntrySchema, entriesSchemaOptions), + schema.arrayOf(MacEntrySchema, entriesSchemaOptions) + ) +); const getTrustedAppForOsScheme = (forUpdateFlow: boolean = false) => schema.object({ diff --git a/yarn.lock b/yarn.lock index 19922d11802d..fefcdfa32a43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5200,10 +5200,12 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0" integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A== -"@types/joi@^13.4.2": - version "13.6.1" - resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae" - integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog== +"@types/joi@^17.2.3": + version "17.2.3" + resolved "https://registry.yarnpkg.com/@types/joi/-/joi-17.2.3.tgz#b7768ed9d84f1ebd393328b9f97c1cf3d2b94798" + integrity sha512-dGjs/lhrWOa+eO0HwgxCSnDm5eMGCsXuvLglMghJq32F6q5LyyNuXb41DHzrg501CKNOSSAHmfB7FDGeUnDmzw== + dependencies: + joi "*" "@types/jquery@*", "@types/jquery@^3.3.31": version "3.3.31" @@ -15331,11 +15333,6 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoek@5.x.x: - version "5.0.4" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da" - integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w== - hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -16701,13 +16698,6 @@ isbinaryfile@4.0.2: resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488" integrity sha512-C3FSxJdNrEr2F4z6uFtNzECDM5hXk+46fxaa+cwBe5/XrWSmzdG8DDgyjfX6/NRdBB21q2JXuRAzPCUs+fclnQ== -isemail@3.x.x: - version "3.1.4" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.4.tgz#76e2187ff7bee59d57522c6fd1c3f09a331933cf" - integrity sha512-yE/W5osEWuAGSLVixV9pAexhkbZzglmuhO2CxdHu7IBh7uzuZogQ4bk0lE26HoZ6HD4ZYfKRKilkNuCnuJIBJw== - dependencies: - punycode "2.x.x" - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -17433,14 +17423,16 @@ jju@~1.4.0: resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo= -joi@^13.5.2: - version "13.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" - integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== +joi@*, joi@^17.4.0: + version "17.4.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.0.tgz#b5c2277c8519e016316e49ababd41a1908d9ef20" + integrity sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg== dependencies: - hoek "5.x.x" - isemail "3.x.x" - topo "3.x.x" + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.0" + "@sideway/formula" "^3.0.0" + "@sideway/pinpoint" "^2.0.0" joi@^17.3.0: version "17.3.0" @@ -22404,16 +22396,16 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - punycode@^1.2.4, punycode@^1.3.2: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + pupa@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" @@ -27021,13 +27013,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -topo@3.x.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a" - integrity sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw== - dependencies: - hoek "5.x.x" - topojson-client@3.1.0, topojson-client@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"