import pMap from 'p-map' ; import { v4 as uuidv4 } from 'uuid' ; import { SavedObject , SavedObjectsClientContract , SavedObjectsImportError , SavedObjectsImportRetry , } from '../types' ; import { ISavedObjectTypeRegistry } from '..' ; interface CheckOriginConflictsParams { objects : Array < SavedObject < { title ? : string ; } > > ; savedObjectsClient : SavedObjectsClientContract ; typeRegistry : ISavedObjectTypeRegistry ; namespace ? : string ; ignoreRegularConflicts ? : boolean ; importIdMap : Map < string , unknown > ; } type CheckOriginConflictParams = Omit < CheckOriginConflictsParams , 'objects' > & { object : SavedObject < { title ? : string ; } > ; } ; interface GetImportIdMapForRetriesParams { objects : SavedObject [ ] ; retries : SavedObjectsImportRetry [ ] ; createNewCopies : boolean ; } interface InexactMatch < T > { object : SavedObject < T > ; destinations : Array < { id : string ; title ? : string ; updatedAt ? : string ; } > ; } interface Left < T > { tag : 'left' ; value : InexactMatch < T > ; } interface Right < T > { tag : 'right' ; value : SavedObject < T > ; } type Either < T > = Left < T > | Right < T > ; const isLeft = < T > ( object ) : object is Left < T > => object . tag === 'left' ; const MAX_CONCURRENT_SEARCHES = 10 ; const createQueryTerm = ( input ) => input . replace ( /\\/g , '\\\\' ) . replace ( /\"/g , '\\"' ) ; const createQuery = ( type , id , rawIdPrefix ) => `"${ createQueryTerm ( `${ rawIdPrefix }${ type }:${ id }` ) }" | "${ createQueryTerm ( id ) }"` ; const transformObjectsToAmbiguousConflictFields = ( objects ) => objects . map ( ( { id , attributes , updated_at : updatedAt } ) => ( { id , title : attributes ?. title , updatedAt , } ) ) ; const getAmbiguousConflictSourceKey = < T > ( { object } ) => `${ object . type }:${ object . originId || object . id }` ; /**
 * Make a search request for an import object to check if any objects of this type that match this object's `originId` or `id` exist in the
 * specified namespace:
 *  - A `Right` result indicates that no conflict destinations were found in this namespace ("no match").
 *  - A `Left` result indicates that one or more conflict destinations exist in this namespace, none of which exactly match this object's ID
 *    ("inexact match"). We can make this assumption because any "exact match" results would have been obtained and filtered out by the
 *    `checkConflicts` submodule, which is called before this.
 */ const checkOriginConflict = async ( params ) : Promise < Either < { title ? : string ; } > > => { const { object , savedObjectsClient , typeRegistry , namespace , importIdMap } = params ; const importIds = new Set ( importIdMap . keys ( ) ) ; const { type , originId } = object ; if ( ! typeRegistry . isMultiNamespace ( type ) ) { return { tag : 'right' , value : object } ; } const search = createQuery ( type , originId || object . id , namespace ? `${ namespace }:` : '' ) ; const findOptions = { type , search , rootSearchFields : [ '_id' , 'originId' ] , page : 1 , perPage : 10 , fields : [ 'title' ] , sortField : 'updated_at' , sortOrder : 'desc' , ... ( namespace && { namespaces : [ namespace ] } ) , } ; const findResult = await savedObjectsClient . find < { title ? : string ; } > ( findOptions ) ; const { total , saved_objects : savedObjects } = findResult ; if ( total === 0 ) { return { tag : 'right' , value : object } ; } const objects = savedObjects . filter ( ( obj ) => ! importIds . has ( `${ obj . type }:${ obj . id }` ) ) ; const destinations = transformObjectsToAmbiguousConflictFields ( objects ) ; if ( destinations . length === 0 ) { return { tag : 'right' , value : object } ; } return { tag : 'left' , value : { object , destinations } } ; } ; /**
 * This function takes all objects to import, and checks "multi-namespace" types for potential conflicts. An object with a multi-namespace
 * type may include an `originId` field, which means that it should conflict with other objects that originate from the same source.
 * Expected behavior of importing saved objects (single-namespace or multi-namespace):
 *  1. The object 'foo' is exported from space A and imported to space B -- a new object 'bar' is created.
 *  2. Then, the object 'bar' is exported from space B and imported to space C -- a new object 'baz' is created.
 *  3. Then, the object 'baz' is exported from space C to space A -- the object conflicts with 'foo', which must be overwritten to continue.
 * This behavior originated with "single-namespace" types, and this function was added to ensure importing objects of multi-namespace types
 * will behave in the same way.
 *
 * To achieve this behavior for multi-namespace types, a search request is made for each object to determine if any objects of this type
 * that match this object's `originId` or `id` exist in the specified namespace:
 *  - If this is a `Right` result; return the import object and allow `createSavedObjects` to handle the conflict (if any).
 *  - If this is a `Left` "partial match" result:
 *     A. If there is a single source and destination match, add the destination to the importIdMap and return the import object, which
 *        will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during).
 *     B. Otherwise, this is an "ambiguous conflict" result; return an error.
 */ export async function checkOriginConflicts ( { objects , ... params } ) { const mapper = async ( object ) => checkOriginConflict ( { object , ... params } ) ; const checkOriginConflictResults = await pMap ( objects , mapper , { concurrency : MAX_CONCURRENT_SEARCHES , } ) ; const ambiguousConflictSourcesMap = checkOriginConflictResults . filter ( isLeft ) . reduce ( ( acc , cur ) => { const key = getAmbiguousConflictSourceKey ( cur . value ) ; const value = acc . get ( key ) ?? [ ] ; return acc . set ( key , [ ... value , cur . value . object ] ) ; } , new Map < string , Array < SavedObject < { title ? : string ; } > > > ( ) ) ; const errors = [ ] ; const importIdMap = new Map < string , { id : string ; omitOriginId ? : boolean ; } > ( ) ; const pendingOverwrites = new Set < string > ( ) ; checkOriginConflictResults . forEach ( ( result ) => { if ( ! isLeft ( result ) ) { return ; } const key = getAmbiguousConflictSourceKey ( result . value ) ; const sources = transformObjectsToAmbiguousConflictFields ( ambiguousConflictSourcesMap . get ( key ) ! ) ; const { object , destinations } = result . value ; const { type , id , attributes } = object ; if ( sources . length === 1 && destinations . length === 1 ) { if ( params . ignoreRegularConflicts ) { importIdMap . set ( `${ type }:${ id }` , { id : destinations [ 0 ] . id } ) ; pendingOverwrites . add ( `${ type }:${ id }` ) ; } else { const { title } = attributes ; errors . push ( { type , id , title , meta : { title } , error : { type : 'conflict' , destinationId : destinations [ 0 ] . id , } , } ) ; } return ; } if ( sources . length > 1 ) { importIdMap . set ( `${ type }:${ id }` , { id : uuidv4 ( ) , omitOriginId : true } ) ; return ; } const { title } = attributes ; errors . push ( { type , id , title , meta : { title } , error : { type : 'ambiguous_conflict' , destinations , } , } ) ; } ) ; return { errors , importIdMap , pendingOverwrites } ; } /**
 * Assume that all objects exist in the `retries` map (due to filtering at the beginning of `resolveSavedObjectsImportErrors`).
 */ export function getImportIdMapForRetries ( params ) { const { objects , retries , createNewCopies } = params ; const retryMap = retries . reduce ( ( acc , cur ) => acc . set ( `${ cur . type }:${ cur . id }` , cur ) , new Map < string , SavedObjectsImportRetry > ( ) ) ; const importIdMap = new Map < string , { id : string ; omitOriginId ? : boolean ; } > ( ) ; objects . forEach ( ( { type , id } ) => { const retry = retryMap . get ( `${ type }:${ id }` ) ; if ( ! retry ) { throw new Error ( `Retry was expected for "${ type }:${ id }" but not found` ) ; } const { destinationId } = retry ; const omitOriginId = createNewCopies || Boolean ( retry . createNewCopy ) ; if ( destinationId && destinationId !== id ) { importIdMap . set ( `${ type }:${ id }` , { id : destinationId , omitOriginId } ) ; } } ) ; return importIdMap ; }