diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 604cdea1a7fbe..ac8930c52ac5c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -165,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | +| [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) | | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsExportTransformContext](./kibana-plugin-core-server.savedobjectsexporttransformcontext.md) | Context passed down to a [export transform function](./kibana-plugin-core-server.savedobjectsexporttransform.md) | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md new file mode 100644 index 0000000000000..f7b96e71c8e53 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) + +## SavedObjectsExportExcludedObject.id property + +id of the excluded object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md new file mode 100644 index 0000000000000..4766ae25a936d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) + +## SavedObjectsExportExcludedObject interface + + +Signature: + +```typescript +export interface SavedObjectsExportExcludedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) | string | id of the excluded object | +| [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) | string | optional cause of the exclusion | +| [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) | string | type of the excluded object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md new file mode 100644 index 0000000000000..0adb1ba35e696 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) + +## SavedObjectsExportExcludedObject.reason property + +optional cause of the exclusion + +Signature: + +```typescript +reason?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md new file mode 100644 index 0000000000000..be28ac2d0ffb6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) + +## SavedObjectsExportExcludedObject.type property + +type of the excluded object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md new file mode 100644 index 0000000000000..90432bf6d6705 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) > [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) + +## SavedObjectsExportResultDetails.excludedObjects property + +excluded objects details + +Signature: + +```typescript +excludedObjects: SavedObjectsExportExcludedObject[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md new file mode 100644 index 0000000000000..05846e28b9cab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) > [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) + +## SavedObjectsExportResultDetails.excludedObjectsCount property + +number of objects that were excluded from the export + +Signature: + +```typescript +excludedObjectsCount: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md index d98088c5f45be..f017f2329170b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md @@ -16,6 +16,8 @@ export interface SavedObjectsExportResultDetails | Property | Type | Description | | --- | --- | --- | +| [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) | SavedObjectsExportExcludedObject[] | excluded objects details | +| [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) | number | number of objects that were excluded from the export | | [exportedCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects | | [missingRefCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references | | [missingReferences](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{
id: string;
type: string;
}> | missing references details | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md index 50d4c5425e8fd..2effed1ae9d70 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md @@ -11,7 +11,7 @@ A type's export transform function will be executed once per user-initiated expo Signature: ```typescript -export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; +export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 56ebb48707f59..a1bc99ce8d13d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -52,6 +52,6 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | -| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | +| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | <Attributes = any>(type: SavedObjectsType<Attributes>) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index 54e01d3110a2d..7f74ce4d7bea7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -11,7 +11,7 @@ See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdef Signature: ```typescript -registerType: (type: SavedObjectsType) => void; +registerType: (type: SavedObjectsType) => void; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md index fbaf58f959075..d98c553656b1f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md @@ -9,5 +9,5 @@ An optional [saved objects management section](./kibana-plugin-core-server.saved Signature: ```typescript -management?: SavedObjectsTypeManagementDefinition; +management?: SavedObjectsTypeManagementDefinition; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index d882938d731c8..c3aba5261561f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface SavedObjectsType +export interface SavedObjectsType ``` ## Remarks @@ -54,7 +54,7 @@ Example after converting to a multi-namespace (shareable) type in 8.1: Note: migration function(s) can be optionally specified for any of these versions and will not interfere with the conversion process. | | [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md). | | [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | string | If defined, the type instances will be stored in the given index instead of the default one. | -| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | +| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition<Attributes> | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | | [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | | [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | (() => SavedObjectMigrationMap) | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | | [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md index f5488d8f0310d..75f820d7a8e56 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md @@ -9,5 +9,5 @@ Function returning the url to use to redirect to the editing page of this object Signature: ```typescript -getEditUrl?: (savedObject: SavedObject) => string; +getEditUrl?: (savedObject: SavedObject) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md index 7b31dda402571..d6d50840aaadb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md @@ -9,7 +9,7 @@ Function returning the url to use to redirect to this object from the management Signature: ```typescript -getInAppUrl?: (savedObject: SavedObject) => { +getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md index 2f39acc66f451..75784666ef963 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md @@ -9,5 +9,5 @@ Function returning the title to display in the management table. If not defined, Signature: ```typescript -getTitle?: (savedObject: SavedObject) => string; +getTitle?: (savedObject: SavedObject) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md new file mode 100644 index 0000000000000..fef178e1d9847 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) + +## SavedObjectsTypeManagementDefinition.isExportable property + +Optional hook to specify whether an object should be exportable. + +If specified, `isExportable` will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export. + +When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + +Signature: + +```typescript +isExportable?: SavedObjectsExportablePredicate; +``` + +## Remarks + +`importableAndExportable` must be `true` to specify this property. + +## Example + +Registering a type with a per-object exportability predicate + +```ts +// src/plugins/my_plugin/server/plugin.ts +import { myType } from './saved_objects'; + +export class Plugin() { + setup: (core: CoreSetup) => { + core.savedObjects.registerType({ + ...myType, + management: { + ...myType.management, + isExportable: (object) => { + if (object.attributes.myCustomAttr === 'foo') { + return false; + } + return true; + } + }, + }); + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md index e9cc2b12108d6..8c42884eb0b31 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -9,7 +9,7 @@ Configuration options for the [type](./kibana-plugin-core-server.savedobjectstyp Signature: ```typescript -export interface SavedObjectsTypeManagementDefinition +export interface SavedObjectsTypeManagementDefinition ``` ## Properties @@ -17,11 +17,12 @@ export interface SavedObjectsTypeManagementDefinition | Property | Type | Description | | --- | --- | --- | | [defaultSearchField](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | string | The default search field to use for this type. Defaults to id. | -| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<any>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | -| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<any>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | -| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<any>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | +| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | +| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<Attributes>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | +| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | | [icon](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | The eui icon name to display in the management table. If not defined, the default icon will be used. | | [importableAndExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | Is the type importable or exportable. Defaults to false. | -| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. | -| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | +| [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | Optional hook to specify whether an object should be exportable.If specified, isExportable will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook<Attributes> | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md index 6302b36a73c68..a0d41d2d64967 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md @@ -10,10 +10,12 @@ It can be used to either mutate the exported objects, or add additional objects See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. +When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + Signature: ```typescript -onExport?: SavedObjectsExportTransform; +onExport?: SavedObjectsExportTransform; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md index f6634c01c66ba..332247b8eb8e1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md @@ -11,7 +11,7 @@ Import hooks are executed during the savedObjects import process and allow to in Signature: ```typescript -onImport?: SavedObjectsImportHook; +onImport?: SavedObjectsImportHook; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md index c839dd16d9a47..20d631ff74aca 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md @@ -11,9 +11,9 @@ To only get the visible types (which is the most common use case), use `getVisib Signature: ```typescript -getAllTypes(): SavedObjectsType[]; +getAllTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md index ab8a79c3a8455..1e29e632a6ec3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md @@ -9,9 +9,9 @@ Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently re Signature: ```typescript -getImportableAndExportableTypes(): SavedObjectsType[]; +getImportableAndExportableTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md index cfa52882bb89d..160aadb73cced 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md @@ -9,7 +9,7 @@ Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition fo Signature: ```typescript -getType(type: string): SavedObjectsType | undefined; +getType(type: string): SavedObjectsType | undefined; ``` ## Parameters @@ -20,5 +20,5 @@ getType(type: string): SavedObjectsType | undefined; Returns: -`SavedObjectsType | undefined` +`SavedObjectsType | undefined` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md index a773c6a0a674f..05f22dcf7010b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md @@ -11,9 +11,9 @@ A visible type is a type that doesn't explicitly define `hidden=true` during reg Signature: ```typescript -getVisibleTypes(): SavedObjectsType[]; +getVisibleTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef1ee69ff529b..77946e15ef686 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -295,6 +295,7 @@ export type { SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreateOptions, SavedObjectsExportResultDetails, + SavedObjectsExportExcludedObject, SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, diff --git a/src/core/server/saved_objects/export/apply_export_transforms.test.ts b/src/core/server/saved_objects/export/apply_export_transforms.test.ts index 95c6bd80a1ac3..ed428ef5759a8 100644 --- a/src/core/server/saved_objects/export/apply_export_transforms.test.ts +++ b/src/core/server/saved_objects/export/apply_export_transforms.test.ts @@ -27,6 +27,8 @@ const createTransform = ( implementation: SavedObjectsExportTransform = (ctx, objs) => objs ): jest.MockedFunction => jest.fn(implementation); +const toMap = (record: Record): Map => new Map(Object.entries(record)); + const expectedContext = { request: expect.any(KibanaRequest), }; @@ -49,10 +51,10 @@ describe('applyExportTransforms', () => { await applyExportTransforms({ request, objects: [foo1, bar1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(fooTransform).toHaveBeenCalledTimes(1); @@ -71,10 +73,10 @@ describe('applyExportTransforms', () => { await applyExportTransforms({ request, objects: [foo1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(fooTransform).toHaveBeenCalledTimes(1); @@ -100,10 +102,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(result).toEqual([foo1, foo2, dolly1, bar1, hello1]); @@ -123,9 +125,9 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, foo2, bar1, bar2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }); expect(result).toEqual([foo1, foo2, dolly1, bar1, bar2]); @@ -150,9 +152,9 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }); expect(result).toEqual([foo1, foo2].map(disableFoo)); @@ -175,10 +177,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(result).toEqual([foo1, dolly1, bar1, hello1]); @@ -201,10 +203,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), sortFunction: (obj1, obj2) => (obj1.id > obj2.id ? 1 : -1), }); @@ -223,9 +225,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -247,9 +249,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -271,9 +273,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -291,9 +293,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error transforming objects to export"`); }); diff --git a/src/core/server/saved_objects/export/apply_export_transforms.ts b/src/core/server/saved_objects/export/apply_export_transforms.ts index 2a788a32b92f6..78e1dd7d6c117 100644 --- a/src/core/server/saved_objects/export/apply_export_transforms.ts +++ b/src/core/server/saved_objects/export/apply_export_transforms.ts @@ -15,7 +15,7 @@ import { getObjKey, SavedObjectComparator } from './utils'; interface ApplyExportTransformsOptions { objects: SavedObject[]; request: KibanaRequest; - transforms: Record; + transforms: Map; sortFunction?: SavedObjectComparator; } @@ -30,7 +30,7 @@ export const applyExportTransforms = async ({ let finalObjects: SavedObject[] = []; for (const [type, typeObjs] of Object.entries(byType)) { - const typeTransformFn = transforms[type]; + const typeTransformFn = transforms.get(type); if (typeTransformFn) { finalObjects = [ ...finalObjects, diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index 0929ff0d40910..aab9f9134ee2c 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -9,9 +9,12 @@ import { applyExportTransformsMock } from './collect_exported_objects.test.mocks'; import { savedObjectsClientMock } from '../../mocks'; import { httpServerMock } from '../../http/http_server.mocks'; +import { loggerMock } from '../../logging/logger.mock'; import { SavedObject, SavedObjectError } from '../../../types'; +import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; -import { collectExportedObjects } from './collect_exported_objects'; +import { collectExportedObjects, ExclusionReason } from './collect_exported_objects'; +import { SavedObjectsExportablePredicate } from '../types'; const createObject = (parts: Partial): SavedObject => ({ id: 'id', @@ -29,14 +32,48 @@ const createError = (parts: Partial = {}): SavedObjectError => }); const toIdTuple = (obj: SavedObject) => ({ type: obj.type, id: obj.id }); +const toExcludedObject = (obj: SavedObject, reason: ExclusionReason = 'excluded') => ({ + type: obj.type, + id: obj.id, + reason, +}); + +const toMap = (record: Record): Map => new Map(Object.entries(record)); describe('collectExportedObjects', () => { let savedObjectsClient: ReturnType; let request: ReturnType; + let logger: ReturnType; + let typeRegistry: SavedObjectTypeRegistry; + + const registerType = ( + name: string, + { + onExport, + isExportable, + }: { + onExport?: SavedObjectsExportTransform; + isExportable?: SavedObjectsExportablePredicate; + } = {} + ) => { + typeRegistry.registerType({ + name, + hidden: false, + namespaceType: 'single', + mappings: { properties: {} }, + management: { + importableAndExportable: true, + onExport, + isExportable, + }, + }); + }; beforeEach(() => { + typeRegistry = new SavedObjectTypeRegistry(); savedObjectsClient = savedObjectsClientMock.create(); request = httpServerMock.createKibanaRequest(); + logger = loggerMock.create(); applyExportTransformsMock.mockImplementation(({ objects }) => objects); savedObjectsClient.bulkGet.mockResolvedValue({ saved_objects: [] }); }); @@ -58,23 +95,62 @@ describe('collectExportedObjects', () => { }); const fooTransform: SavedObjectsExportTransform = jest.fn(); + registerType('foo', { onExport: fooTransform }); await collectExportedObjects({ objects: [obj1, obj2], savedObjectsClient, request, - exportTransforms: { foo: fooTransform }, + typeRegistry, includeReferences: true, + logger, }); expect(applyExportTransformsMock).toHaveBeenCalledTimes(1); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [obj1, obj2], - transforms: { foo: fooTransform }, + transforms: toMap({ foo: fooTransform }), request, }); }); + it('calls `isExportable` with the correct parameters', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + const fooExportable: SavedObjectsExportablePredicate = jest.fn().mockReturnValue(true); + registerType('foo', { isExportable: fooExportable }); + + const barExportable: SavedObjectsExportablePredicate = jest.fn().mockReturnValue(true); + registerType('bar', { isExportable: barExportable }); + + await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(fooExportable).toHaveBeenCalledTimes(2); + expect(fooExportable).toHaveBeenCalledWith(foo1); + expect(fooExportable).toHaveBeenCalledWith(foo2); + + expect(barExportable).toHaveBeenCalledTimes(1); + expect(barExportable).toHaveBeenCalledWith(bar3); + }); + it('returns the collected objects', async () => { const foo1 = createObject({ type: 'foo', @@ -96,6 +172,10 @@ describe('collectExportedObjects', () => { id: '3', }); + registerType('foo'); + registerType('bar'); + registerType('dolly'); + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, dolly3]); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [bar2], @@ -105,14 +185,220 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toHaveLength(0); expect(objects.map(toIdTuple)).toEqual([foo1, dolly3, bar2].map(toIdTuple)); }); + it('excludes objects filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + registerType('foo', { isExportable: (obj) => obj.id !== '2' }); + registerType('bar', { isExportable: () => true }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar3]); + expect(excludedObjects).toEqual([foo2].map((obj) => toExcludedObject(obj))); + }); + + it('excludes objects when the predicate throws', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + registerType('foo', { + isExportable: (obj) => { + if (obj.id === '1') { + throw new Error('reason'); + } + return true; + }, + }); + registerType('bar', { isExportable: () => true }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo2, bar3]); + expect(excludedObjects).toEqual( + [foo1].map((obj) => toExcludedObject(obj, 'predicate_error')) + ); + }); + + it('logs an error for each predicate error', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const foo3 = createObject({ + type: 'foo', + id: '3', + }); + + registerType('foo', { + isExportable: (obj) => { + if (obj.id !== '2') { + throw new Error('reason'); + } + return true; + }, + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, foo3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo2]); + expect(excludedObjects).toEqual( + [foo1, foo3].map((obj) => toExcludedObject(obj, 'predicate_error')) + ); + + expect(logger.error).toHaveBeenCalledTimes(2); + const logMessages = logger.error.mock.calls.map((call) => call[0]); + + expect( + (logMessages[0] as string).startsWith( + `Error invoking "isExportable" for object foo:1. Error was: Error: reason` + ) + ).toBe(true); + expect( + (logMessages[1] as string).startsWith( + `Error invoking "isExportable" for object foo:3. Error was: Error: reason` + ) + ).toBe(true); + }); + + it('excludes references filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + references: [ + { + type: 'bar', + id: '2', + name: 'bar-2', + }, + { + type: 'excluded', + id: '1', + name: 'excluded-1', + }, + ], + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + }); + const excluded1 = createObject({ + type: 'excluded', + id: '1', + }); + + registerType('foo'); + registerType('bar'); + registerType('excluded', { isExportable: () => false }); + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [bar2, excluded1], + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar2]); + expect(excludedObjects).toEqual([excluded1].map((obj) => toExcludedObject(obj))); + }); + + it('excludes additional objects filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + }); + const excluded1 = createObject({ + type: 'excluded', + id: '1', + }); + + registerType('foo'); + registerType('bar'); + registerType('excluded', { isExportable: () => false }); + + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [ + ...objects, + bar2, + excluded1, + ]); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar2]); + expect(excludedObjects).toEqual([excluded1].map((obj) => toExcludedObject(obj))); + }); + it('returns the missing references', async () => { const foo1 = createObject({ type: 'foo', @@ -163,8 +449,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toEqual([missing1, missing2].map(toIdTuple)); @@ -185,8 +472,9 @@ describe('collectExportedObjects', () => { objects: [obj1, obj2], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toHaveLength(0); @@ -228,8 +516,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -241,12 +530,12 @@ describe('collectExportedObjects', () => { expect(applyExportTransformsMock).toHaveBeenCalledTimes(2); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [foo1], - transforms: {}, + transforms: toMap({}), request, }); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [bar2], - transforms: {}, + transforms: toMap({}), request, }); }); @@ -302,8 +591,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2); @@ -366,8 +656,9 @@ describe('collectExportedObjects', () => { objects: [foo1, bar2], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -411,8 +702,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -474,8 +766,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2); @@ -490,6 +783,67 @@ describe('collectExportedObjects', () => { expect.any(Object) ); }); + + it('excludes references filtered by the `isExportable` predicate for additional objects returned by the export transform', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + references: [ + { + type: 'dolly', + id: '3', + name: 'dolly-3', + }, + { + type: 'baz', + id: '4', + name: 'baz-4', + }, + ], + }); + const dolly3 = createObject({ + type: 'dolly', + id: '3', + references: [ + { + type: 'baz', + id: '4', + name: 'baz-4', + }, + ], + }); + const baz4 = createObject({ + type: 'baz', + id: '4', + }); + + registerType('foo'); + registerType('bar'); + registerType('dolly'); + registerType('baz', { isExportable: () => false }); + + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, bar2]); + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [dolly3, baz4], + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar2, dolly3]); + expect(excludedObjects).toEqual([baz4].map((obj) => toExcludedObject(obj))); + }); }); describe('when `includeReferences` is `false`', () => { @@ -510,8 +864,9 @@ describe('collectExportedObjects', () => { objects: [obj1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: false, + logger, }); expect(missingRefs).toHaveLength(0); diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index d45782a83c284..4789fd3bff67f 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -8,7 +8,9 @@ import type { SavedObject } from '../../../types'; import type { KibanaRequest } from '../../http'; -import { SavedObjectsClientContract } from '../types'; +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'; @@ -22,41 +24,80 @@ interface CollectExportedObjectOptions { /** The http request initiating the export. */ request: KibanaRequest; /** export transform per type */ - exportTransforms: Record; + 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, - exportTransforms, + typeRegistry, savedObjectsClient, + logger, }: CollectExportedObjectOptions): Promise => { + const exportTransforms = buildTransforms(typeRegistry); + const isExportable = buildIsExportable(typeRegistry); + const collectedObjects: SavedObject[] = []; const collectedMissingRefs: CollectedReference[] = []; + const collectedNonExportableObjects: ExcludedObject[] = []; const alreadyProcessed: Set = new Set(); let currentObjects = objects; do { - const transformed = ( + currentObjects = currentObjects.filter((object) => !alreadyProcessed.has(objKey(object))); + + // first, evict current objects that are not exportable + const { + exportable: untransformedExportableInitialObjects, + nonExportable: nonExportableInitialObjects, + } = await splitByExportability(currentObjects, isExportable, logger); + collectedNonExportableObjects.push(...nonExportableInitialObjects); + nonExportableInitialObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); + + // second, apply export transforms to exportable objects + const transformedObjects = ( await applyExportTransforms({ request, - objects: currentObjects, + objects: untransformedExportableInitialObjects, transforms: exportTransforms, }) ).filter((object) => !alreadyProcessed.has(objKey(object))); + transformedObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); - transformed.forEach((obj) => alreadyProcessed.add(objKey(obj))); - collectedObjects.push(...transformed); + // last, evict additional objects that are not exportable + 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` is true, recurse on exportable objects' references. if (includeReferences) { - const references = collectReferences(transformed, alreadyProcessed); + const references = collectReferences(allExportableObjects, alreadyProcessed); if (references.length) { const { objects: fetchedObjects, missingRefs } = await fetchReferences({ references, @@ -75,6 +116,7 @@ export const collectExportedObjects = async ({ return { objects: collectedObjects, + excludedObjects: collectedNonExportableObjects, missingRefs: collectedMissingRefs, }; }; @@ -126,3 +168,83 @@ const fetchReferences = async ({ .map((obj) => ({ type: obj.type, id: obj.id })), }; }; + +const buildTransforms = (typeRegistry: ISavedObjectTypeRegistry) => + typeRegistry.getAllTypes().reduce((transformMap, type) => { + if (type.management?.onExport) { + transformMap.set(type.name, type.management.onExport); + } + return transformMap; + }, new Map()); + +const buildIsExportable = ( + typeRegistry: ISavedObjectTypeRegistry +): SavedObjectsExportablePredicate => { + const exportablePerType = typeRegistry.getAllTypes().reduce((exportableMap, type) => { + if (type.management?.isExportable) { + exportableMap.set(type.name, type.management.isExportable); + } + return exportableMap; + }, new Map()); + + return (obj: SavedObject) => { + const typePredicate = exportablePerType.get(obj.type); + return typePredicate ? typePredicate(obj) : true; + }; +}; + +const splitByExportability = ( + objects: SavedObject[], + isExportable: SavedObjectsExportablePredicate, + logger: Logger +) => { + const exportableObjects: SavedObject[] = []; + const nonExportableObjects: ExcludedObject[] = []; + + 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: SavedObject[], keys: ObjectKey[]) => { + const included: SavedObject[] = []; + const excluded: SavedObject[] = []; + objects.forEach((obj) => { + if (keys.includes(objKey(obj))) { + included.push(obj); + } else { + excluded.push(obj); + } + }); + return { + included, + excluded, + }; +}; diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 4af184e54b49c..d9b48ce431117 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -13,6 +13,7 @@ export type { SavedObjectsExportResultDetails, SavedObjectsExportTransformContext, SavedObjectsExportTransform, + SavedObjectsExportExcludedObject, } from './types'; export { SavedObjectsExporter } from './saved_objects_exporter'; export type { ISavedObjectsExporter } from './saved_objects_exporter'; diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 6bdb8003de49d..5968c8dabe8a8 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -77,32 +77,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -185,6 +187,8 @@ describe('getSortedObjectsForExport()', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(response[response.length - 1]).toMatchInlineSnapshot(` Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, "exportedCount": 20, "missingRefCount": 0, "missingReferences": Array [], @@ -269,6 +273,8 @@ describe('getSortedObjectsForExport()', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); expect(response[response.length - 1]).toMatchInlineSnapshot(` Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, "exportedCount": 1500, "missingRefCount": 0, "missingReferences": Array [], @@ -422,32 +428,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -579,32 +587,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -674,26 +684,28 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 1, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 1, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -770,32 +782,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -929,38 +943,40 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object { - "name": "foo", - }, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "bar", - }, - "id": "2", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "baz", - }, - "id": "3", - "references": Array [], - "type": "index-pattern", - }, - Object { - "exportedCount": 3, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object { + "name": "foo", + }, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "bar", + }, + "id": "2", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "baz", + }, + "id": "3", + "references": Array [], + "type": "index-pattern", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 3, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); }); }); @@ -1003,32 +1019,34 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -1211,32 +1229,34 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 9d56bb4872a6d..211dcdc4ee62d 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -18,7 +18,6 @@ import { SavedObjectExportBaseOptions, SavedObjectsExportByObjectOptions, SavedObjectsExportByTypeOptions, - SavedObjectsExportTransform, } from './types'; import { SavedObjectsExportError } from './errors'; import { collectExportedObjects } from './collect_exported_objects'; @@ -34,8 +33,8 @@ export type ISavedObjectsExporter = PublicMethodsOf; */ export class SavedObjectsExporter { readonly #savedObjectsClient: SavedObjectsClientContract; - readonly #exportTransforms: Record; readonly #exportSizeLimit: number; + readonly #typeRegistry: ISavedObjectTypeRegistry; readonly #log: Logger; constructor({ @@ -52,15 +51,7 @@ export class SavedObjectsExporter { this.#log = logger; this.#savedObjectsClient = savedObjectsClient; this.#exportSizeLimit = exportSizeLimit; - this.#exportTransforms = typeRegistry.getAllTypes().reduce((transforms, type) => { - if (type.management?.onExport) { - return { - ...transforms, - [type.name]: type.management.onExport, - }; - } - return transforms; - }, {} as Record); + this.#typeRegistry = typeRegistry; } /** @@ -121,13 +112,15 @@ export class SavedObjectsExporter { const { objects: collectedObjects, missingRefs: missingReferences, + excludedObjects, } = await collectExportedObjects({ objects: savedObjects, includeReferences: includeReferencesDeep, namespace, request, - exportTransforms: this.#exportTransforms, + typeRegistry: this.#typeRegistry, savedObjectsClient: this.#savedObjectsClient, + logger: this.#log, }); // sort with the provided sort function then with the default export sorting @@ -142,6 +135,8 @@ export class SavedObjectsExporter { exportedCount: exportedObjects.length, missingRefCount: missingReferences.length, missingReferences, + excludedObjectsCount: excludedObjects.length, + excludedObjects, }; this.#log.debug(`Exporting [${redactedObjects.length}] saved objects.`); return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index 7891af6df5b1b..a805ec3a06c1b 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -72,6 +72,20 @@ export interface SavedObjectsExportResultDetails { /** the missing reference type. */ type: string; }>; + /** number of objects that were excluded from the export */ + excludedObjectsCount: number; + /** excluded objects details */ + excludedObjects: SavedObjectsExportExcludedObject[]; +} + +/** @public */ +export interface SavedObjectsExportExcludedObject { + /** id of the excluded object */ + id: string; + /** type of the excluded object */ + type: string; + /** optional cause of the exclusion */ + reason?: string; } /** @@ -158,7 +172,7 @@ export interface SavedObjectsExportTransformContext { * * @public */ -export type SavedObjectsExportTransform = ( +export type SavedObjectsExportTransform = ( context: SavedObjectsExportTransformContext, objects: Array> ) => SavedObject[] | Promise; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index b1b1584d7de88..5f853d49219dc 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -41,6 +41,7 @@ export type { SavedObjectsExportError, SavedObjectsExportTransformContext, SavedObjectsExportTransform, + SavedObjectsExportExcludedObject, } from './export'; export { SavedObjectsSerializer } from './serialization'; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b95f187cd44ca..e50c1e540bfaf 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -141,7 +141,7 @@ export interface SavedObjectsServiceSetup { * } * ``` */ - registerType: (type: SavedObjectsType) => void; + registerType: (type: SavedObjectsType) => void; } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 964ba671b5964..1bb214de701e2 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -253,7 +253,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat * * @public */ -export interface SavedObjectsType { +export interface SavedObjectsType { /** * The name of the type, which is also used as the internal id. */ @@ -337,7 +337,7 @@ export interface SavedObjectsType { /** * An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type. */ - management?: SavedObjectsTypeManagementDefinition; + management?: SavedObjectsTypeManagementDefinition; } /** @@ -345,7 +345,7 @@ export interface SavedObjectsType { * * @public */ -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { /** * Is the type importable or exportable. Defaults to `false`. */ @@ -363,12 +363,12 @@ export interface SavedObjectsTypeManagementDefinition { * Function returning the title to display in the management table. * If not defined, will use the object's type and id to generate a label. */ - getTitle?: (savedObject: SavedObject) => string; + getTitle?: (savedObject: SavedObject) => string; /** * Function returning the url to use to redirect to the editing page of this object. * If not defined, editing will not be allowed. */ - getEditUrl?: (savedObject: SavedObject) => string; + getEditUrl?: (savedObject: SavedObject) => string; /** * Function returning the url to use to redirect to this object from the management section. * If not defined, redirecting to the object will not be allowed. @@ -377,7 +377,9 @@ export interface SavedObjectsTypeManagementDefinition { * the object page, relative to the base path. `uiCapabilitiesPath` is the path to check in the * {@link Capabilities | uiCapabilities} to check if the user has permission to access the object. */ - getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; + getInAppUrl?: ( + savedObject: SavedObject + ) => { path: string; uiCapabilitiesPath: string }; /** * An optional export transform function that can be used transform the objects of the registered type during * the export process. @@ -386,9 +388,14 @@ export interface SavedObjectsTypeManagementDefinition { * * See {@link SavedObjectsExportTransform | the transform type documentation} for more info and examples. * + * When implementing both `isExportable` and `onExport`, it is mandatory that + * `isExportable` returns the same value for an object before and after going + * though the export transform. + * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + * * @remarks `importableAndExportable` must be `true` to specify this property. */ - onExport?: SavedObjectsExportTransform; + onExport?: SavedObjectsExportTransform; /** * An optional {@link SavedObjectsImportHook | import hook} to use when importing given type. * @@ -431,5 +438,52 @@ export interface SavedObjectsTypeManagementDefinition { * @remarks messages returned in the warnings are user facing and must be translated. * @remarks `importableAndExportable` must be `true` to specify this property. */ - onImport?: SavedObjectsImportHook; + onImport?: SavedObjectsImportHook; + + /** + * Optional hook to specify whether an object should be exportable. + * + * If specified, `isExportable` will be called during export for each + * of this type's objects in the export, and the ones not matching the + * predicate will be excluded from the export. + * + * When implementing both `isExportable` and `onExport`, it is mandatory that + * `isExportable` returns the same value for an object before and after going + * though the export transform. + * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + * + * @example + * Registering a type with a per-object exportability predicate + * ```ts + * // src/plugins/my_plugin/server/plugin.ts + * import { myType } from './saved_objects'; + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.savedObjects.registerType({ + * ...myType, + * management: { + * ...myType.management, + * isExportable: (object) => { + * if (object.attributes.myCustomAttr === 'foo') { + * return false; + * } + * return true; + * } + * }, + * }); + * } + * } + * ``` + * + * @remarks `importableAndExportable` must be `true` to specify this property. + */ + isExportable?: SavedObjectsExportablePredicate; } + +/** + * @public + */ +export type SavedObjectsExportablePredicate = ( + obj: SavedObject +) => boolean; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ce13174ee19cc..9e7721fde90e7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2508,8 +2508,17 @@ export class SavedObjectsExportError extends Error { readonly type: string; } +// @public (undocumented) +export interface SavedObjectsExportExcludedObject { + id: string; + reason?: string; + type: string; +} + // @public export interface SavedObjectsExportResultDetails { + excludedObjects: SavedObjectsExportExcludedObject[]; + excludedObjectsCount: number; exportedCount: number; missingRefCount: number; missingReferences: Array<{ @@ -2519,7 +2528,7 @@ export interface SavedObjectsExportResultDetails { } // @public -export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; +export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; // @public export interface SavedObjectsExportTransformContext { @@ -2930,7 +2939,7 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; - registerType: (type: SavedObjectsType) => void; + registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } @@ -2956,12 +2965,12 @@ export interface SavedObjectStatusMeta { } // @public (undocumented) -export interface SavedObjectsType { +export interface SavedObjectsType { convertToAliasScript?: string; convertToMultiNamespaceTypeVersion?: string; hidden: boolean; indexPattern?: string; - management?: SavedObjectsTypeManagementDefinition; + management?: SavedObjectsTypeManagementDefinition; mappings: SavedObjectsTypeMappingDefinition; migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap); name: string; @@ -2969,18 +2978,20 @@ export interface SavedObjectsType { } // @public -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { defaultSearchField?: string; - getEditUrl?: (savedObject: SavedObject) => string; - getInAppUrl?: (savedObject: SavedObject) => { + getEditUrl?: (savedObject: SavedObject) => string; + getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string; }; - getTitle?: (savedObject: SavedObject) => string; + getTitle?: (savedObject: SavedObject) => string; icon?: string; importableAndExportable?: boolean; - onExport?: SavedObjectsExportTransform; - onImport?: SavedObjectsImportHook; + // Warning: (ae-forgotten-export) The symbol "SavedObjectsExportablePredicate" needs to be exported by the entry point index.d.ts + isExportable?: SavedObjectsExportablePredicate; + onExport?: SavedObjectsExportTransform; + onImport?: SavedObjectsImportHook; } // @public @@ -3045,11 +3056,11 @@ export class SavedObjectsUtils { // @public export class SavedObjectTypeRegistry { - getAllTypes(): SavedObjectsType[]; - getImportableAndExportableTypes(): SavedObjectsType[]; + getAllTypes(): SavedObjectsType[]; + getImportableAndExportableTypes(): SavedObjectsType[]; getIndex(type: string): string | undefined; - getType(type: string): SavedObjectsType | undefined; - getVisibleTypes(): SavedObjectsType[]; + getType(type: string): SavedObjectsType | undefined; + getVisibleTypes(): SavedObjectsType[]; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; isMultiNamespace(type: string): boolean; diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts index c9f9b8591860d..7487e57da9e8c 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts @@ -14,14 +14,22 @@ describe('extractExportDetails', () => { }; const detailsLine = ( exported: number, - missingRefs: SavedObjectsExportResultDetails['missingReferences'] = [] + { + missingRefs = [], + excludedObjects = [], + }: { + missingRefs?: SavedObjectsExportResultDetails['missingReferences']; + excludedObjects?: SavedObjectsExportResultDetails['excludedObjects']; + } = {} ) => { return ( JSON.stringify({ exportedCount: exported, missingRefCount: missingRefs.length, missingReferences: missingRefs, - }) + '\n' + excludedObjectsCount: excludedObjects.length, + excludedObjects, + } as SavedObjectsExportResultDetails) + '\n' ); }; @@ -43,6 +51,8 @@ describe('extractExportDetails', () => { exportedCount: 3, missingRefCount: 0, missingReferences: [], + excludedObjectsCount: 0, + excludedObjects: [], }); }); @@ -51,10 +61,12 @@ describe('extractExportDetails', () => { [ [ objLine('1', 'index-pattern'), - detailsLine(1, [ - { id: '2', type: 'index-pattern' }, - { id: '3', type: 'index-pattern' }, - ]), + detailsLine(1, { + missingRefs: [ + { id: '2', type: 'index-pattern' }, + { id: '3', type: 'index-pattern' }, + ], + }), ].join(''), ], { @@ -71,6 +83,39 @@ describe('extractExportDetails', () => { { id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }, ], + excludedObjectsCount: 0, + excludedObjects: [], + }); + }); + + it('should properly extract the excluded objects', async () => { + const exportData = new Blob( + [ + [ + objLine('1', 'index-pattern'), + detailsLine(1, { + excludedObjects: [ + { id: '2', type: 'index-pattern', reason: 'foo' }, + { id: '3', type: 'index-pattern' }, + ], + }), + ].join(''), + ], + { + type: 'application/ndjson', + endings: 'transparent', + } + ); + const result = await extractExportDetails(exportData); + expect(result).toEqual({ + exportedCount: 1, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 2, + excludedObjects: [ + { id: '2', type: 'index-pattern', reason: 'foo' }, + { id: '3', type: 'index-pattern' }, + ], }); }); diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts index 40f8039a8cdae..4d142330dca88 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts @@ -33,6 +33,12 @@ export interface SavedObjectsExportResultDetails { id: string; type: string; }>; + excludedObjectsCount: number; + excludedObjects: Array<{ + id: string; + type: string; + reason?: string; + }>; } function isExportDetails(object: any): object is SavedObjectsExportResultDetails { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 364b3ab0d9eb6..9b8474fc08bbd 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -258,7 +258,7 @@ describe('SavedObjectsTable', () => { }); }); - it('should display a warning is export contains missing references', async () => { + it('should display a warning if the export contains missing references', async () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, @@ -280,6 +280,8 @@ describe('SavedObjectsTable', () => { exportedCount: 2, missingRefCount: 1, missingReferences: [{ id: '7', type: 'visualisation' }], + excludedObjectsCount: 0, + excludedObjects: [], })); const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); @@ -303,6 +305,53 @@ describe('SavedObjectsTable', () => { }); }); + it('should display a specific message if the export contains excluded objects', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({ + _id: obj.id, + _source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + }; + + extractExportDetailsMock.mockImplementation(() => ({ + exportedCount: 2, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 1, + excludedObjects: [{ id: '7', type: 'visualisation' }], + })); + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().onExport(true); + + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: + 'Your file is downloading in the background. ' + + 'Some objects were excluded from the export. ' + + 'Please see the last line in the exported file for a list of excluded objects.', + }); + }); + it('should allow the user to choose when exporting all', async () => { const component = shallowRender(); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index e23c74bc1bc19..42c1220ef5540 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -358,7 +358,7 @@ export class SavedObjectsTable extends Component { @@ -395,31 +395,45 @@ export class SavedObjectsTable extends Component { + showExportCompleteMessage = (exportDetails: SavedObjectsExportResultDetails | undefined) => { const { notifications } = this.props; - if (exportDetails && exportDetails.missingReferences.length > 0) { - notifications.toasts.addWarning({ - title: i18n.translate( - 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', - { - defaultMessage: - 'Your file is downloading in the background. ' + - 'Some related objects could not be found. ' + - 'Please see the last line in the exported file for a list of missing objects.', - } - ), - }); - } else { - notifications.toasts.addSuccess({ - title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { - defaultMessage: 'Your file is downloading in the background', - }), - }); + if (exportDetails) { + if (exportDetails.missingReferences.length > 0) { + return notifications.toasts.addWarning({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + } + ), + }); + } + if (exportDetails.excludedObjects.length > 0) { + return notifications.toasts.addSuccess({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithExcludedObjectsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some objects were excluded from the export. ' + + 'Please see the last line in the exported file for a list of excluded objects.', + } + ), + }); + } } + return notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { + defaultMessage: 'Your file is downloading in the background', + }), + }); }; finishImport = () => { diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json new file mode 100644 index 0000000000000..f7015ee20251d --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json @@ -0,0 +1,135 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:1", + "source": { + "test-is-exportable": { + "title": "obj 1", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [ + { + "type": "test-is-exportable", + "id": "2", + "name": "ref-1" + }, + { + "type": "test-is-exportable", + "id": "3", + "name": "ref-2" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:2", + "source": { + "test-is-exportable": { + "title": "obj 2", + "enabled": false + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:3", + "source": { + "test-is-exportable": { + "title": "obj 3", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [ + { + "type": "test-is-exportable", + "id": "4", + "name": "ref-1" + }, + { + "type": "test-is-exportable", + "id": "5", + "name": "ref-2" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:4", + "source": { + "test-is-exportable": { + "title": "obj 4", + "enabled": false + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:5", + "source": { + "test-is-exportable": { + "title": "obj 5", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:error", + "source": { + "test-is-exportable": { + "title": "obj error", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json new file mode 100644 index 0000000000000..abec2eeb77492 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json @@ -0,0 +1,505 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "test-export-transform": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-is-exportable": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-export-add": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-add-dep": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-transform-error": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-invalid-transform": { + "properties": { + "title": { "type": "text" } + } + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "bounds": { + "dynamic": false, + "properties": {} + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } +} diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts index 408ac03dd946b..15afdb229b1fd 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts @@ -152,6 +152,30 @@ export class SavedObjectExportTransformsPlugin implements Plugin { getTitle: (obj) => obj.attributes.title, }, }); + + // example of a SO type implementing the `isExportable` API + savedObjects.registerType<{ enabled: boolean; title: string }>({ + name: 'test-is-exportable', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + enabled: { type: 'boolean' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj) => obj.attributes.title, + isExportable: (obj) => { + if (obj.id === 'error') { + throw new Error('something went wrong'); + } + return obj.attributes.enabled === true; + }, + }, + }); } public start() {} diff --git a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts index 0351c5abdde46..8437e050091fb 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import type { SavedObject } from '../../../../src/core/types'; +import type { SavedObjectsExportResultDetails } from '../../../../src/core/server'; import { PluginFunctionalProviderContext } from '../../services'; function parseNdJson(input: string): Array> { @@ -139,7 +140,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); - describe('FOO nested export transforms', () => { + describe('nested export transforms', () => { before(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' @@ -183,5 +184,121 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); }); + + describe('isExportable API', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + }); + + it('should only export objects returning `true` for `isExportable`', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text).sort((obj1, obj2) => + obj1.id.localeCompare(obj2.id) + ); + expect(objects.map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:1', + 'test-is-exportable:3', + 'test-is-exportable:5', + ]); + }); + }); + + it('lists objects that got filtered', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + const exportDetails = (objects[ + objects.length - 1 + ] as unknown) as SavedObjectsExportResultDetails; + + expect(exportDetails.excludedObjectsCount).to.eql(2); + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: '2', + reason: 'excluded', + }, + { + type: 'test-is-exportable', + id: '4', + reason: 'excluded', + }, + ]); + }); + }); + + it('excludes objects if `isExportable` throws', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '5', + }, + { + type: 'test-is-exportable', + id: 'error', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.length).to.eql(2); + expect([objects[0]].map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:5', + ]); + const exportDetails = (objects[ + objects.length - 1 + ] as unknown) as SavedObjectsExportResultDetails; + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: 'error', + reason: 'predicate_error', + }, + ]); + }); + }); + }); }); } diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index d9ebbac810231..ea2f321458c22 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -21,12 +21,15 @@ const { export interface ExportTestDefinition extends TestDefinition { request: ReturnType; } + export type ExportTestSuite = TestSuite; + interface SuccessResult { type: string; id: string; originId?: string; } + export interface ExportTestCase { title: string; type: string; @@ -135,7 +138,13 @@ export const createRequest = ({ type, id }: ExportTestCase) => const getTestTitle = ({ failure, title }: ExportTestCase) => `${failure?.reason || 'success'} ["${title}"]`; -const EMPTY_RESULT = { exportedCount: 0, missingRefCount: 0, missingReferences: [] }; +const EMPTY_RESULT = { + excludedObjects: [], + excludedObjectsCount: 0, + exportedCount: 0, + missingRefCount: 0, + missingReferences: [], +}; export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectSavedObjectForbiddenBulkGet = expectResponses.forbiddenTypes('bulk_get'); @@ -189,6 +198,8 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest