diff --git a/src/core/server/config/object_to_config_adapter.test.ts b/src/core/server/config/object_to_config_adapter.test.ts new file mode 100644 index 0000000000000..af41741e6208a --- /dev/null +++ b/src/core/server/config/object_to_config_adapter.test.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ObjectToConfigAdapter } from './object_to_config_adapter'; + +describe('ObjectToConfigAdapter', () => { + describe('#getFlattenedPaths()', () => { + it('considers arrays as final values', () => { + const data = { + string: 'string', + array: ['an', 'array'], + }; + const config = new ObjectToConfigAdapter(data); + + expect(config.getFlattenedPaths()).toEqual(['string', 'array']); + }); + + it('handles nested arrays', () => { + const data = { + string: 'string', + array: ['an', 'array'], + nested: { + number: 12, + array: [{ key: 1 }, { key: 2 }], + }, + }; + const config = new ObjectToConfigAdapter(data); + + expect(config.getFlattenedPaths()).toEqual([ + 'string', + 'array', + 'nested.number', + 'nested.array', + ]); + }); + }); +}); diff --git a/src/core/server/config/object_to_config_adapter.ts b/src/core/server/config/object_to_config_adapter.ts index b6ec772603565..d4c2f73364060 100644 --- a/src/core/server/config/object_to_config_adapter.ts +++ b/src/core/server/config/object_to_config_adapter.ts @@ -19,6 +19,7 @@ import { cloneDeep, get, has, set } from 'lodash'; +import { getFlattenedObject } from '../../utils'; import { Config, ConfigPath } from './'; /** @@ -41,24 +42,10 @@ export class ObjectToConfigAdapter implements Config { } public getFlattenedPaths() { - return [...flattenObjectKeys(this.rawConfig)]; + return Object.keys(getFlattenedObject(this.rawConfig)); } public toRaw() { return cloneDeep(this.rawConfig); } } - -function* flattenObjectKeys( - obj: { [key: string]: any }, - path: string = '' -): IterableIterator { - if (typeof obj !== 'object' || obj === null) { - yield path; - } else { - for (const [key, value] of Object.entries(obj)) { - const newPath = path !== '' ? `${path}.${key}` : key; - yield* flattenObjectKeys(value, newPath); - } - } -} diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index c4452fc6a1209..2106a0748d814 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -200,6 +200,24 @@ describe('getUnusedConfigKeys', () => { ).toEqual(['foo.dolly']); }); + it('handles array values', async () => { + expect( + await getUnusedConfigKeys({ + coreHandledConfigPaths: ['core', 'array'], + pluginSpecs: [], + disabledPluginSpecs: [], + settings: { + core: { + prop: 'value', + array: [1, 2, 3], + }, + array: ['some', 'values'], + }, + legacyConfig: getConfig({}), + }) + ).toEqual([]); + }); + describe('using deprecation', () => { it('should use the plugin deprecations provider', async () => { expect( diff --git a/src/core/utils/get_flattened_object.test.ts b/src/core/utils/get_flattened_object.test.ts new file mode 100644 index 0000000000000..26a4c5f1eb1fa --- /dev/null +++ b/src/core/utils/get_flattened_object.test.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getFlattenedObject } from './get_flattened_object'; + +describe('getFlattenedObject()', () => { + it('throws when rootValue is not an object or is an array', () => { + expect(() => getFlattenedObject(1 as any)).toThrowError(); + expect(() => getFlattenedObject(Infinity as any)).toThrowError(); + expect(() => getFlattenedObject(NaN as any)).toThrowError(); + expect(() => getFlattenedObject(false as any)).toThrowError(); + expect(() => getFlattenedObject(null as any)).toThrowError(); + expect(() => getFlattenedObject(undefined as any)).toThrowError(); + expect(() => getFlattenedObject([])).toThrowError(); + }); + + it('flattens objects', () => { + expect(getFlattenedObject({ a: 'b' })).toEqual({ a: 'b' }); + expect(getFlattenedObject({ a: { b: 'c' } })).toEqual({ 'a.b': 'c' }); + expect(getFlattenedObject({ a: { b: 'c' }, d: { e: 'f' } })).toEqual({ + 'a.b': 'c', + 'd.e': 'f', + }); + }); + + it('does not flatten arrays', () => { + expect(getFlattenedObject({ a: ['b'] })).toEqual({ a: ['b'] }); + expect(getFlattenedObject({ a: { b: ['c', 'd'] } })).toEqual({ 'a.b': ['c', 'd'] }); + }); +}); diff --git a/src/core/utils/get_flattened_object.ts b/src/core/utils/get_flattened_object.ts new file mode 100644 index 0000000000000..ce03793284236 --- /dev/null +++ b/src/core/utils/get_flattened_object.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +function shouldReadKeys(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Flattens a deeply nested object to a map of dot-separated + * paths pointing to all primitive values **and arrays** + * from `rootValue`. + * + * example: + * getFlattenedObject({ a: { b: 1, c: [2,3] } }) + * // => { 'a.b': 1, 'a.c': [2,3] } + * + * @param {Object} rootValue + * @returns {Object} + */ +export function getFlattenedObject(rootValue: Record) { + if (!shouldReadKeys(rootValue)) { + throw new TypeError(`Root value is not flatten-able, received ${rootValue}`); + } + + const result: { [key: string]: any } = {}; + (function flatten(prefix, object) { + for (const [key, value] of Object.entries(object)) { + const path = prefix ? `${prefix}.${key}` : key; + if (shouldReadKeys(value)) { + flatten(path, value); + } else { + result[path] = value; + } + } + })('', rootValue); + return result; +} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 7c8ed481c0a7d..2bb44e987d676 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -28,3 +28,4 @@ export * from './pick'; export * from './promise'; export * from './url'; export * from './unset'; +export * from './get_flattened_object';