import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey' ; import { isNotFoundFromUnsupportedServer } from '../../../elasticsearch' ; import { LegacyUrlAlias , LEGACY_URL_ALIAS_TYPE } from '../../object_types' ; import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry' ; import type { SavedObjectsRawDoc , SavedObjectsRawDocSource , SavedObjectsSerializer , } from '../../serialization' ; import { findLegacyUrlAliases } from './legacy_url_aliases' ; import { Either , rawDocExistsInNamespaces } from './internal_utils' ; import { getObjectKey , isLeft , isRight } from './internal_utils' ; import type { CreatePointInTimeFinderFn } from './point_in_time_finder' ; import type { RepositoryEsClient } from './repository_es_client' ; import { ALL_NAMESPACES_STRING } from './utils' ; import { SavedObjectsErrorHelpers } from './errors' ; /**
 * If the object will be created in this many spaces (or "*" all current and future spaces), we use find to fetch all aliases.
 * If the object does not exceed this threshold, we use mget to fetch aliases instead.
 * This threshold is a bit arbitrary, but it is intended to optimize so we don't make expensive PIT/find operations unless it is necessary.
 */ const FIND_ALIASES_THRESHOLD = 3 ; internal export const ALIAS_SEARCH_PER_PAGE = 1000 ; export interface PreflightCheckForCreateObject { /** The type of the object. */ type : string ; /** The ID of the object. */ id : string ; /** The namespaces that the consumer intends to create this object in. */ namespaces : string [ ] ; /** Whether or not the object should be overwritten if it would encounter a regular conflict. */ overwrite ? : boolean ; } export interface PreflightCheckForCreateParams { registry : ISavedObjectTypeRegistry ; client : RepositoryEsClient ; serializer : SavedObjectsSerializer ; getIndexForType : ( type : string ) => string ; createPointInTimeFinder : CreatePointInTimeFinderFn ; objects : PreflightCheckForCreateObject [ ] ; } export interface PreflightCheckForCreateResult { /** The type of the object. */ type : string ; /** The ID of the object. */ id : string ; /** Only included if we did not encounter an error _and_ the object was found. */ existingDocument ? : SavedObjectsRawDoc ; /** Only included if we encountered an error. */ error ? : { type : 'aliasConflict' | 'unresolvableConflict' | 'conflict' ; metadata ? : { spacesWithConflictingAliases ? : string [ ] ; isNotOverwritable ? : boolean ; } ; } ; } interface ParsedObject { type : string ; id : string ; overwrite : boolean ; spaces : Set < string > ; } function isMgetDoc ( doc ? ) { return Boolean ( doc && 'found' in doc ) ; } param objects internal export async function preflightCheckForCreate ( params ) { const { registry , client , serializer , getIndexForType , createPointInTimeFinder , objects } = params ; const aliasConflictsOrObjectsToGet = await optionallyFindAliases ( createPointInTimeFinder , objects ) ; const objectsAndAliasesToBulkGet = aliasConflictsOrObjectsToGet . filter ( isRight ) . map ( ( { value } ) => value ) ; const { bulkGetResponse , aliasSpaces } = await bulkGetObjectsAndAliases ( client , serializer , getIndexForType , objectsAndAliasesToBulkGet ) ; let getResponseIndex = 0 ; let aliasSpacesIndex = 0 ; const results = [ ] ; for ( const either of aliasConflictsOrObjectsToGet ) { if ( isLeft ( either ) ) { const { type , id , spacesWithConflictingAliases } = either . value ; const error = { type : 'aliasConflict' as const , metadata : { spacesWithConflictingAliases } , } ; results . push ( { type , id , error } ) ; } else { const { type , id , spaces , overwrite , checkAliases } = either . value ; const objectDoc = bulkGetResponse ?. body . docs [ getResponseIndex ++ ] ! ; if ( checkAliases ) { const spacesWithConflictingAliases = [ ] ; for ( let i = 0 ; i < spaces . size ; i ++ ) { const aliasDoc = bulkGetResponse ?. body . docs [ getResponseIndex ++ ] ; const index = aliasSpacesIndex ++ ; if ( isMgetDoc ( aliasDoc ) && aliasDoc . found ) { const legacyUrlAlias = aliasDoc . _source ! [ LEGACY_URL_ALIAS_TYPE ] ; if ( ! legacyUrlAlias ?. disabled ) { spacesWithConflictingAliases . push ( aliasSpaces [ index ] ) ; } } } if ( spacesWithConflictingAliases . length ) { const error = { type : 'aliasConflict' as const , metadata : { spacesWithConflictingAliases } , } ; results . push ( { type , id , error } ) ; continue ; } } let existingDocument ; if ( isMgetDoc ( objectDoc ) && objectDoc . found ) { if ( ! rawDocExistsInNamespaces ( registry , objectDoc , [ ... spaces ] ) ) { const error = { type : 'unresolvableConflict' as const , metadata : { isNotOverwritable : true } , } ; results . push ( { type , id , error } ) ; continue ; } else if ( ! overwrite ) { const error = { type : 'conflict' as const } ; results . push ( { type , id , error } ) ; continue ; } existingDocument = objectDoc as SavedObjectsRawDoc ; } results . push ( { type , id , existingDocument } ) ; } } return results ; } async function optionallyFindAliases ( createPointInTimeFinder , objects ) { const objectsToGetOrObjectsToFind = objects . map < Either < ParsedObject > > ( ( object ) => { const { type , id , namespaces , overwrite = false } = object ; const spaces = new Set ( namespaces ) ; const tag = spaces . size > FIND_ALIASES_THRESHOLD || spaces . has ( ALL_NAMESPACES_STRING ) ? 'Right' : 'Left' ; return { tag , value : { type , id , overwrite , spaces } } ; } ) ; const objectsToFind = objectsToGetOrObjectsToFind . filter ( isRight ) . map ( ( { value : { type , id } } ) => ( { type , id } ) ) ; const aliasMap = await findLegacyUrlAliases ( createPointInTimeFinder , objectsToFind , ALIAS_SEARCH_PER_PAGE ) ; const result = objectsToGetOrObjectsToFind . map < Either < ParsedObject & { spacesWithConflictingAliases : string [ ] ; } , ParsedObject & { checkAliases : boolean ; } > > ( ( either ) => { let checkAliases = true ; if ( isRight ( either ) ) { const { type , id , spaces } = either . value ; const key = getObjectKey ( { type , id } ) ; const spacesWithMatchingAliases = aliasMap . get ( key ) ; if ( spacesWithMatchingAliases ) { let spacesWithConflictingAliases = [ ] ; if ( spaces . has ( ALL_NAMESPACES_STRING ) ) { spacesWithConflictingAliases = [ ... spacesWithMatchingAliases ] ; } else { spacesWithConflictingAliases = intersection ( spaces , spacesWithMatchingAliases ) ; } if ( spacesWithConflictingAliases . length ) { return { tag : 'Left' , value : { ... either . value , spacesWithConflictingAliases } } ; } } checkAliases = false ; } return { tag : 'Right' , value : { ... either . value , checkAliases } } ; } ) ; return result ; } async function bulkGetObjectsAndAliases ( client , serializer , getIndexForType , objectsAndAliasesToBulkGet ) { const docsToBulkGet = [ ] ; const aliasSpaces = [ ] ; for ( const { type , id , spaces , checkAliases } of objectsAndAliasesToBulkGet ) { docsToBulkGet . push ( { _id : serializer . generateRawId ( undefined , type , id ) , _index : getIndexForType ( type ) , _source : [ 'type' , 'namespaces' ] , } ) ; if ( checkAliases ) { for ( const space of spaces ) { const rawAliasId = serializer . generateRawLegacyUrlAliasId ( space , type , id ) ; docsToBulkGet . push ( { _id : rawAliasId , _index : getIndexForType ( LEGACY_URL_ALIAS_TYPE ) , _source : [ `${ LEGACY_URL_ALIAS_TYPE }.disabled` ] , } ) ; aliasSpaces . push ( space ) ; } } } const bulkGetResponse = docsToBulkGet . length ? await client . mget < SavedObjectsRawDocSource > ( { body : { docs : docsToBulkGet } } , { ignore : [ 404 ] } ) : undefined ; if ( bulkGetResponse && isNotFoundFromUnsupportedServer ( { statusCode : bulkGetResponse . statusCode , headers : bulkGetResponse . headers , } ) ) { throw SavedObjectsErrorHelpers . createGenericNotFoundEsUnavailableError ( ) ; } return { bulkGetResponse , aliasSpaces } ; } function intersection < T > ( a , b ) { const result = [ ] ; for ( const x of a ) { if ( b . has ( x ) ) { result . push ( x ) ; } } return result ; }