diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx index 39da6dcf336b5..581b1223b7892 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import Joi from 'joi'; +import * as t from 'io-ts'; import { EuiLink, EuiCode } from '@elastic/eui'; import { @@ -100,11 +100,14 @@ const fielddataFrequencyFilterParam = { }, }, }, - schema: Joi.object().keys({ - min: Joi.number(), - max: Joi.number(), - min_segment_size: Joi.number(), - }), + schema: t.intersection([ + t.partial({ + min: t.number, + max: t.number, + min_segment_size: t.number, + }), + t.brand(t.UnknownRecord, (v: any): v is any => !Array.isArray(v), 'Array'), + ]), }; const analyzerValidations = [ @@ -178,40 +181,40 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.string(), + schema: t.string, }, store: { fieldConfig: { type: FIELD_TYPES.CHECKBOX, defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, index: { fieldConfig: { type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, doc_values: { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, doc_values_binary: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, fielddata: { fieldConfig: { type: FIELD_TYPES.CHECKBOX, defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, fielddata_frequency_filter: fielddataFrequencyFilterParam, fielddata_frequency_filter_percentage: { @@ -280,19 +283,19 @@ export const PARAMETERS_DEFINITION = { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, coerce_shape: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, ignore_malformed: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, null_value: { fieldConfig: { @@ -300,7 +303,7 @@ export const PARAMETERS_DEFINITION = { type: FIELD_TYPES.TEXT, label: nullValueLabel, }, - schema: Joi.string(), + schema: t.string, }, null_value_ip: { fieldConfig: { @@ -323,7 +326,7 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.number(), + schema: t.number, }, null_value_boolean: { fieldConfig: { @@ -332,7 +335,7 @@ export const PARAMETERS_DEFINITION = { deserializer: (value: string | boolean) => mapIndexToValue.indexOf(value), serializer: (value: number) => mapIndexToValue[value], }, - schema: Joi.any().valid([true, false, 'true', 'false']), + schema: t.union([t.literal(true), t.literal(false), t.literal('true'), t.literal('false')]), }, null_value_geo_point: { fieldConfig: { @@ -376,7 +379,7 @@ export const PARAMETERS_DEFINITION = { } }, }, - schema: Joi.any(), + schema: t.any, }, copy_to: { fieldConfig: { @@ -398,7 +401,7 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.string(), + schema: t.string, }, max_input_length: { fieldConfig: { @@ -421,7 +424,7 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.number(), + schema: t.number, }, locale: { fieldConfig: { @@ -454,7 +457,7 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.string(), + schema: t.string, }, orientation: { fieldConfig: { @@ -464,7 +467,7 @@ export const PARAMETERS_DEFINITION = { defaultMessage: 'Orientation', }), }, - schema: Joi.string(), + schema: t.string, }, boost: { fieldConfig: { @@ -484,7 +487,7 @@ export const PARAMETERS_DEFINITION = { }, ], } as FieldConfig, - schema: Joi.number(), + schema: t.number, }, scaling_factor: { title: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.scalingFactorFieldTitle', { @@ -535,7 +538,7 @@ export const PARAMETERS_DEFINITION = { defaultMessage: 'Value must be greater than 0.', }), } as FieldConfig, - schema: Joi.number(), + schema: t.number, }, dynamic: { fieldConfig: { @@ -545,7 +548,7 @@ export const PARAMETERS_DEFINITION = { type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, enabled: { fieldConfig: { @@ -555,7 +558,7 @@ export const PARAMETERS_DEFINITION = { type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, format: { fieldConfig: { @@ -577,7 +580,7 @@ export const PARAMETERS_DEFINITION = { /> ), }, - schema: Joi.string(), + schema: t.string, }, analyzer: { fieldConfig: { @@ -587,7 +590,7 @@ export const PARAMETERS_DEFINITION = { defaultValue: INDEX_DEFAULT, validations: analyzerValidations, }, - schema: Joi.string(), + schema: t.string, }, search_analyzer: { fieldConfig: { @@ -597,7 +600,7 @@ export const PARAMETERS_DEFINITION = { defaultValue: INDEX_DEFAULT, validations: analyzerValidations, }, - schema: Joi.string(), + schema: t.string, }, search_quote_analyzer: { fieldConfig: { @@ -607,7 +610,7 @@ export const PARAMETERS_DEFINITION = { defaultValue: INDEX_DEFAULT, validations: analyzerValidations, }, - schema: Joi.string(), + schema: t.string, }, normalizer: { fieldConfig: { @@ -636,76 +639,76 @@ export const PARAMETERS_DEFINITION = { defaultMessage: `The name of a normalizer defined in the index's settings.`, }), }, - schema: Joi.string(), + schema: t.string, }, index_options: { fieldConfig: { ...indexOptionsConfig, defaultValue: 'positions', }, - schema: Joi.string(), + schema: t.string, }, index_options_keyword: { fieldConfig: { ...indexOptionsConfig, defaultValue: 'docs', }, - schema: Joi.string(), + schema: t.string, }, index_options_flattened: { fieldConfig: { ...indexOptionsConfig, defaultValue: 'docs', }, - schema: Joi.string(), + schema: t.string, }, eager_global_ordinals: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, index_phrases: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, preserve_separators: { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, preserve_position_increments: { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, ignore_z_value: { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, points_only: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, norms: { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, norms_keyword: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, term_vector: { fieldConfig: { @@ -715,7 +718,7 @@ export const PARAMETERS_DEFINITION = { }), defaultValue: 'no', }, - schema: Joi.string(), + schema: t.string, }, path: { fieldConfig: { @@ -741,7 +744,7 @@ export const PARAMETERS_DEFINITION = { serializer: (value: AliasOption[]) => (value.length === 0 ? '' : value[0].id), } as FieldConfig, targetTypesNotAllowed: ['object', 'nested', 'alias'] as DataType[], - schema: Joi.string(), + schema: t.string, }, position_increment_gap: { fieldConfig: { @@ -771,7 +774,7 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.number(), + schema: t.number, }, index_prefixes: { fieldConfig: { defaultValue: {} }, // Needed for FieldParams typing @@ -791,9 +794,9 @@ export const PARAMETERS_DEFINITION = { } as FieldConfig, }, }, - schema: Joi.object().keys({ - min_chars: Joi.number(), - max_chars: Joi.number(), + schema: t.partial({ + min_chars: t.number, + max_chars: t.number, }), }, similarity: { @@ -804,13 +807,13 @@ export const PARAMETERS_DEFINITION = { defaultMessage: 'Similarity algorithm', }), }, - schema: Joi.string(), + schema: t.string, }, split_queries_on_whitespace: { fieldConfig: { defaultValue: false, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, ignore_above: { fieldConfig: { @@ -842,13 +845,13 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.number(), + schema: t.number, }, enable_position_increments: { fieldConfig: { defaultValue: true, }, - schema: Joi.boolean().strict(), + schema: t.boolean, }, depth_limit: { fieldConfig: { @@ -868,7 +871,7 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.number(), + schema: t.number, }, dims: { fieldConfig: { @@ -894,6 +897,6 @@ export const PARAMETERS_DEFINITION = { }, ], }, - schema: Joi.string(), + schema: t.string, }, }; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts new file mode 100644 index 0000000000000..363ccfc2a5fab --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ValidationError, Validation } from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { Reporter } from 'io-ts/lib/Reporter'; + +export type ReporterResult = Array<{ path: string[]; message: string }>; + +export const failure = (validation: any): ReporterResult => { + return validation.map((e: ValidationError) => { + const path: string[] = []; + let validationName = ''; + + e.context.forEach((ctx, idx) => { + if (ctx.key) { + path.push(ctx.key); + } + + if (idx === e.context.length - 1) { + validationName = ctx.type.name; + } + }); + const lastItemName = path[path.length - 1]; + return { + path, + message: + 'Invalid value ' + + JSON.stringify(e.value) + + ` supplied to ${lastItemName}(${validationName})`, + }; + }); +}; + +const empty: never[] = []; +const success = () => empty; + +export const ErrorReporter: Reporter = { + report: (validation: Validation) => fold(failure, success)(validation as any), +}; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts index e9af16af2afa0..5c9fa3fd05d8c 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.test.ts @@ -63,9 +63,9 @@ describe('Mappings configuration validator', () => { expect(errors).not.toBe(undefined); expect(errors!.length).toBe(3); expect(errors!).toEqual([ - { code: 'ERR_CONFIG', configName: 'numeric_detection' }, - { code: 'ERR_CONFIG', configName: 'dynamic_date_formats' }, { code: 'ERR_CONFIG', configName: '_source' }, + { code: 'ERR_CONFIG', configName: 'dynamic_date_formats' }, + { code: 'ERR_CONFIG', configName: 'numeric_detection' }, ]); }); }); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts index cd7fc57d1dbc8..990d5ec961a6f 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; +import { pick } from 'lodash'; +import * as t from 'io-ts'; +import { ordString } from 'fp-ts/lib/Ord'; +import { toArray } from 'fp-ts/lib/Set'; +import { isLeft, isRight } from 'fp-ts/lib/Either'; +import { ErrorReporter } from './error_reporter'; import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants'; import { FieldMeta } from '../types'; import { getFieldMeta } from '../lib'; @@ -72,7 +77,7 @@ const validateParameter = (parameter: string, value: any): boolean => { const parameterSchema = (PARAMETERS_DEFINITION as any)[parameter]!.schema; if (parameterSchema) { - return Boolean(Joi.validate(value, parameterSchema).error) === false; + return isRight(parameterSchema.decode(value)); } // Fallback, if no schema defined for the parameter (this should not happen in theory) @@ -192,54 +197,55 @@ export const validateProperties = (properties = {}): PropertiesValidatorResponse * Single source of truth to validate the *configuration* of the mappings. * Whenever a user loads a JSON object it will be validate against this Joi schema. */ -export const mappingsConfigurationSchema = Joi.object().keys({ - dynamic: Joi.any().valid([true, false, 'strict']), - date_detection: Joi.boolean().strict(), - numeric_detection: Joi.boolean().strict(), - dynamic_date_formats: Joi.array().items(Joi.string()), - _source: Joi.object().keys({ - enabled: Joi.boolean().strict(), - includes: Joi.array().items(Joi.string()), - excludes: Joi.array().items(Joi.string()), +export const mappingsConfigurationSchema = t.partial({ + dynamic: t.union([t.literal(true), t.literal(false), t.literal('strict')]), + date_detection: t.boolean, + numeric_detection: t.boolean, + dynamic_date_formats: t.array(t.string), + _source: t.partial({ + enabled: t.boolean, + includes: t.array(t.string), + excludes: t.array(t.string), }), - _meta: Joi.object(), - _routing: Joi.object().keys({ - required: Joi.boolean().strict(), + _meta: t.UnknownRecord, + _routing: t.partial({ + required: t.boolean, }), }); +const mappingsConfigurationSchemaKeys = Object.keys(mappingsConfigurationSchema.props); + const validateMappingsConfiguration = ( mappingsConfiguration: any ): { value: any; errors: MappingsValidationError[] } => { // Array to keep track of invalid configuration parameters. - const configurationRemoved: string[] = []; + const configurationRemoved: Set = new Set(); - const { value: parsedConfiguration, error: configurationError } = Joi.validate( - mappingsConfiguration, - mappingsConfigurationSchema, - { - stripUnknown: true, - abortEarly: false, - } - ); + let copyOfMappingsConfig = { ...mappingsConfiguration }; + const result = mappingsConfigurationSchema.decode(mappingsConfiguration); - if (configurationError) { + if (isLeft(result)) { /** * To keep the logic simple we will strip out the parameters that contain errors */ - configurationError.details.forEach(error => { + const errors = ErrorReporter.report(result); + errors.forEach(error => { const configurationName = error.path[0]; - configurationRemoved.push(configurationName); - delete parsedConfiguration[configurationName]; + configurationRemoved.add(configurationName); + delete copyOfMappingsConfig[configurationName]; }); } - const errors: MappingsValidationError[] = configurationRemoved.map(configName => ({ - code: 'ERR_CONFIG', - configName, - })); + copyOfMappingsConfig = pick(copyOfMappingsConfig, mappingsConfigurationSchemaKeys); + + const errors: MappingsValidationError[] = toArray(ordString)(configurationRemoved) + .map(configName => ({ + code: 'ERR_CONFIG', + configName, + })) + .sort((a, b) => a.configName.localeCompare(b.configName)) as MappingsValidationError[]; - return { value: parsedConfiguration, errors }; + return { value: copyOfMappingsConfig, errors }; }; export const validateMappings = (mappings: any = {}): MappingsValidatorResponse => {