import { i18n } from '@osd/i18n' ; import { SavedObjectsClientCommon } from '../..' ; import { createIndexPatternCache } from '.' ; import { IndexPattern } from './index_pattern' ; import { createEnsureDefaultIndexPattern , EnsureDefaultIndexPattern , } from './ensure_default_index_pattern' ; import { OnNotification , OnError , OnUnsupportedTimePattern , UiSettingsCommon , IIndexPatternsApiClient , GetFieldsOptions , IndexPatternSpec , IndexPatternAttributes , FieldSpec , FieldFormatMap , IndexPatternFieldMap , } from '../types' ; import { FieldFormatsStartCommon } from '../../field_formats' ; import { UI_SETTINGS , SavedObject } from '../../../common' ; import { SavedObjectNotFound } from '../../../../opensearch_dashboards_utils/common' ; import { IndexPatternMissingIndices } from '../lib' ; import { findByTitle } from '../utils' ; import { DuplicateIndexPatternError } from '../errors' ; const indexPatternCache = createIndexPatternCache ( ) ; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3 ; const savedObjectType = 'index-pattern' ; export interface IndexPatternSavedObjectAttrs { title : string ; } interface IndexPatternsServiceDeps { uiSettings : UiSettingsCommon ; savedObjectsClient : SavedObjectsClientCommon ; apiClient : IIndexPatternsApiClient ; fieldFormats : FieldFormatsStartCommon ; onNotification : OnNotification ; onError : OnError ; onRedirectNoIndexPattern ? : ( ) => void ; onUnsupportedTimePattern : OnUnsupportedTimePattern ; } export class IndexPatternsService { private config : UiSettingsCommon ; private savedObjectsClient : SavedObjectsClientCommon ; private savedObjectsCache ? : Array < SavedObject < IndexPatternSavedObjectAttrs > > | null ; private apiClient : IIndexPatternsApiClient ; private fieldFormats : FieldFormatsStartCommon ; private onNotification : OnNotification ; private onError : OnError ; private onUnsupportedTimePattern : OnUnsupportedTimePattern ; ensureDefaultIndexPattern : EnsureDefaultIndexPattern ; constructor ( { uiSettings , savedObjectsClient , apiClient , fieldFormats , onNotification , onError , onUnsupportedTimePattern , onRedirectNoIndexPattern = ( ) => { } , } ) { this . apiClient = apiClient ; this . config = uiSettings ; this . savedObjectsClient = savedObjectsClient ; this . fieldFormats = fieldFormats ; this . onNotification = onNotification ; this . onError = onError ; this . onUnsupportedTimePattern = onUnsupportedTimePattern ; this . ensureDefaultIndexPattern = createEnsureDefaultIndexPattern ( uiSettings , onRedirectNoIndexPattern ) ; } /**
     * Refresh cache of index pattern ids and titles
     */ private async refreshSavedObjectsCache ( ) { this . savedObjectsCache = await this . savedObjectsClient . find < IndexPatternSavedObjectAttrs > ( { type : 'index-pattern' , fields : [ 'title' ] , perPage : 10000 , } ) ; } param refresh getIds = async ( refresh = false ) => { if ( ! this . savedObjectsCache || refresh ) { await this . refreshSavedObjectsCache ( ) ; } if ( ! this . savedObjectsCache ) { return [ ] ; } return this . savedObjectsCache . map ( ( obj ) => obj ?. id ) ; } ; param refresh getTitles = async ( refresh = false ) : Promise < string [ ] > => { if ( ! this . savedObjectsCache || refresh ) { await this . refreshSavedObjectsCache ( ) ; } if ( ! this . savedObjectsCache ) { return [ ] ; } return this . savedObjectsCache . map ( ( obj ) => obj ?. attributes ?. title ) ; } ; param refresh getIdsWithTitle = async ( refresh = false ) : Promise < Array < { id : string ; title : string ; } > > => { if ( ! this . savedObjectsCache || refresh ) { await this . refreshSavedObjectsCache ( ) ; } if ( ! this . savedObjectsCache ) { return [ ] ; } return this . savedObjectsCache . map ( ( obj ) => ( { id : obj ?. id , title : obj ?. attributes ?. title , } ) ) ; } ; param id clearCache = ( id ? ) => { this . savedObjectsCache = null ; if ( id ) { indexPatternCache . clear ( id ) ; } else { indexPatternCache . clearAll ( ) ; } } ; getCache = async ( ) => { if ( ! this . savedObjectsCache ) { await this . refreshSavedObjectsCache ( ) ; } return this . savedObjectsCache ; } ; /**
     * Get default index pattern
     */ getDefault = async ( ) => { const defaultIndexPatternId = await this . config . get ( 'defaultIndex' ) ; if ( defaultIndexPatternId ) { return await this . get ( defaultIndexPatternId ) ; } return null ; } ; param id param force setDefault = async ( id , force = false ) => { if ( force || ! this . config . get ( 'defaultIndex' ) ) { await this . config . set ( 'defaultIndex' , id ) ; } } ; private isFieldRefreshRequired ( specs ? ) { if ( ! specs ) { return true ; } return Object . values ( specs ) . every ( ( spec ) => { const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec ; const hasDocValuesFlag = 'readFromDocValues' in spec ; return ! hasFieldCaps || ! hasDocValuesFlag ; } ) ; } param options getFieldsForWildcard = async ( options = { } ) => { const metaFields = await this . config . get ( UI_SETTINGS . META_FIELDS ) ; return this . apiClient . getFieldsForWildcard ( { pattern : options . pattern , metaFields , type : options . type , params : options . params || { } , } ) ; } ; param options getFieldsForIndexPattern = async ( indexPattern , options = { } ) => this . getFieldsForWildcard ( { pattern : indexPattern . title as string , ... options , type : indexPattern . type , params : indexPattern . typeMeta && indexPattern . typeMeta . params , } ) ; param indexPattern refreshFields = async ( indexPattern ) => { try { const fields = await this . getFieldsForIndexPattern ( indexPattern ) ; const scripted = indexPattern . getScriptedFields ( ) . map ( ( field ) => field . spec ) ; indexPattern . fields . replaceAll ( [ ... fields , ... scripted ] ) ; } catch ( err ) { if ( err instanceof IndexPatternMissingIndices ) { this . onNotification ( { title : ( err as any ) . message , color : 'danger' , iconType : 'alert' } ) ; } this . onError ( err , { title : i18n . translate ( 'data.indexPatterns.fetchFieldErrorTitle' , { defaultMessage : 'Error fetching fields for index pattern {title} (ID: {id})' , values : { id : indexPattern . id , title : indexPattern . title } , } ) , } ) ; } } ; param fields param id param title param options private refreshFieldSpecMap = async ( fields , id , title , options ) => { const scriptdFields = Object . values ( fields ) . filter ( ( field ) => field . scripted ) ; try { const newFields = await this . getFieldsForWildcard ( options ) ; return this . fieldArrayToMap ( [ ... newFields , ... scriptdFields ] ) ; } catch ( err ) { if ( err instanceof IndexPatternMissingIndices ) { this . onNotification ( { title : ( err as any ) . message , color : 'danger' , iconType : 'alert' } ) ; return { } ; } this . onError ( err , { title : i18n . translate ( 'data.indexPatterns.fetchFieldErrorTitle' , { defaultMessage : 'Error fetching fields for index pattern {title} (ID: {id})' , values : { id , title } , } ) , } ) ; } return fields ; } ; param fieldSpecs param fieldFormatMap private addFormatsToFields = ( fieldSpecs , fieldFormatMap ) => { Object . entries ( fieldFormatMap ) . forEach ( ( [ fieldName , value ] ) => { const field = fieldSpecs . find ( ( fld ) => fld . name === fieldName ) ; if ( field ) { field . format = value ; } } ) ; } ; param fields fieldArrayToMap = ( fields ) => fields . reduce < IndexPatternFieldMap > ( ( collector , field ) => { collector [ field . name ] = field ; return collector ; } , { } ) ; param savedObject savedObjectToSpec = ( savedObject ) : IndexPatternSpec => { const { id , version , attributes : { title , timeFieldName , intervalName , fields , sourceFilters , fieldFormatMap , typeMeta , type , } , } = savedObject ; const parsedSourceFilters = sourceFilters ? JSON . parse ( sourceFilters ) : undefined ; const parsedTypeMeta = typeMeta ? JSON . parse ( typeMeta ) : undefined ; const parsedFieldFormatMap = fieldFormatMap ? JSON . parse ( fieldFormatMap ) : { } ; const parsedFields = fields ? JSON . parse ( fields ) : [ ] ; this . addFormatsToFields ( parsedFields , parsedFieldFormatMap ) ; return { id , version , title , intervalName , timeFieldName , sourceFilters : parsedSourceFilters , fields : this . fieldArrayToMap ( parsedFields ) , typeMeta : parsedTypeMeta , type , } ; } ; param id get = async ( id ) : Promise < IndexPattern > => { const cache = indexPatternCache . get ( id ) ; if ( cache ) { return cache ; } const savedObject = await this . savedObjectsClient . get < IndexPatternAttributes > ( savedObjectType , id ) ; if ( ! savedObject . version ) { throw new SavedObjectNotFound ( savedObjectType , id , 'management/opensearch-dashboards/indexPatterns' ) ; } const spec = this . savedObjectToSpec ( savedObject ) ; const { title , type , typeMeta } = spec ; const parsedFieldFormats = savedObject . attributes . fieldFormatMap ? JSON . parse ( savedObject . attributes . fieldFormatMap ) : { } ; const isFieldRefreshRequired = this . isFieldRefreshRequired ( spec . fields ) ; let isSaveRequired = isFieldRefreshRequired ; try { spec . fields = isFieldRefreshRequired ? await this . refreshFieldSpecMap ( spec . fields || { } , id , spec . title as string , { pattern : title , metaFields : await this . config . get ( UI_SETTINGS . META_FIELDS ) , type , params : typeMeta && typeMeta . params , } ) : spec . fields ; } catch ( err ) { isSaveRequired = false ; if ( err instanceof IndexPatternMissingIndices ) { this . onNotification ( { title : ( err as any ) . message , color : 'danger' , iconType : 'alert' , } ) ; } else { this . onError ( err , { title : i18n . translate ( 'data.indexPatterns.fetchFieldErrorTitle' , { defaultMessage : 'Error fetching fields for index pattern {title} (ID: {id})' , values : { id , title } , } ) , } ) ; } } Object . entries ( parsedFieldFormats ) . forEach ( ( [ fieldName , value ] ) => { const field = spec . fields ?. [ fieldName ] ; if ( field ) { field . format = value ; } } ) ; const indexPattern = await this . create ( spec , true ) ; indexPatternCache . set ( id , indexPattern ) ; if ( isSaveRequired ) { try { this . updateSavedObject ( indexPattern ) ; } catch ( err ) { this . onError ( err , { title : i18n . translate ( 'data.indexPatterns.fetchFieldSaveErrorTitle' , { defaultMessage : 'Error saving after fetching fields for index pattern {title} (ID: {id})' , values : { id : indexPattern . id , title : indexPattern . title , } , } ) , } ) ; } } if ( indexPattern . isUnsupportedTimePattern ( ) ) { this . onUnsupportedTimePattern ( { id : indexPattern . id as string , title : indexPattern . title , index : indexPattern . getIndex ( ) , } ) ; } indexPattern . resetOriginalSavedObjectBody ( ) ; return indexPattern ; } ; migrate ( indexPattern , newTitle ) { return this . savedObjectsClient . update < IndexPatternAttributes > ( savedObjectType , indexPattern . id ! , { title : newTitle , intervalName : null , } , { version : indexPattern . version , } ) . then ( ( { attributes : { title , intervalName } } ) => { indexPattern . title = title ; indexPattern . intervalName = intervalName ; } ) . then ( ( ) => this ) ; } param spec param skipFetchFields async create ( spec , skipFetchFields = false ) { const shortDotsEnable = await this . config . get ( UI_SETTINGS . SHORT_DOTS_ENABLE ) ; const metaFields = await this . config . get ( UI_SETTINGS . META_FIELDS ) ; const indexPattern = new IndexPattern ( { spec , savedObjectsClient : this . savedObjectsClient , fieldFormats : this . fieldFormats , shortDotsEnable , metaFields , } ) ; if ( ! skipFetchFields ) { await this . refreshFields ( indexPattern ) ; } return indexPattern ; } find = async ( search , size = 10 ) : Promise < IndexPattern [ ] > => { const savedObjects = await this . savedObjectsClient . find < IndexPatternSavedObjectAttrs > ( { type : 'index-pattern' , fields : [ 'title' ] , search , searchFields : [ 'title' ] , perPage : size , } ) ; const getIndexPatternPromises = savedObjects . map ( async ( savedObject ) => { return await this . get ( savedObject . id ) ; } ) ; return await Promise . all ( getIndexPatternPromises ) ; } ; param spec param override param skipFetchFields async createAndSave ( spec , override = false , skipFetchFields = false ) { const indexPattern = await this . create ( spec , skipFetchFields ) ; await this . createSavedObject ( indexPattern , override ) ; await this . setDefault ( indexPattern . id as string ) ; return indexPattern ; } param indexPattern param override async createSavedObject ( indexPattern , override = false ) { const dupe = await findByTitle ( this . savedObjectsClient , indexPattern . title ) ; if ( dupe ) { if ( override ) { await this . delete ( dupe . id ) ; } else { throw new DuplicateIndexPatternError ( `Duplicate index pattern: ${ indexPattern . title }` ) ; } } const body = indexPattern . getAsSavedObjectBody ( ) ; const response = await this . savedObjectsClient . create ( savedObjectType , body , { id : indexPattern . id , } ) ; indexPattern . id = response . id ; indexPatternCache . set ( indexPattern . id , indexPattern ) ; return indexPattern ; } param indexPattern param saveAttempts async updateSavedObject ( indexPattern , saveAttempts = 0 , ignoreErrors = false ) { if ( ! indexPattern . id ) return ; const body = indexPattern . getAsSavedObjectBody ( ) ; const originalBody = indexPattern . getOriginalSavedObjectBody ( ) ; const originalChangedKeys = [ ] ; Object . entries ( body ) . forEach ( ( [ key , value ] ) => { if ( value !== ( originalBody as any ) [ key ] ) { originalChangedKeys . push ( key ) ; } } ) ; return this . savedObjectsClient . update ( savedObjectType , indexPattern . id , body , { version : indexPattern . version } ) . then ( ( resp ) => { indexPattern . id = resp . id ; indexPattern . version = resp . version ; } ) . catch ( async ( err ) => { if ( err ?. res ?. status === 409 && saveAttempts ++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS ) { const samePattern = await this . get ( indexPattern . id as string ) ; const updatedBody = samePattern . getAsSavedObjectBody ( ) ; const serverChangedKeys = [ ] ; Object . entries ( updatedBody ) . forEach ( ( [ key , value ] ) => { if ( value !== ( body as any ) [ key ] && value !== ( originalBody as any ) [ key ] ) { serverChangedKeys . push ( key ) ; } } ) ; let unresolvedCollision = false ; for ( const originalKey of originalChangedKeys ) { for ( const serverKey of serverChangedKeys ) { if ( originalKey === serverKey ) { unresolvedCollision = true ; break ; } } } if ( unresolvedCollision ) { if ( ignoreErrors ) { return ; } const title = i18n . translate ( 'data.indexPatterns.unableWriteLabel' , { defaultMessage : 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.' , } ) ; this . onNotification ( { title , color : 'danger' } ) ; throw err ; } serverChangedKeys . forEach ( ( key ) => { ( indexPattern as any ) [ key ] = ( samePattern as any ) [ key ] ; } ) ; indexPattern . version = samePattern . version ; indexPatternCache . clear ( indexPattern . id ! ) ; return this . updateSavedObject ( indexPattern , saveAttempts , ignoreErrors ) ; } throw err ; } ) ; } param indexPatternId async delete ( indexPatternId ) { indexPatternCache . clear ( indexPatternId ) ; return this . savedObjectsClient . delete ( 'index-pattern' , indexPatternId ) ; } } export type IndexPatternsContract = PublicMethodsOf < IndexPatternsService > ;