import { defaultsDeep , omit } from 'lodash' ; import { SavedObjectsErrorHelpers } from '../saved_objects' ; import { SavedObjectsClientContract } from '../saved_objects/types' ; import { Logger } from '../logging' ; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config' ; import { IUiSettingsClient , UiSettingsParams , PublicUiSettingsParams } from './types' ; import { CannotOverrideError } from './ui_settings_errors' ; export interface UiSettingsServiceOptions { type : string ; id : string ; buildNum : number ; savedObjectsClient : SavedObjectsClientContract ; overrides ? : Record < string , any > ; defaults ? : Record < string , UiSettingsParams > ; log : Logger ; } interface ReadOptions { ignore401Errors ? : boolean ; autoCreateOrUpgradeIfMissing ? : boolean ; } interface UserProvidedValue < T = unknown > { userValue ? : T ; isOverridden ? : boolean ; } type UiSettingsRawValue = UiSettingsParams & UserProvidedValue ; type UserProvided < T = unknown > = Record < string , UserProvidedValue < T > > ; type UiSettingsRaw = Record < string , UiSettingsRawValue > ; export class UiSettingsClient 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 < UiSettingsServiceOptions [ 'overrides' ] > ; private readonly defaults : NonNullable < UiSettingsServiceOptions [ 'defaults' ] > ; private readonly log : Logger ; constructor ( options ) { const { type , id , buildNum , savedObjectsClient , log , defaults = { } , overrides = { } } = options ; this . type = type ; this . id = id ; this . buildNum = buildNum ; this . savedObjectsClient = savedObjectsClient ; this . defaults = defaults ; this . overrides = overrides ; this . log = log ; } getRegistered ( ) { const copiedDefaults = { } ; for ( const [ key , value ] of Object . entries ( this . defaults ) ) { copiedDefaults [ key ] = omit ( value , 'schema' ) ; } return copiedDefaults ; } async get < T = any > ( key ) { const all = await this . getAll ( ) ; return all [ key ] ; } async getAll < T = any > ( ) { const raw = await this . getRaw ( ) ; return Object . keys ( raw ) . reduce ( ( all , key ) => { const item = raw [ key ] ; all [ key ] = ( 'userValue' in item ? item . userValue : item . value ) as T ; return all ; } , { } as Record < string , T > ) ; } async getUserProvided < T = unknown > ( ) { const userProvided = this . onReadHook < T > ( await this . read ( ) ) ; for ( const [ key , value ] of Object . entries ( this . overrides ) ) { userProvided [ key ] = value === null ? { isOverridden : true } : { isOverridden : true , userValue : value } ; } return userProvided ; } async setMany ( changes ) { this . onWriteHook ( changes ) ; await this . write ( { changes } ) ; } async set ( key , value ) { await this . setMany ( { [ key ] : value } ) ; } async remove ( key ) { await this . set ( key , null ) ; } async removeMany ( keys ) { const changes = { } ; keys . forEach ( ( key ) => { changes [ key ] = null ; } ) ; await this . setMany ( changes ) ; } isOverridden ( key ) { return this . overrides . hasOwnProperty ( key ) ; } private assertUpdateAllowed ( key ) { if ( this . isOverridden ( key ) ) { throw new CannotOverrideError ( `Unable to update "${ key }" because it is overridden` ) ; } } private async getRaw ( ) { const userProvided = await this . getUserProvided ( ) ; return defaultsDeep ( userProvided , this . defaults ) ; } private validateKey ( key , value ) { const definition = this . defaults [ key ] ; if ( value === null || definition === undefined ) return ; if ( definition . schema ) { definition . schema . validate ( value , { } , `validation [${ key }]` ) ; } } private onWriteHook ( changes ) { for ( const key of Object . keys ( changes ) ) { this . assertUpdateAllowed ( key ) ; } for ( const [ key , value ] of Object . entries ( changes ) ) { this . validateKey ( key , value ) ; } } private onReadHook < T = unknown > ( values ) { const filteredValues = { } ; for ( const [ key , userValue ] of Object . entries ( values ) ) { if ( userValue === null || this . isOverridden ( key ) ) continue ; try { this . validateKey ( key , userValue ) ; filteredValues [ key ] = { userValue : userValue as T , } ; } catch ( error ) { this . log . warn ( `Ignore invalid UiSettings value. ${ error }.` ) ; } } return filteredValues ; } private async write ( { changes , autoCreateOrUpgradeIfMissing = true , } ) { changes = this . translateChanges ( changes , 'timeline' , 'timelion' ) ; try { await this . savedObjectsClient . update ( this . type , this . id , changes ) ; } catch ( error ) { if ( ! SavedObjectsErrorHelpers . isNotFoundError ( error ) || ! autoCreateOrUpgradeIfMissing ) { throw error ; } await createOrUpgradeSavedConfig ( { savedObjectsClient : this . savedObjectsClient , version : this . id , buildNum : this . buildNum , log : this . log , handleWriteErrors : false , } ) ; await this . write ( { changes , autoCreateOrUpgradeIfMissing : false , } ) ; } } private async read ( { ignore401Errors = false , autoCreateOrUpgradeIfMissing = true , } = { } ) { try { const resp = await this . savedObjectsClient . get < Record < string , any > > ( this . type , this . id ) ; return this . translateChanges ( resp . attributes , 'timelion' , 'timeline' ) ; } catch ( error ) { if ( SavedObjectsErrorHelpers . isNotFoundError ( error ) && autoCreateOrUpgradeIfMissing ) { const failedUpgradeAttributes = await createOrUpgradeSavedConfig ( { savedObjectsClient : this . savedObjectsClient , version : this . id , buildNum : this . buildNum , log : this . log , handleWriteErrors : true , } ) ; if ( ! failedUpgradeAttributes ) { return await this . read ( { ignore401Errors , autoCreateOrUpgradeIfMissing : false , } ) ; } return failedUpgradeAttributes ; } if ( this . isIgnorableError ( error , ignore401Errors ) ) { return { } ; } throw error ; } } private isIgnorableError ( error , ignore401Errors ) { const { isForbiddenError , isOpenSearchUnavailableError , isNotAuthorizedError , } = this . savedObjectsClient . errors ; return ( isForbiddenError ( error ) || isOpenSearchUnavailableError ( error ) || ( ignore401Errors && isNotAuthorizedError ( error ) ) ) ; } private translateChanges ( changes , source , dest ) { return Object . keys ( changes ) . reduce ( ( translatedChanges , key ) => { translatedChanges [ key . replace ( source , dest ) ] = changes [ key ] ; return translatedChanges ; } , { } ) ; } }