From d1a99ea6eec614d7edf6441b5b79c0b1bbda0123 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 4 Oct 2019 12:48:05 +0200 Subject: [PATCH] Convert uiSettings service to TypeScript (#47018) * tsify is_config_version_upgradeable * tsify get_upgradeable_config * tsify create_or_upgrade_saved_config * tsify ui_settings_service * tsify ui_settings_service_factory * tsify ui_settings_service_for_request * declare types on server object * tsify set route * tsify set_many route * tsify get route * tsify delete route * tsify logWithMetadata * improve ui_settings_service typings * introduce uiService mocks * remove private methods from public contract * add types for server.uiSettingsServiceFactory * rename IUiSettingsService --> IUiSettingsClient --- src/legacy/server/kbn_server.d.ts | 7 +- .../ui_settings/create_objects_client_stub.ts | 8 + .../create_or_upgrade_saved_config.test.ts | 25 ++- ...g.js => create_or_upgrade_saved_config.ts} | 49 ++--- ...le_config.js => get_upgradeable_config.ts} | 16 +- .../{index.js => index.ts} | 0 .../create_or_upgrade.test.ts | 1 - .../is_config_version_upgradeable.test.ts | 1 - ...le.js => is_config_version_upgradeable.ts} | 8 +- .../ui_settings_mixin.test.ts | 12 +- .../routes/{delete.js => delete.ts} | 11 +- .../ui/ui_settings/routes/{get.js => get.ts} | 9 +- .../ui_settings/routes/{index.js => index.ts} | 0 .../routes/integration_tests/lib/servers.ts | 3 +- .../ui/ui_settings/routes/{set.js => set.ts} | 30 +-- .../routes/{set_many.js => set_many.ts} | 24 ++- .../ui_settings/ui_settings_service.mock.ts | 40 ++++ .../ui_settings/ui_settings_service.test.ts | 8 +- ...ings_service.js => ui_settings_service.ts} | 180 ++++++++++++------ ...tory.js => ui_settings_service_factory.ts} | 35 ++-- ....js => ui_settings_service_for_request.ts} | 21 +- .../server/lib/helpers/setup_request.test.ts | 9 +- 22 files changed, 325 insertions(+), 172 deletions(-) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{create_or_upgrade_saved_config.js => create_or_upgrade_saved_config.ts} (55%) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{get_upgradeable_config.js => get_upgradeable_config.ts} (80%) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{index.js => index.ts} (100%) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{is_config_version_upgradeable.js => is_config_version_upgradeable.ts} (89%) rename src/legacy/ui/ui_settings/routes/{delete.js => delete.ts} (81%) rename src/legacy/ui/ui_settings/routes/{get.js => get.ts} (84%) rename src/legacy/ui/ui_settings/routes/{index.js => index.ts} (100%) rename src/legacy/ui/ui_settings/routes/{set.js => set.ts} (70%) rename src/legacy/ui/ui_settings/routes/{set_many.js => set_many.ts} (73%) create mode 100644 src/legacy/ui/ui_settings/ui_settings_service.mock.ts rename src/legacy/ui/ui_settings/{ui_settings_service.js => ui_settings_service.ts} (52%) rename src/legacy/ui/ui_settings/{ui_settings_service_factory.js => ui_settings_service_factory.ts} (59%) rename src/legacy/ui/ui_settings/{ui_settings_service_for_request.js => ui_settings_service_for_request.ts} (74%) diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 69bf95e57cab9..b3e7078d8b5a9 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -39,6 +39,8 @@ import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/ela import { CapabilitiesModifier } from './capabilities'; import { IndexPatternsServiceFactory } from './index_patterns'; import { Capabilities } from '../../core/public'; +import { IUiSettingsClient } from '../../legacy/ui/ui_settings/ui_settings_service'; +import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; export interface KibanaConfig { get(key: string): T; @@ -77,14 +79,15 @@ declare module 'hapi' { name: string, factoryFn: (request: Request) => Record ) => void; - uiSettingsServiceFactory: (options: any) => any; + uiSettingsServiceFactory: (options?: UiSettingsServiceFactoryOptions) => IUiSettingsClient; + logWithMetadata: (tags: string[], message: string, meta: Record) => void; } interface Request { getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract; getBasePath(): string; getDefaultRoute(): Promise; - getUiSettingsService(): any; + getUiSettingsService(): IUiSettingsClient; getCapabilities(): Promise; } diff --git a/src/legacy/ui/ui_settings/create_objects_client_stub.ts b/src/legacy/ui/ui_settings/create_objects_client_stub.ts index ebbedb761fae9..ad19b5c8bc7cf 100644 --- a/src/legacy/ui/ui_settings/create_objects_client_stub.ts +++ b/src/legacy/ui/ui_settings/create_objects_client_stub.ts @@ -26,6 +26,10 @@ export interface SavedObjectsClientStub { update: sinon.SinonStub; get: sinon.SinonStub; create: sinon.SinonStub; + bulkCreate: sinon.SinonStub; + bulkGet: sinon.SinonStub; + delete: sinon.SinonStub; + find: sinon.SinonStub; errors: typeof savedObjectsClientErrors; } @@ -35,6 +39,10 @@ export function createObjectsClientStub(esDocSource = {}): SavedObjectsClientStu get: sinon.stub().returns({ attributes: esDocSource }), create: sinon.stub(), errors: savedObjectsClientErrors, + bulkCreate: sinon.stub(), + bulkGet: sinon.stub(), + delete: sinon.stub(), + find: sinon.stub(), }; return savedObjectsClient; } diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 9b9a2fad39aca..654c0fbb66c8b 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -21,9 +21,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import Chance from 'chance'; -// @ts-ignore import * as getUpgradeableConfigNS from './get_upgradeable_config'; -// @ts-ignore import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; const chance = new Chance(); @@ -45,7 +43,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { id: options.id, version: 'foo', })), - }; + } as any; // mute until we have savedObjects mocks async function run(options = {}) { const resp = await createOrUpgradeSavedConfig({ @@ -103,7 +101,12 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { [chance.word()]: chance.sentence(), }; - getUpgradeableConfig.returns({ id: prevVersion, attributes: savedAttributes }); + getUpgradeableConfig.resolves({ + id: prevVersion, + attributes: savedAttributes, + type: '', + references: [], + }); await run(); @@ -125,7 +128,12 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('should log a message for upgrades', async () => { const { getUpgradeableConfig, logWithMetadata, run } = setup(); - getUpgradeableConfig.returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); + getUpgradeableConfig.resolves({ + id: prevVersion, + attributes: { buildNum: buildNum - 100 }, + type: '', + references: [], + }); await run(); sinon.assert.calledOnce(logWithMetadata); @@ -143,7 +151,12 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { it('does not log when upgrade fails', async () => { const { getUpgradeableConfig, logWithMetadata, run, savedObjectsClient } = setup(); - getUpgradeableConfig.returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); + getUpgradeableConfig.resolves({ + id: prevVersion, + attributes: { buildNum: buildNum - 100 }, + type: '', + references: [], + }); savedObjectsClient.create.callsFake(async () => { throw new Error('foo'); diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts similarity index 55% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index c175e583ee916..0dc3d5f50e97e 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -18,37 +18,38 @@ */ import { defaults } from 'lodash'; +import { SavedObjectsClientContract, SavedObjectAttribute } from 'src/core/server'; +import { Legacy } from 'kibana'; import { getUpgradeableConfig } from './get_upgradeable_config'; -export async function createOrUpgradeSavedConfig(options) { - const { - savedObjectsClient, - version, - buildNum, - logWithMetadata, - onWriteError, - } = options; +interface Options { + savedObjectsClient: SavedObjectsClientContract; + version: string; + buildNum: number; + logWithMetadata: Legacy.Server['logWithMetadata']; + onWriteError?: ( + error: Error, + attributes: Record + ) => Record | undefined; +} +export async function createOrUpgradeSavedConfig( + options: Options +): Promise | undefined> { + const { savedObjectsClient, version, buildNum, logWithMetadata, onWriteError } = options; // try to find an older config we can upgrade const upgradeableConfig = await getUpgradeableConfig({ savedObjectsClient, - version + version, }); // default to the attributes of the upgradeableConfig if available - const attributes = defaults( - { buildNum }, - upgradeableConfig ? upgradeableConfig.attributes : {} - ); + const attributes = defaults({ buildNum }, upgradeableConfig ? upgradeableConfig.attributes : {}); try { // create the new SavedConfig - await savedObjectsClient.create( - 'config', - attributes, - { id: version } - ); + await savedObjectsClient.create('config', attributes, { id: version }); } catch (error) { if (onWriteError) { return onWriteError(error, attributes); @@ -58,9 +59,13 @@ export async function createOrUpgradeSavedConfig(options) { } if (upgradeableConfig) { - logWithMetadata(['plugin', 'elasticsearch'], `Upgrade config from ${upgradeableConfig.id} to ${version}`, { - prevVersion: upgradeableConfig.id, - newVersion: version - }); + logWithMetadata( + ['plugin', 'elasticsearch'], + `Upgrade config from ${upgradeableConfig.id} to ${version}`, + { + prevVersion: upgradeableConfig.id, + newVersion: version, + } + ); } } diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts similarity index 80% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index 1108a01167580..350137a81a49b 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.js +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { SavedObjectsClientContract } from 'src/core/server'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; /** @@ -26,18 +26,22 @@ import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; * @property {string} version * @return {Promise} */ -export async function getUpgradeableConfig({ savedObjectsClient, version }) { +export async function getUpgradeableConfig({ + savedObjectsClient, + version, +}: { + savedObjectsClient: SavedObjectsClientContract; + version: string; +}) { // attempt to find a config we can upgrade const { saved_objects: savedConfigs } = await savedObjectsClient.find({ type: 'config', page: 1, perPage: 1000, sortField: 'buildNum', - sortOrder: 'desc' + sortOrder: 'desc', }); // try to find a config that we can upgrade - return savedConfigs.find(savedConfig => ( - isConfigVersionUpgradeable(savedConfig.id, version) - )); + return savedConfigs.find(savedConfig => isConfigVersionUpgradeable(savedConfig.id, version)); } diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/index.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/index.ts similarity index 100% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/index.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/index.ts diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index 7d5f4e970638d..753e73058af2f 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -24,7 +24,6 @@ import { SavedObjectsClientContract } from 'src/core/server'; import KbnServer from '../../../../server/kbn_server'; import { createTestServers } from '../../../../../test_utils/kbn_server'; -// @ts-ignore import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; describe('createOrUpgradeSavedConfig()', () => { diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts index 91231da968227..6bb2cb3b87850 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts @@ -19,7 +19,6 @@ import expect from '@kbn/expect'; -// @ts-ignore import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; // @ts-ignore import { pkg } from '../../../utils'; diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts similarity index 89% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts index beeba6717f24a..8359f02ffee74 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.js +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts @@ -20,14 +20,12 @@ import semver from 'semver'; const rcVersionRegex = /^(\d+\.\d+\.\d+)\-rc(\d+)$/i; -function extractRcNumber(version) { +function extractRcNumber(version: string): [string, number] { const match = version.match(rcVersionRegex); - return match - ? [match[1], parseInt(match[2], 10)] - : [version, Infinity]; + return match ? [match[1], parseInt(match[2], 10)] : [version, Infinity]; } -export function isConfigVersionUpgradeable(savedVersion, kibanaVersion) { +export function isConfigVersionUpgradeable(savedVersion: string, kibanaVersion: string): boolean { if ( typeof savedVersion !== 'string' || typeof kibanaVersion !== 'string' || diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts index f522f119a26cc..f43c6436d1c33 100644 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts @@ -23,9 +23,7 @@ import expect from '@kbn/expect'; // @ts-ignore import { Config } from '../../../server/config'; -// @ts-ignore import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; -// @ts-ignore import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request'; // @ts-ignore import { uiSettingsMixin } from '../ui_settings_mixin'; @@ -123,7 +121,8 @@ describe('uiSettingsMixin()', () => { foo: 'bar', }); sinon.assert.calledOnce(uiSettingsServiceFactoryStub); - sinon.assert.calledWithExactly(uiSettingsServiceFactoryStub, server, { + sinon.assert.calledWithExactly(uiSettingsServiceFactoryStub, server as any, { + // @ts-ignore foo doesn't exist on Hapi.Server foo: 'bar', overrides: { foo: 'bar', @@ -162,7 +161,12 @@ describe('uiSettingsMixin()', () => { sinon.assert.notCalled(getUiSettingsServiceForRequestStub); const request = {}; decorations.request.getUiSettingsService.call(request); - sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server, request); + sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server as any, request as any, { + overrides: { + foo: 'bar', + }, + getDefaults: sinon.match.func, + }); }); }); diff --git a/src/legacy/ui/ui_settings/routes/delete.js b/src/legacy/ui/ui_settings/routes/delete.ts similarity index 81% rename from src/legacy/ui/ui_settings/routes/delete.js rename to src/legacy/ui/ui_settings/routes/delete.ts index 78e07bbceab01..7825204e6b99b 100644 --- a/src/legacy/ui/ui_settings/routes/delete.js +++ b/src/legacy/ui/ui_settings/routes/delete.ts @@ -16,21 +16,22 @@ * specific language governing permissions and limitations * under the License. */ +import { Legacy } from 'kibana'; -async function handleRequest(request) { +async function handleRequest(request: Legacy.Request) { const { key } = request.params; const uiSettings = request.getUiSettingsService(); await uiSettings.remove(key); return { - settings: await uiSettings.getUserProvided() + settings: await uiSettings.getUserProvided(), }; } export const deleteRoute = { path: '/api/kibana/settings/{key}', method: 'DELETE', - handler: async (request, h) => { - return h.response(await handleRequest(request)); - } + handler: async (request: Legacy.Request) => { + return await handleRequest(request); + }, }; diff --git a/src/legacy/ui/ui_settings/routes/get.js b/src/legacy/ui/ui_settings/routes/get.ts similarity index 84% rename from src/legacy/ui/ui_settings/routes/get.js rename to src/legacy/ui/ui_settings/routes/get.ts index 7e91bc46596b5..3e165a12522bb 100644 --- a/src/legacy/ui/ui_settings/routes/get.js +++ b/src/legacy/ui/ui_settings/routes/get.ts @@ -16,18 +16,19 @@ * specific language governing permissions and limitations * under the License. */ +import { Legacy } from 'kibana'; -async function handleRequest(request) { +async function handleRequest(request: Legacy.Request) { const uiSettings = request.getUiSettingsService(); return { - settings: await uiSettings.getUserProvided() + settings: await uiSettings.getUserProvided(), }; } export const getRoute = { path: '/api/kibana/settings', method: 'GET', - handler: function (request) { + handler(request: Legacy.Request) { return handleRequest(request); - } + }, }; diff --git a/src/legacy/ui/ui_settings/routes/index.js b/src/legacy/ui/ui_settings/routes/index.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/index.js rename to src/legacy/ui/ui_settings/routes/index.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts b/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts index 5b0fbf5a5f256..b076a2a86e166 100644 --- a/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts +++ b/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts @@ -23,6 +23,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import KbnServer from '../../../../../server/kbn_server'; import { createTestServers } from '../../../../../../test_utils/kbn_server'; import { CallCluster } from '../../../../../../legacy/core_plugins/elasticsearch'; +import { IUiSettingsClient } from '../../../ui_settings_service'; let kbnServer: KbnServer; let servers: ReturnType; @@ -33,7 +34,7 @@ interface AllServices { kbnServer: KbnServer; savedObjectsClient: SavedObjectsClientContract; callCluster: CallCluster; - uiSettings: any; + uiSettings: IUiSettingsClient; deleteKibanaIndex: typeof deleteKibanaIndex; } diff --git a/src/legacy/ui/ui_settings/routes/set.js b/src/legacy/ui/ui_settings/routes/set.ts similarity index 70% rename from src/legacy/ui/ui_settings/routes/set.js rename to src/legacy/ui/ui_settings/routes/set.ts index e50c9bf08de3e..1f1ab17a0daf7 100644 --- a/src/legacy/ui/ui_settings/routes/set.js +++ b/src/legacy/ui/ui_settings/routes/set.ts @@ -16,18 +16,18 @@ * specific language governing permissions and limitations * under the License. */ - +import { Legacy } from 'kibana'; import Joi from 'joi'; -async function handleRequest(request) { +async function handleRequest(request: Legacy.Request) { const { key } = request.params; - const { value } = request.payload; + const { value } = request.payload as any; const uiSettings = request.getUiSettingsService(); await uiSettings.set(key, value); return { - settings: await uiSettings.getUserProvided() + settings: await uiSettings.getUserProvided(), }; } @@ -36,16 +36,20 @@ export const setRoute = { method: 'POST', config: { validate: { - params: Joi.object().keys({ - key: Joi.string().required(), - }).default(), + params: Joi.object() + .keys({ + key: Joi.string().required(), + }) + .default(), - payload: Joi.object().keys({ - value: Joi.any().required() - }).required() + payload: Joi.object() + .keys({ + value: Joi.any().required(), + }) + .required(), }, - handler(request) { + handler(request: Legacy.Request) { return handleRequest(request); - } - } + }, + }, }; diff --git a/src/legacy/ui/ui_settings/routes/set_many.js b/src/legacy/ui/ui_settings/routes/set_many.ts similarity index 73% rename from src/legacy/ui/ui_settings/routes/set_many.js rename to src/legacy/ui/ui_settings/routes/set_many.ts index 8e7882f48ef70..18b1046417fec 100644 --- a/src/legacy/ui/ui_settings/routes/set_many.js +++ b/src/legacy/ui/ui_settings/routes/set_many.ts @@ -16,17 +16,17 @@ * specific language governing permissions and limitations * under the License. */ - +import { Legacy } from 'kibana'; import Joi from 'joi'; -async function handleRequest(request) { - const { changes } = request.payload; +async function handleRequest(request: Legacy.Request) { + const { changes } = request.payload as any; const uiSettings = request.getUiSettingsService(); await uiSettings.setMany(changes); return { - settings: await uiSettings.getUserProvided() + settings: await uiSettings.getUserProvided(), }; } @@ -35,12 +35,16 @@ export const setManyRoute = { method: 'POST', config: { validate: { - payload: Joi.object().keys({ - changes: Joi.object().unknown(true).required() - }).required() + payload: Joi.object() + .keys({ + changes: Joi.object() + .unknown(true) + .required(), + }) + .required(), }, - handler(request) { + handler(request: Legacy.Request) { return handleRequest(request); - } - } + }, + }, }; diff --git a/src/legacy/ui/ui_settings/ui_settings_service.mock.ts b/src/legacy/ui/ui_settings/ui_settings_service.mock.ts new file mode 100644 index 0000000000000..7c1a17ebd447c --- /dev/null +++ b/src/legacy/ui/ui_settings/ui_settings_service.mock.ts @@ -0,0 +1,40 @@ +/* + * 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 { IUiSettingsClient } from './ui_settings_service'; + +const createServiceMock = () => { + const mocked: jest.Mocked = { + getDefaults: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + getUserProvided: jest.fn(), + setMany: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + removeMany: jest.fn(), + isOverridden: jest.fn(), + }; + mocked.get.mockResolvedValue(false); + return mocked; +}; + +export const uiSettingsServiceMock = { + create: createServiceMock, +}; diff --git a/src/legacy/ui/ui_settings/ui_settings_service.test.ts b/src/legacy/ui/ui_settings/ui_settings_service.test.ts index bb407d7b3a91b..f37076b27ad6f 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service.test.ts +++ b/src/legacy/ui/ui_settings/ui_settings_service.test.ts @@ -21,9 +21,7 @@ import expect from '@kbn/expect'; import Chance from 'chance'; import sinon from 'sinon'; -// @ts-ignore import { UiSettingsService } from './ui_settings_service'; -// @ts-ignore import * as createOrUpgradeSavedConfigNS from './create_or_upgrade_saved_config/create_or_upgrade_saved_config'; import { createObjectsClientStub, savedObjectsClientErrors } from './create_objects_client_stub'; @@ -43,7 +41,7 @@ describe('ui settings', () => { const sandbox = sinon.createSandbox(); function setup(options: SetupOptions = {}) { - const { getDefaults, defaults = {}, overrides, esDocSource = {} } = options; + const { getDefaults, defaults = {}, overrides = {}, esDocSource = {} } = options; const savedObjectsClient = createObjectsClientStub(esDocSource); @@ -233,7 +231,7 @@ describe('ui settings', () => { }); try { - await uiSettings.setMany(['bar', 'foo']); + await uiSettings.setMany({ baz: 'baz', foo: 'foo' }); } catch (error) { expect(error.message).to.be('Unable to update "foo" because it is overridden'); } @@ -489,7 +487,7 @@ describe('ui settings', () => { it('pulls user configuration from ES', async () => { const esDocSource = {}; const { uiSettings, assertGetQuery } = setup({ esDocSource }); - await uiSettings.get(); + await uiSettings.get('any'); assertGetQuery(); }); diff --git a/src/legacy/ui/ui_settings/ui_settings_service.js b/src/legacy/ui/ui_settings/ui_settings_service.ts similarity index 52% rename from src/legacy/ui/ui_settings/ui_settings_service.js rename to src/legacy/ui/ui_settings/ui_settings_service.ts index 9f79ed2dbe168..57312140b16b3 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service.js +++ b/src/legacy/ui/ui_settings/ui_settings_service.ts @@ -16,28 +16,77 @@ * specific language governing permissions and limitations * under the License. */ - +import { Legacy } from 'kibana'; import { defaultsDeep } from 'lodash'; import Boom from 'boom'; +import { SavedObjectsClientContract, SavedObjectAttribute } from 'src/core/server'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; +export interface UiSettingsServiceOptions { + type: string; + id: string; + buildNum: number; + savedObjectsClient: SavedObjectsClientContract; + overrides?: Record; + getDefaults?: () => Record; + logWithMetadata?: Legacy.Server['logWithMetadata']; +} + +interface ReadOptions { + ignore401Errors?: boolean; + autoCreateOrUpgradeIfMissing?: boolean; +} + +interface UserProvidedValue { + userValue?: SavedObjectAttribute; + isOverridden?: boolean; +} + +type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; + +type UserProvided = Record; +type UiSettingsRaw = Record; + +type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; + +interface UiSettingsParams { + name: string; + value: SavedObjectAttribute; + description: string; + category: string[]; + options?: string[]; + optionLabels?: Record; + requiresPageReload?: boolean; + readonly?: boolean; + type?: UiSettingsType; +} + +export interface IUiSettingsClient { + getDefaults: () => Promise>; + get: (key: string) => Promise; + getAll: () => Promise>; + getUserProvided: () => Promise; + setMany: (changes: Record) => Promise; + set: (key: string, value: T) => Promise; + remove: (key: string) => Promise; + removeMany: (keys: string[]) => Promise; + isOverridden: (key: string) => boolean; +} /** * Service that provides access to the UiSettings stored in elasticsearch. * @class UiSettingsService */ -export class UiSettingsService { - /** - * @constructor - * @param {Object} options - * @property {string} options.type type of SavedConfig object - * @property {string} options.id id of SavedConfig object - * @property {number} options.buildNum - * @property {SavedObjectsClient} options.savedObjectsClient - * @property {Function} [options.getDefaults] - * @property {Function} [options.log] - */ - constructor(options) { +export class UiSettingsService implements IUiSettingsClient { + private readonly _type: UiSettingsServiceOptions['type']; + private readonly _id: UiSettingsServiceOptions['id']; + private readonly _buildNum: UiSettingsServiceOptions['buildNum']; + private readonly _savedObjectsClient: UiSettingsServiceOptions['savedObjectsClient']; + private readonly _overrides: NonNullable; + private readonly _getDefaults: NonNullable; + private readonly _logWithMetadata: NonNullable; + + constructor(options: UiSettingsServiceOptions) { const { type, id, @@ -65,36 +114,38 @@ export class UiSettingsService { } // returns a Promise for the value of the requested setting - async get(key) { + async get(key: string): Promise { const all = await this.getAll(); return all[key]; } - async getAll() { + async getAll() { const raw = await this.getRaw(); - return Object.keys(raw) - .reduce((all, key) => { + return Object.keys(raw).reduce( + (all, key) => { const item = raw[key]; - const hasUserValue = 'userValue' in item; - all[key] = hasUserValue ? item.userValue : item.value; + all[key] = ('userValue' in item ? item.userValue : item.value) as T; return all; - }, {}); + }, + {} as Record + ); } - async getRaw() { + // NOTE: should be a private method + async getRaw(): Promise { const userProvided = await this.getUserProvided(); return defaultsDeep(userProvided, await this.getDefaults()); } - async getUserProvided(options) { - const userProvided = {}; + async getUserProvided(options: ReadOptions = {}): Promise { + const userProvided: UserProvided = {}; // write the userValue for each key stored in the saved object that is not overridden for (const [key, userValue] of Object.entries(await this._read(options))) { if (userValue !== null && !this.isOverridden(key)) { userProvided[key] = { - userValue + userValue, }; } } @@ -102,45 +153,51 @@ export class UiSettingsService { // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object for (const [key, userValue] of Object.entries(this._overrides)) { - userProvided[key] = userValue === null - ? { isOverridden: true } - : { isOverridden: true, userValue }; + userProvided[key] = + userValue === null ? { isOverridden: true } : { isOverridden: true, userValue }; } return userProvided; } - async setMany(changes) { + async setMany(changes: Record) { await this._write({ changes }); } - async set(key, value) { + async set(key: string, value: T) { await this.setMany({ [key]: value }); } - async remove(key) { + async remove(key: string) { await this.set(key, null); } - async removeMany(keys) { - const changes = {}; + async removeMany(keys: string[]) { + const changes: Record = {}; keys.forEach(key => { changes[key] = null; }); await this.setMany(changes); } - isOverridden(key) { + isOverridden(key: string) { return this._overrides.hasOwnProperty(key); } - assertUpdateAllowed(key) { + // NOTE: should be private method + assertUpdateAllowed(key: string) { if (this.isOverridden(key)) { throw Boom.badRequest(`Unable to update "${key}" because it is overridden`); } } - async _write({ changes, autoCreateOrUpgradeIfMissing = true }) { + private async _write({ + changes, + autoCreateOrUpgradeIfMissing = true, + }: { + changes: Record; + autoCreateOrUpgradeIfMissing?: boolean; + }) { for (const key of Object.keys(changes)) { this.assertUpdateAllowed(key); } @@ -162,72 +219,77 @@ export class UiSettingsService { await this._write({ changes, - autoCreateOrUpgradeIfMissing: false + autoCreateOrUpgradeIfMissing: false, }); } } - async _read(options = {}) { - const { - ignore401Errors = false, - autoCreateOrUpgradeIfMissing = true - } = options; - + private async _read({ + ignore401Errors = false, + autoCreateOrUpgradeIfMissing = true, + }: ReadOptions = {}): Promise> { const { isConflictError, isNotFoundError, isForbiddenError, - isEsUnavailableError, isNotAuthorizedError, } = this._savedObjectsClient.errors; - const isIgnorableError = error => ( - isForbiddenError(error) || - isEsUnavailableError(error) || - (ignore401Errors && isNotAuthorizedError(error)) - ); - try { const resp = await this._savedObjectsClient.get(this._type, this._id); return resp.attributes; } catch (error) { if (isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { - const failedUpgradeAttributes = await createOrUpgradeSavedConfig({ + const failedUpgradeAttributes = await createOrUpgradeSavedConfig({ savedObjectsClient: this._savedObjectsClient, version: this._id, buildNum: this._buildNum, logWithMetadata: this._logWithMetadata, - async onWriteError(error, attributes) { - if (isConflictError(error)) { + onWriteError(writeError, attributes) { + if (isConflictError(writeError)) { // trigger `!failedUpgradeAttributes` check below, since another // request caused the uiSettings object to be created so we can // just re-read - return false; + return; } - if (isNotAuthorizedError(error) || isForbiddenError(error)) { + if (isNotAuthorizedError(writeError) || isForbiddenError(writeError)) { return attributes; } - throw error; - } + throw writeError; + }, }); if (!failedUpgradeAttributes) { return await this._read({ - ...options, - autoCreateOrUpgradeIfMissing: false + ignore401Errors, + autoCreateOrUpgradeIfMissing: false, }); } return failedUpgradeAttributes; } - if (isIgnorableError(error)) { + if (this.isIgnorableError(error, ignore401Errors)) { return {}; } throw error; } } + + private isIgnorableError(error: Error, ignore401Errors: boolean) { + const { + isForbiddenError, + isEsUnavailableError, + isNotAuthorizedError, + } = this._savedObjectsClient.errors; + + return ( + isForbiddenError(error) || + isEsUnavailableError(error) || + (ignore401Errors && isNotAuthorizedError(error)) + ); + } } diff --git a/src/legacy/ui/ui_settings/ui_settings_service_factory.js b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts similarity index 59% rename from src/legacy/ui/ui_settings/ui_settings_service_factory.js rename to src/legacy/ui/ui_settings/ui_settings_service_factory.ts index f83a0d9825557..9e1384494161c 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service_factory.js +++ b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts @@ -16,29 +16,30 @@ * specific language governing permissions and limitations * under the License. */ +import { Legacy } from 'kibana'; +import { + IUiSettingsClient, + UiSettingsService, + UiSettingsServiceOptions, +} from './ui_settings_service'; -import { UiSettingsService } from './ui_settings_service'; - +export type UiSettingsServiceFactoryOptions = Pick< + UiSettingsServiceOptions, + 'savedObjectsClient' | 'getDefaults' | 'overrides' +>; /** * Create an instance of UiSettingsService that will use the - * passed `callCluster` function to communicate with elasticsearch + * passed `savedObjectsClient` to communicate with elasticsearch * - * @param {Hapi.Server} server - * @param {Object} options - * @property {AsyncFunction} options.callCluster function that accepts a method name and - * param object which causes a request via some elasticsearch client - * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about - * the uiSettings. - * @return {UiSettingsService} + * @return {IUiSettingsClient} */ -export function uiSettingsServiceFactory(server, options) { +export function uiSettingsServiceFactory( + server: Legacy.Server, + options: UiSettingsServiceFactoryOptions +): IUiSettingsClient { const config = server.config(); - const { - savedObjectsClient, - getDefaults, - overrides, - } = options; + const { savedObjectsClient, getDefaults, overrides } = options; return new UiSettingsService({ type: 'config', @@ -47,6 +48,6 @@ export function uiSettingsServiceFactory(server, options) { savedObjectsClient, getDefaults, overrides, - logWithMetadata: (...args) => server.logWithMetadata(...args), + logWithMetadata: server.logWithMetadata, }); } diff --git a/src/legacy/ui/ui_settings/ui_settings_service_for_request.js b/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts similarity index 74% rename from src/legacy/ui/ui_settings/ui_settings_service_for_request.js rename to src/legacy/ui/ui_settings/ui_settings_service_for_request.ts index 422c9cc14f833..e265ad5f1e115 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service_for_request.js +++ b/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts @@ -17,8 +17,11 @@ * under the License. */ +import { Legacy } from 'kibana'; import { uiSettingsServiceFactory } from './ui_settings_service_factory'; +import { IUiSettingsClient, UiSettingsServiceOptions } from './ui_settings_service'; +type Options = Pick; /** * Get/create an instance of UiSettingsService bound to a specific request. * Each call is cached (keyed on the request object itself) and subsequent @@ -28,20 +31,20 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; * @param {Hapi.Server} server * @param {Hapi.Request} request * @param {Object} [options={}] - * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about - * the uiSettings. - * @return {UiSettingsService} + + * @return {IUiSettingsClient} */ -export function getUiSettingsServiceForRequest(server, request, options = {}) { - const { - getDefaults, - overrides, - } = options; +export function getUiSettingsServiceForRequest( + server: Legacy.Server, + request: Legacy.Request, + options: Options +): IUiSettingsClient { + const { getDefaults, overrides } = options; const uiSettingsService = uiSettingsServiceFactory(server, { getDefaults, overrides, - savedObjectsClient: request.getSavedObjectsClient() + savedObjectsClient: request.getSavedObjectsClient(), }); return uiSettingsService; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index bd45cec316dfc..91809fbaede03 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -6,6 +6,7 @@ import { Legacy } from 'kibana'; import { setupRequest } from './setup_request'; +import { uiSettingsServiceMock } from '../../../../../../../src/legacy/ui/ui_settings/ui_settings_service.mock'; function getMockRequest() { const callWithRequestSpy = jest.fn(); @@ -125,8 +126,10 @@ describe('setupRequest', () => { it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); + const uiSettingsService = uiSettingsServiceMock.create(); // mock includeFrozen to return false - mockRequest.getUiSettingsService = () => ({ get: async () => false }); + uiSettingsService.get.mockResolvedValue(false); + mockRequest.getUiSettingsService = () => uiSettingsService; const { client } = await setupRequest(mockRequest); await client.search({}); const params = callWithRequestSpy.mock.calls[0][2]; @@ -136,8 +139,10 @@ describe('setupRequest', () => { it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); + const uiSettingsService = uiSettingsServiceMock.create(); // mock includeFrozen to return true - mockRequest.getUiSettingsService = () => ({ get: async () => true }); + uiSettingsService.get.mockResolvedValue(true); + mockRequest.getUiSettingsService = () => uiSettingsService; const { client } = await setupRequest(mockRequest); await client.search({}); const params = callWithRequestSpy.mock.calls[0][2];