import type { SavedObject } from '../../../types' ; import type { KibanaRequest } from '../../http' ; import type { Logger } from '../../logging' ; import { SavedObjectsClientContract , SavedObjectsExportablePredicate } from '../types' ; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry' ; import type { SavedObjectsExportTransform } from './types' ; import { applyExportTransforms } from './apply_export_transforms' ; interface CollectExportedObjectOptions { savedObjectsClient : SavedObjectsClientContract ; objects : SavedObject [ ] ; /** flag to also include all related saved objects in the export stream. */ includeReferences ? : boolean ; /** optional namespace to override the namespace used by the savedObjectsClient. */ namespace ? : string ; /** The http request initiating the export. */ request : KibanaRequest ; /** export transform per type */ typeRegistry : ISavedObjectTypeRegistry ; /** logger to use to log potential errors */ logger : Logger ; } interface CollectExportedObjectResult { objects : SavedObject [ ] ; excludedObjects : ExcludedObject [ ] ; missingRefs : CollectedReference [ ] ; } interface ExcludedObject { id : string ; type : string ; reason : ExclusionReason ; } export type ExclusionReason = 'predicate_error' | 'excluded' ; export const collectExportedObjects = async ( { objects , includeReferences = true , namespace , request , typeRegistry , savedObjectsClient , logger , } ) : Promise < CollectExportedObjectResult > => { const exportTransforms = buildTransforms ( typeRegistry ) ; const isExportable = buildIsExportable ( typeRegistry ) ; const collectedObjects = [ ] ; const collectedMissingRefs = [ ] ; const collectedNonExportableObjects = [ ] ; const alreadyProcessed = new Set ( ) ; let currentObjects = objects ; do { currentObjects = currentObjects . filter ( ( object ) => ! alreadyProcessed . has ( objKey ( object ) ) ) ; const { exportable : untransformedExportableInitialObjects , nonExportable : nonExportableInitialObjects , } = await splitByExportability ( currentObjects , isExportable , logger ) ; collectedNonExportableObjects . push ( ... nonExportableInitialObjects ) ; nonExportableInitialObjects . forEach ( ( obj ) => alreadyProcessed . add ( objKey ( obj ) ) ) ; const transformedObjects = ( await applyExportTransforms ( { request , objects : untransformedExportableInitialObjects , transforms : exportTransforms , } ) ) . filter ( ( object ) => ! alreadyProcessed . has ( objKey ( object ) ) ) ; transformedObjects . forEach ( ( obj ) => alreadyProcessed . add ( objKey ( obj ) ) ) ; const { included : exportableInitialObjects , excluded : additionalObjects } = splitByKeys ( transformedObjects , untransformedExportableInitialObjects . map ( ( obj ) => objKey ( obj ) ) ) ; const { exportable : exportableAdditionalObjects , nonExportable : nonExportableAdditionalObjects , } = await splitByExportability ( additionalObjects , isExportable , logger ) ; const allExportableObjects = [ ... exportableInitialObjects , ... exportableAdditionalObjects ] ; collectedNonExportableObjects . push ( ... nonExportableAdditionalObjects ) ; collectedObjects . push ( ... allExportableObjects ) ; if ( includeReferences ) { const references = collectReferences ( allExportableObjects , alreadyProcessed ) ; if ( references . length ) { const { objects : fetchedObjects , missingRefs } = await fetchReferences ( { references , namespace , client : savedObjectsClient , } ) ; collectedMissingRefs . push ( ... missingRefs ) ; currentObjects = fetchedObjects ; } else { currentObjects = [ ] ; } } else { currentObjects = [ ] ; } } while ( includeReferences && currentObjects . length ) ; return { objects : collectedObjects , excludedObjects : collectedNonExportableObjects , missingRefs : collectedMissingRefs , } ; } ; const objKey = ( obj ) => `${ obj . type }:${ obj . id }` ; type ObjectKey = string ; interface CollectedReference { id : string ; type : string ; } const collectReferences = ( objects , alreadyProcessed ) : CollectedReference [ ] => { const references = new Map ( ) ; objects . forEach ( ( obj ) => { obj . references ?. forEach ( ( ref ) => { const refKey = objKey ( ref ) ; if ( ! alreadyProcessed . has ( refKey ) ) { references . set ( refKey , { type : ref . type , id : ref . id } ) ; } } ) ; } ) ; return [ ... references . values ( ) ] ; } ; interface FetchReferencesResult { objects : SavedObject [ ] ; missingRefs : CollectedReference [ ] ; } const fetchReferences = async ( { references , client , namespace , } ) : Promise < FetchReferencesResult > => { const { saved_objects : savedObjects } = await client . bulkGet ( references , { namespace } ) ; return { objects : savedObjects . filter ( ( obj ) => ! obj . error ) , missingRefs : savedObjects . filter ( ( obj ) => obj . error ) . map ( ( obj ) => ( { type : obj . type , id : obj . id } ) ) , } ; } ; const buildTransforms = ( typeRegistry ) => typeRegistry . getAllTypes ( ) . reduce ( ( transformMap , type ) => { if ( type . management ?. onExport ) { transformMap . set ( type . name , type . management . onExport ) ; } return transformMap ; } , new Map < string , SavedObjectsExportTransform > ( ) ) ; const buildIsExportable = ( typeRegistry ) : SavedObjectsExportablePredicate < any > => { const exportablePerType = typeRegistry . getAllTypes ( ) . reduce ( ( exportableMap , type ) => { if ( type . management ?. isExportable ) { exportableMap . set ( type . name , type . management . isExportable ) ; } return exportableMap ; } , new Map < string , SavedObjectsExportablePredicate > ( ) ) ; return ( obj ) => { const typePredicate = exportablePerType . get ( obj . type ) ; return typePredicate ? typePredicate ( obj ) : true ; } ; } ; const splitByExportability = ( objects , isExportable , logger ) => { const exportableObjects = [ ] ; const nonExportableObjects = [ ] ; objects . forEach ( ( obj ) => { try { const exportable = isExportable ( obj ) ; if ( exportable ) { exportableObjects . push ( obj ) ; } else { nonExportableObjects . push ( { id : obj . id , type : obj . type , reason : 'excluded' , } ) ; } } catch ( e ) { logger . error ( `Error invoking "isExportable" for object ${ obj . type }:${ obj . id }. Error was: ${ e . stack ?? e . message }` ) ; nonExportableObjects . push ( { id : obj . id , type : obj . type , reason : 'predicate_error' , } ) ; } } ) ; return { exportable : exportableObjects , nonExportable : nonExportableObjects , } ; } ; const splitByKeys = ( objects , keys ) => { const included = [ ] ; const excluded = [ ] ; objects . forEach ( ( obj ) => { if ( keys . includes ( objKey ( obj ) ) ) { included . push ( obj ) ; } else { excluded . push ( obj ) ; } } ) ; return { included , excluded , } ; } ;