diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index 7daf5d086d9e4..b7fc255a57acf 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -164,6 +164,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [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) |
| [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) | |
| [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) | |
| [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find()
method.\*Note\*: this type is different between the Public and Server Saved Objects clients. |
@@ -297,6 +298,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. |
| [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). |
| [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. |
+| [SavedObjectsExportTransform](./kibana-plugin-core-server.savedobjectsexporttransform.md) | Transformation function used to mutate the exported objects of the associated type.A type's export transform function will be executed once per user-initiated export, for all objects of that type. |
| [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation |
| [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) | A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. |
| [SavedObjectsImportWarning](./kibana-plugin-core-server.savedobjectsimportwarning.md) | Composite type of all the possible types of import warnings.See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) for more details. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md
index eb35bb6a4ea5c..0e8fa73039d40 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md
@@ -18,4 +18,5 @@ export interface SavedObjectExportBaseOptions
| [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean
| flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. |
| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean
| flag to also include all related saved objects in the export stream. |
| [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string
| optional namespace to override the namespace used by the savedObjectsClient. |
+| [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | KibanaRequest
| The http request initiating the export. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.request.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.request.md
new file mode 100644
index 0000000000000..d425f9b88e818
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.request.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md)
+
+## SavedObjectExportBaseOptions.request property
+
+The http request initiating the export.
+
+Signature:
+
+```typescript
+request: KibanaRequest;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md
index cc192b03ca7c2..5e959bbee7beb 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md
@@ -9,8 +9,9 @@ Constructs a new instance of the `SavedObjectsExporter` class
Signature:
```typescript
-constructor({ savedObjectsClient, exportSizeLimit, }: {
+constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: {
savedObjectsClient: SavedObjectsClientContract;
+ typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
});
```
@@ -19,5 +20,5 @@ constructor({ savedObjectsClient, exportSizeLimit, }: {
| Parameter | Type | Description |
| --- | --- | --- |
-| { savedObjectsClient, exportSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
exportSizeLimit: number;
}
| |
+| { savedObjectsClient, typeRegistry, exportSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
}
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md
index d8d9248f34af6..727108b824c84 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md
@@ -15,7 +15,7 @@ export declare class SavedObjectsExporter
| Constructor | Modifiers | Description |
| --- | --- | --- |
-| [(constructor)({ savedObjectsClient, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter
class |
+| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter
class |
## Properties
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md
new file mode 100644
index 0000000000000..5a390bd450421
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [invalidTransformError](./kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md)
+
+## SavedObjectsExportError.invalidTransformError() method
+
+Error returned when a [export tranform](./kibana-plugin-core-server.savedobjectsexporttransform.md) performed an invalid operation during the transform, such as removing objects from the export, or changing an object's type or id.
+
+Signature:
+
+```typescript
+static invalidTransformError(objectKeys: string[]): SavedObjectsExportError;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| objectKeys | string[]
| |
+
+Returns:
+
+`SavedObjectsExportError`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md
index bfeaa03a94700..7d5c6e5d89a5b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md
@@ -29,5 +29,7 @@ export declare class SavedObjectsExportError extends Error
| Method | Modifiers | Description |
| --- | --- | --- |
| [exportSizeExceeded(limit)](./kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md) | static
| |
+| [invalidTransformError(objectKeys)](./kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md) | static
| Error returned when a [export tranform](./kibana-plugin-core-server.savedobjectsexporttransform.md) performed an invalid operation during the transform, such as removing objects from the export, or changing an object's type or id. |
| [objectFetchError(objects)](./kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md) | static
| |
+| [objectTransformError(objects, cause)](./kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md) | static
| Error returned when a [export tranform](./kibana-plugin-core-server.savedobjectsexporttransform.md) threw an error |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md
new file mode 100644
index 0000000000000..4463e9ff06da0
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [objectTransformError](./kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md)
+
+## SavedObjectsExportError.objectTransformError() method
+
+Error returned when a [export tranform](./kibana-plugin-core-server.savedobjectsexporttransform.md) threw an error
+
+Signature:
+
+```typescript
+static objectTransformError(objects: SavedObject[], cause: Error): SavedObjectsExportError;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| objects | SavedObject[]
| |
+| cause | Error
| |
+
+Returns:
+
+`SavedObjectsExportError`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md
new file mode 100644
index 0000000000000..50d4c5425e8fd
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md
@@ -0,0 +1,86 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportTransform](./kibana-plugin-core-server.savedobjectsexporttransform.md)
+
+## SavedObjectsExportTransform type
+
+Transformation function used to mutate the exported objects of the associated type.
+
+A type's export transform function will be executed once per user-initiated export, for all objects of that type.
+
+Signature:
+
+```typescript
+export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise;
+```
+
+## Remarks
+
+Trying to change an object's id or type during the transform will result in a runtime error during the export process.
+
+## Example 1
+
+Registering a transform function changing the object's attributes during the export
+
+```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,
+ onExport: (ctx, objects) => {
+ return objects.map((obj) => ({
+ ...obj,
+ attributes: {
+ ...obj.attributes,
+ enabled: false,
+ }
+ })
+ }
+ },
+ });
+ }
+}
+
+```
+
+## Example 2
+
+Registering a transform function adding additional objects to the export
+
+```ts
+// src/plugins/my_plugin/server/plugin.ts
+import { myType } from './saved_objects';
+
+export class Plugin() {
+ setup: (core: CoreSetup) => {
+ const savedObjectStartContractPromise = getStartServices().then(
+ ([{ savedObjects: savedObjectsStart }]) => savedObjectsStart
+ );
+
+ core.savedObjects.registerType({
+ ...myType,
+ management: {
+ ...myType.management,
+ onExport: async (ctx, objects) => {
+ const { getScopedClient } = await savedObjectStartContractPromise;
+ const client = getScopedClient(ctx.request);
+
+ const depResponse = await client.find({
+ type: 'my-nested-object',
+ hasReference: objs.map(({ id, type }) => ({ id, type })),
+ });
+
+ return [...objs, ...depResponse.saved_objects];
+ }
+ },
+ });
+ }
+}
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md
new file mode 100644
index 0000000000000..271f0048842b2
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportTransformContext](./kibana-plugin-core-server.savedobjectsexporttransformcontext.md)
+
+## SavedObjectsExportTransformContext interface
+
+Context passed down to a [export transform function](./kibana-plugin-core-server.savedobjectsexporttransform.md)
+
+Signature:
+
+```typescript
+export interface SavedObjectsExportTransformContext
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [request](./kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md) | KibanaRequest
| The request that initiated the export request. Can be used to create scoped services or client inside the [transformation](./kibana-plugin-core-server.savedobjectsexporttransform.md) |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md
new file mode 100644
index 0000000000000..fe04698899c7c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportTransformContext](./kibana-plugin-core-server.savedobjectsexporttransformcontext.md) > [request](./kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md)
+
+## SavedObjectsExportTransformContext.request property
+
+The request that initiated the export request. Can be used to create scoped services or client inside the [transformation](./kibana-plugin-core-server.savedobjectsexporttransform.md)
+
+Signature:
+
+```typescript
+request: KibanaRequest;
+```
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 92b6ddf29b8ec..e9cc2b12108d6 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md
@@ -22,5 +22,6 @@ export interface SavedObjectsTypeManagementDefinition
| [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. |
| [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. |
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
new file mode 100644
index 0000000000000..6302b36a73c68
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md)
+
+## SavedObjectsTypeManagementDefinition.onExport property
+
+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.
+
+Signature:
+
+```typescript
+onExport?: SavedObjectsExportTransform;
+```
+
+## Remarks
+
+`importableAndExportable` must be `true` to specify this property.
+
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 55733ca5d4443..f6634c01c66ba 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
@@ -14,6 +14,10 @@ Import hooks are executed during the savedObjects import process and allow to in
onImport?: SavedObjectsImportHook;
```
+## Remarks
+
+`importableAndExportable` must be `true` to specify this property.
+
## Example
Registering a hook displaying a warning about a specific type of object
@@ -48,5 +52,4 @@ export class Plugin() {
}
```
- messages returned in the warnings are user facing and must be translated.
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index da818470133cd..d5a23ed4b4a84 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -9,6 +9,7 @@ import { ApiResponse } from '@elastic/elasticsearch/lib/Transport';
import Boom from '@hapi/boom';
import { ConfigDeprecationProvider } from '@kbn/config';
import { ConfigPath } from '@kbn/config';
+import { DetailedPeerCertificate } from 'tls';
import { EnvironmentMode } from '@kbn/config';
import { EuiBreadcrumb } from '@elastic/eui';
import { EuiButtonEmptyProps } from '@elastic/eui';
@@ -18,20 +19,25 @@ import { EuiGlobalToastListToast } from '@elastic/eui';
import { History } from 'history';
import { Href } from 'history';
import { IconType } from '@elastic/eui';
+import { IncomingHttpHeaders } from 'http';
import { KibanaClient } from '@elastic/elasticsearch/api/kibana';
import { Location } from 'history';
import { LocationDescriptorObject } from 'history';
import { Logger } from '@kbn/logging';
import { LogMeta } from '@kbn/logging';
import { MaybePromise } from '@kbn/utility-types';
+import { ObjectType } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import { Path } from 'history';
+import { PeerCertificate } from 'tls';
import { PublicMethodsOf } from '@kbn/utility-types';
import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types';
import React from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
+import { Request } from '@hapi/hapi';
import * as Rx from 'rxjs';
+import { SchemaTypeError } from '@kbn/config-schema';
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
@@ -39,6 +45,7 @@ import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiCounterMetricType } from '@kbn/analytics';
import { UnregisterCallback } from 'history';
+import { URL } from 'url';
import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types';
// @internal (undocumented)
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 0eb246b4c978b..43177a5ff188a 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -318,6 +318,8 @@ export {
SavedObjectsExportByObjectOptions,
SavedObjectsExportByTypeOptions,
SavedObjectsExportError,
+ SavedObjectsExportTransform,
+ SavedObjectsExportTransformContext,
SavedObjectsImporter,
ISavedObjectsImporter,
SavedObjectsImportError,
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
new file mode 100644
index 0000000000000..b1d0a35162524
--- /dev/null
+++ b/src/core/server/saved_objects/export/apply_export_transforms.test.ts
@@ -0,0 +1,300 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedObject } from '../../../types';
+import { KibanaRequest } from '../../http';
+import { httpServerMock } from '../../http/http_server.mocks';
+import { applyExportTransforms } from './apply_export_transforms';
+import { SavedObjectsExportTransform } from './types';
+
+const createObj = (
+ type: string,
+ id: string,
+ attributes: Record = {}
+): SavedObject => ({
+ type,
+ id,
+ attributes,
+ references: [],
+});
+
+const createTransform = (
+ implementation: SavedObjectsExportTransform = (ctx, objs) => objs
+): jest.MockedFunction => jest.fn(implementation);
+
+const expectedContext = {
+ request: expect.any(KibanaRequest),
+};
+
+describe('applyExportTransforms', () => {
+ let request: ReturnType;
+
+ beforeEach(() => {
+ request = httpServerMock.createKibanaRequest();
+ });
+
+ it('calls the transform functions with the correct parameters', async () => {
+ const foo1 = createObj('foo', '1');
+ const foo2 = createObj('foo', '2');
+ const bar1 = createObj('bar', '1');
+
+ const fooTransform = createTransform();
+ const barTransform = createTransform();
+
+ await applyExportTransforms({
+ request,
+ objects: [foo1, bar1, foo2],
+ transforms: {
+ foo: fooTransform,
+ bar: barTransform,
+ },
+ });
+
+ expect(fooTransform).toHaveBeenCalledTimes(1);
+ expect(fooTransform).toHaveBeenCalledWith(expectedContext, [foo1, foo2]);
+
+ expect(barTransform).toHaveBeenCalledTimes(1);
+ expect(barTransform).toHaveBeenCalledWith(expectedContext, [bar1]);
+ });
+
+ it('does not call the transform functions if no objects are present', async () => {
+ const foo1 = createObj('foo', '1');
+
+ const fooTransform = createTransform();
+ const barTransform = createTransform();
+
+ await applyExportTransforms({
+ request,
+ objects: [foo1],
+ transforms: {
+ foo: fooTransform,
+ bar: barTransform,
+ },
+ });
+
+ expect(fooTransform).toHaveBeenCalledTimes(1);
+ expect(fooTransform).toHaveBeenCalledWith(expectedContext, [foo1]);
+
+ expect(barTransform).not.toHaveBeenCalled();
+ });
+
+ it('allows to add objects to the export', async () => {
+ const foo1 = createObj('foo', '1');
+ const foo2 = createObj('foo', '2');
+ const bar1 = createObj('bar', '1');
+ const dolly1 = createObj('dolly', '1');
+ const hello1 = createObj('hello', '1');
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return [...objs, dolly1];
+ });
+ const barTransform = createTransform((ctx, objs) => {
+ return [...objs, hello1];
+ });
+
+ const result = await applyExportTransforms({
+ request,
+ objects: [foo1, bar1, foo2],
+ transforms: {
+ foo: fooTransform,
+ bar: barTransform,
+ },
+ });
+
+ expect(result).toEqual([foo1, foo2, dolly1, bar1, hello1]);
+ });
+
+ it('returns unmutated objects if no transform is defined for the type', async () => {
+ const foo1 = createObj('foo', '1');
+ const foo2 = createObj('foo', '2');
+ const bar1 = createObj('bar', '1');
+ const bar2 = createObj('bar', '2');
+ const dolly1 = createObj('dolly', '1');
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return [...objs, dolly1];
+ });
+
+ const result = await applyExportTransforms({
+ request,
+ objects: [foo1, foo2, bar1, bar2],
+ transforms: {
+ foo: fooTransform,
+ },
+ });
+
+ expect(result).toEqual([foo1, foo2, dolly1, bar1, bar2]);
+ });
+
+ it('allows to mutate objects', async () => {
+ const foo1 = createObj('foo', '1', { enabled: true });
+ const foo2 = createObj('foo', '2', { enabled: true });
+
+ const disableFoo = (obj: SavedObject) => ({
+ ...obj,
+ attributes: {
+ ...obj.attributes,
+ enabled: false,
+ },
+ });
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return objs.map(disableFoo);
+ });
+
+ const result = await applyExportTransforms({
+ request,
+ objects: [foo1, foo2],
+ transforms: {
+ foo: fooTransform,
+ },
+ });
+
+ expect(result).toEqual([foo1, foo2].map(disableFoo));
+ });
+
+ it('supports async transforms', async () => {
+ const foo1 = createObj('foo', '1');
+ const bar1 = createObj('bar', '1');
+ const dolly1 = createObj('dolly', '1');
+ const hello1 = createObj('hello', '1');
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return Promise.resolve([...objs, dolly1]);
+ });
+
+ const barTransform = createTransform((ctx, objs) => {
+ return [...objs, hello1];
+ });
+
+ const result = await applyExportTransforms({
+ request,
+ objects: [foo1, bar1],
+ transforms: {
+ foo: fooTransform,
+ bar: barTransform,
+ },
+ });
+
+ expect(result).toEqual([foo1, dolly1, bar1, hello1]);
+ });
+
+ it('uses the provided sortFunction when provided', async () => {
+ const foo1 = createObj('foo', 'A');
+ const bar1 = createObj('bar', 'B');
+ const dolly1 = createObj('dolly', 'C');
+ const hello1 = createObj('hello', 'D');
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return [...objs, dolly1];
+ });
+
+ const barTransform = createTransform((ctx, objs) => {
+ return [...objs, hello1];
+ });
+
+ const result = await applyExportTransforms({
+ request,
+ objects: [foo1, bar1],
+ transforms: {
+ foo: fooTransform,
+ bar: barTransform,
+ },
+ sortFunction: (obj1, obj2) => (obj1.id > obj2.id ? 1 : -1),
+ });
+
+ expect(result).toEqual([foo1, bar1, dolly1, hello1]);
+ });
+
+ it('throws when removing objects', async () => {
+ const foo1 = createObj('foo', '1', { enabled: true });
+ const foo2 = createObj('foo', '2', { enabled: true });
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return [objs[0]];
+ });
+
+ await expect(
+ applyExportTransforms({
+ request,
+ objects: [foo1, foo2],
+ transforms: {
+ foo: fooTransform,
+ },
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Invalid transform performed on objects to export"`
+ );
+ });
+
+ it('throws when changing the object type', async () => {
+ const foo1 = createObj('foo', '1', { enabled: true });
+ const foo2 = createObj('foo', '2', { enabled: true });
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return objs.map((obj) => ({
+ ...obj,
+ type: 'mutated',
+ }));
+ });
+
+ await expect(
+ applyExportTransforms({
+ request,
+ objects: [foo1, foo2],
+ transforms: {
+ foo: fooTransform,
+ },
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Invalid transform performed on objects to export"`
+ );
+ });
+
+ it('throws when changing the object id', async () => {
+ const foo1 = createObj('foo', '1', { enabled: true });
+ const foo2 = createObj('foo', '2', { enabled: true });
+
+ const fooTransform = createTransform((ctx, objs) => {
+ return objs.map((obj, idx) => ({
+ ...obj,
+ id: `mutated-${idx}`,
+ }));
+ });
+
+ await expect(
+ applyExportTransforms({
+ request,
+ objects: [foo1, foo2],
+ transforms: {
+ foo: fooTransform,
+ },
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Invalid transform performed on objects to export"`
+ );
+ });
+
+ it('throws if the transform function throws', async () => {
+ const foo1 = createObj('foo', '1');
+
+ const fooTransform = createTransform(() => {
+ throw new Error('oups.');
+ });
+
+ await expect(
+ applyExportTransforms({
+ request,
+ objects: [foo1],
+ transforms: {
+ 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
new file mode 100644
index 0000000000000..0297fe201ef61
--- /dev/null
+++ b/src/core/server/saved_objects/export/apply_export_transforms.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedObject } from '../../../types';
+import { KibanaRequest } from '../../http';
+import { SavedObjectsExportError } from './errors';
+import { SavedObjectsExportTransform, SavedObjectsExportTransformContext } from './types';
+import { getObjKey, SavedObjectComparator } from './utils';
+
+interface ApplyExportTransformsOptions {
+ objects: SavedObject[];
+ request: KibanaRequest;
+ transforms: Record;
+ sortFunction?: SavedObjectComparator;
+}
+
+export const applyExportTransforms = async ({
+ objects,
+ request,
+ transforms,
+ sortFunction,
+}: ApplyExportTransformsOptions): Promise => {
+ const context = createContext(request);
+ const byType = splitByType(objects);
+
+ let finalObjects: SavedObject[] = [];
+ for (const [type, typeObjs] of Object.entries(byType)) {
+ const typeTransformFn = transforms[type];
+ if (typeTransformFn) {
+ finalObjects = [
+ ...finalObjects,
+ ...(await applyTransform(typeObjs, typeTransformFn, context)),
+ ];
+ } else {
+ finalObjects = [...finalObjects, ...typeObjs];
+ }
+ }
+
+ if (sortFunction) {
+ finalObjects.sort(sortFunction);
+ }
+
+ return finalObjects;
+};
+
+const applyTransform = async (
+ objs: SavedObject[],
+ transformFn: SavedObjectsExportTransform,
+ context: SavedObjectsExportTransformContext
+) => {
+ const objKeys = objs.map(getObjKey);
+ let transformedObjects: SavedObject[];
+ try {
+ transformedObjects = await transformFn(context, objs);
+ } catch (e) {
+ throw SavedObjectsExportError.objectTransformError(objs, e);
+ }
+ assertValidTransform(transformedObjects, objKeys);
+ return transformedObjects;
+};
+
+const createContext = (request: KibanaRequest): SavedObjectsExportTransformContext => {
+ return {
+ request,
+ };
+};
+
+const splitByType = (objects: SavedObject[]): Record => {
+ return objects.reduce((memo, obj) => {
+ memo[obj.type] = [...(memo[obj.type] ?? []), obj];
+ return memo;
+ }, {} as Record);
+};
+
+const assertValidTransform = (transformedObjects: SavedObject[], initialKeys: string[]) => {
+ const transformedKeys = transformedObjects.map(getObjKey);
+ const missingKeys: string[] = [];
+ initialKeys.forEach((initialKey) => {
+ if (!transformedKeys.includes(initialKey)) {
+ missingKeys.push(initialKey);
+ }
+ });
+ if (missingKeys.length) {
+ throw SavedObjectsExportError.invalidTransformError(missingKeys);
+ }
+};
diff --git a/src/core/server/saved_objects/export/errors.ts b/src/core/server/saved_objects/export/errors.ts
index 96a729846f6b5..5720f3b2daf3e 100644
--- a/src/core/server/saved_objects/export/errors.ts
+++ b/src/core/server/saved_objects/export/errors.ts
@@ -36,4 +36,32 @@ export class SavedObjectsExportError extends Error {
objects,
});
}
+
+ /**
+ * Error returned when a {@link SavedObjectsExportTransform | export tranform} threw an error
+ */
+ static objectTransformError(objects: SavedObject[], cause: Error) {
+ return new SavedObjectsExportError(
+ 'object-transform-error',
+ 'Error transforming objects to export',
+ {
+ objects,
+ cause: cause.message,
+ }
+ );
+ }
+
+ /**
+ * Error returned when a {@link SavedObjectsExportTransform | export tranform} performed an invalid operation
+ * during the transform, such as removing objects from the export, or changing an object's type or id.
+ */
+ static invalidTransformError(objectKeys: string[]) {
+ return new SavedObjectsExportError(
+ 'invalid-transform-error',
+ 'Invalid transform performed on objects to export',
+ {
+ objectKeys,
+ }
+ );
+ }
}
diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts
index 386b8c208ad6d..8ac2e68819c46 100644
--- a/src/core/server/saved_objects/export/index.ts
+++ b/src/core/server/saved_objects/export/index.ts
@@ -11,6 +11,8 @@ export {
SavedObjectExportBaseOptions,
SavedObjectsExportByTypeOptions,
SavedObjectsExportResultDetails,
+ SavedObjectsExportTransformContext,
+ SavedObjectsExportTransform,
} from './types';
export { ISavedObjectsExporter, SavedObjectsExporter } from './saved_objects_exporter';
export { SavedObjectsExportError } from './errors';
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 e7d37280762fd..346f14cbeb071 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
@@ -6,24 +6,30 @@
* Public License, v 1.
*/
+import type { SavedObject } from '../../../types';
import { SavedObjectsExporter } from './saved_objects_exporter';
import { savedObjectsClientMock } from '../service/saved_objects_client.mock';
+import { SavedObjectTypeRegistry } from '../saved_objects_type_registry';
+import { httpServerMock } from '../../http/http_server.mocks';
import { Readable } from 'stream';
import { createPromiseFromStreams, createConcatStream } from '@kbn/utils';
-async function readStreamToCompletion(stream: Readable) {
+async function readStreamToCompletion(stream: Readable): Promise>> {
return createPromiseFromStreams([stream, createConcatStream([])]);
}
const exportSizeLimit = 500;
+const request = httpServerMock.createKibanaRequest();
describe('getSortedObjectsForExport()', () => {
let savedObjectsClient: ReturnType;
+ let typeRegistry: SavedObjectTypeRegistry;
let exporter: SavedObjectsExporter;
beforeEach(() => {
+ typeRegistry = new SavedObjectTypeRegistry();
savedObjectsClient = savedObjectsClientMock.create();
- exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit });
+ exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit, typeRegistry });
});
describe('#exportByTypes', () => {
@@ -56,6 +62,7 @@ describe('getSortedObjectsForExport()', () => {
page: 0,
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
});
@@ -115,6 +122,52 @@ describe('getSortedObjectsForExport()', () => {
`);
});
+ test('applies the export transforms', async () => {
+ typeRegistry.registerType({
+ name: 'foo',
+ mappings: { properties: {} },
+ namespaceType: 'single',
+ hidden: false,
+ management: {
+ importableAndExportable: true,
+ onExport: (ctx, objects) => {
+ objects.forEach((obj: SavedObject) => {
+ obj.attributes.foo = 'modified';
+ });
+ return objects;
+ },
+ },
+ });
+ exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit, typeRegistry });
+
+ savedObjectsClient.find.mockResolvedValueOnce({
+ total: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'foo',
+ attributes: {
+ foo: 'initial',
+ },
+ score: 0,
+ references: [],
+ },
+ ],
+ per_page: 1,
+ page: 0,
+ });
+ const exportStream = await exporter.exportByTypes({
+ request,
+ types: ['foo'],
+ excludeExportDetails: true,
+ });
+
+ const response = await readStreamToCompletion(exportStream);
+
+ expect(response).toHaveLength(1);
+ expect(response[0].attributes.foo).toEqual('modified');
+ });
+
test('omits the `namespaces` property from the export', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
@@ -146,6 +199,7 @@ describe('getSortedObjectsForExport()', () => {
page: 0,
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
});
@@ -234,6 +288,7 @@ describe('getSortedObjectsForExport()', () => {
page: 0,
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
excludeExportDetails: true,
});
@@ -293,6 +348,7 @@ describe('getSortedObjectsForExport()', () => {
page: 0,
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
search: 'foo',
});
@@ -375,6 +431,7 @@ describe('getSortedObjectsForExport()', () => {
page: 0,
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
hasReference: [
{
@@ -468,6 +525,7 @@ describe('getSortedObjectsForExport()', () => {
page: 0,
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
namespace: 'foo',
});
@@ -531,7 +589,7 @@ describe('getSortedObjectsForExport()', () => {
});
test('export selected types throws error when exceeding exportSizeLimit', async () => {
- exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1 });
+ exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1, typeRegistry });
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
@@ -562,6 +620,7 @@ describe('getSortedObjectsForExport()', () => {
});
await expect(
exporter.exportByTypes({
+ request,
types: ['index-pattern', 'search'],
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`);
@@ -603,6 +662,7 @@ describe('getSortedObjectsForExport()', () => {
],
});
const exportStream = await exporter.exportByTypes({
+ request,
types: ['index-pattern'],
});
const response = await readStreamToCompletion(exportStream);
@@ -667,6 +727,7 @@ describe('getSortedObjectsForExport()', () => {
],
});
const exportStream = await exporter.exportByObjects({
+ request,
objects: [
{
type: 'index-pattern',
@@ -759,6 +820,7 @@ describe('getSortedObjectsForExport()', () => {
});
await expect(
exporter.exportByObjects({
+ request,
objects: [
{
type: 'index-pattern',
@@ -774,9 +836,10 @@ describe('getSortedObjectsForExport()', () => {
});
test('export selected objects throws error when exceeding exportSizeLimit', async () => {
- exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1 });
+ exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1, typeRegistry });
const exportOpts = {
+ request,
objects: [
{
type: 'index-pattern',
@@ -803,6 +866,7 @@ describe('getSortedObjectsForExport()', () => {
],
});
const exportStream = await exporter.exportByObjects({
+ request,
objects: [
{ type: 'multi', id: '1' },
{ type: 'multi', id: '2' },
@@ -846,6 +910,7 @@ describe('getSortedObjectsForExport()', () => {
],
});
const exportStream = await exporter.exportByObjects({
+ request,
objects: [
{
type: 'search',
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 7588f13a57644..bd3e60fc1a140 100644
--- a/src/core/server/saved_objects/export/saved_objects_exporter.ts
+++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts
@@ -9,6 +9,7 @@
import { createListStream } from '@kbn/utils';
import { PublicMethodsOf } from '@kbn/utility-types';
import { SavedObject, SavedObjectsClientContract } from '../types';
+import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
import { fetchNestedDependencies } from './fetch_nested_dependencies';
import { sortObjects } from './sort_objects';
import {
@@ -16,8 +17,11 @@ import {
SavedObjectExportBaseOptions,
SavedObjectsExportByObjectOptions,
SavedObjectsExportByTypeOptions,
+ SavedObjectsExportTransform,
} from './types';
import { SavedObjectsExportError } from './errors';
+import { applyExportTransforms } from './apply_export_transforms';
+import { byIdAscComparator, getPreservedOrderComparator, SavedObjectComparator } from './utils';
/**
* @public
@@ -29,17 +33,29 @@ export type ISavedObjectsExporter = PublicMethodsOf;
*/
export class SavedObjectsExporter {
readonly #savedObjectsClient: SavedObjectsClientContract;
+ readonly #exportTransforms: Record;
readonly #exportSizeLimit: number;
constructor({
savedObjectsClient,
+ typeRegistry,
exportSizeLimit,
}: {
savedObjectsClient: SavedObjectsClientContract;
+ typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
}) {
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);
}
/**
@@ -51,7 +67,8 @@ export class SavedObjectsExporter {
*/
public async exportByTypes(options: SavedObjectsExportByTypeOptions) {
const objects = await this.fetchByTypes(options);
- return this.processObjects(objects, {
+ return this.processObjects(objects, byIdAscComparator, {
+ request: options.request,
includeReferencesDeep: options.includeReferencesDeep,
excludeExportDetails: options.excludeExportDetails,
namespace: options.namespace,
@@ -70,7 +87,9 @@ export class SavedObjectsExporter {
throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit);
}
const objects = await this.fetchByObjects(options);
- return this.processObjects(objects, {
+ const comparator = getPreservedOrderComparator(objects);
+ return this.processObjects(objects, comparator, {
+ request: options.request,
includeReferencesDeep: options.includeReferencesDeep,
excludeExportDetails: options.excludeExportDetails,
namespace: options.namespace,
@@ -79,7 +98,9 @@ export class SavedObjectsExporter {
private async processObjects(
savedObjects: SavedObject[],
+ sortFunction: SavedObjectComparator,
{
+ request,
excludeExportDetails = false,
includeReferencesDeep = false,
namespace,
@@ -88,6 +109,13 @@ export class SavedObjectsExporter {
let exportedObjects: Array>;
let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = [];
+ savedObjects = await applyExportTransforms({
+ request,
+ objects: savedObjects,
+ transforms: this.#exportTransforms,
+ sortFunction,
+ });
+
if (includeReferencesDeep) {
const fetchResult = await fetchNestedDependencies(
savedObjects,
@@ -145,7 +173,7 @@ export class SavedObjectsExporter {
findResponse.saved_objects
// exclude the find-specific `score` property from the exported objects
.map(({ score, ...obj }) => obj)
- .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1))
+ .sort(byIdAscComparator)
);
}
}
diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts
index d8d162e51c294..bf7b265e45d29 100644
--- a/src/core/server/saved_objects/export/types.ts
+++ b/src/core/server/saved_objects/export/types.ts
@@ -6,10 +6,13 @@
* Public License, v 1.
*/
-import { SavedObjectsFindOptionsReference } from '../types';
+import { KibanaRequest } from '../../http';
+import { SavedObject, SavedObjectsFindOptionsReference } from '../types';
/** @public */
export interface SavedObjectExportBaseOptions {
+ /** The http request initiating the export. */
+ request: KibanaRequest;
/** flag to also include all related saved objects in the export stream. */
includeReferencesDeep?: boolean;
/** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */
@@ -64,3 +67,92 @@ export interface SavedObjectsExportResultDetails {
type: string;
}>;
}
+
+/**
+ * Context passed down to a {@link SavedObjectsExportTransform | export transform function}
+ *
+ * @public
+ */
+export interface SavedObjectsExportTransformContext {
+ /**
+ * The request that initiated the export request. Can be used to create scoped
+ * services or client inside the {@link SavedObjectsExportTransform | transformation}
+ */
+ request: KibanaRequest;
+}
+
+/**
+ * Transformation function used to mutate the exported objects of the associated type.
+ *
+ * A type's export transform function will be executed once per user-initiated export,
+ * for all objects of that type.
+ *
+ * @example
+ * Registering a transform function changing the object's attributes during the export
+ * ```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,
+ * onExport: (ctx, objects) => {
+ * return objects.map((obj) => ({
+ * ...obj,
+ * attributes: {
+ * ...obj.attributes,
+ * enabled: false,
+ * }
+ * })
+ * }
+ * },
+ * });
+ * }
+ * }
+ * ```
+ *
+ * @example
+ * Registering a transform function adding additional objects to the export
+ * ```ts
+ * // src/plugins/my_plugin/server/plugin.ts
+ * import { myType } from './saved_objects';
+ *
+ * export class Plugin() {
+ * setup: (core: CoreSetup) => {
+ * const savedObjectStartContractPromise = getStartServices().then(
+ * ([{ savedObjects: savedObjectsStart }]) => savedObjectsStart
+ * );
+ *
+ * core.savedObjects.registerType({
+ * ...myType,
+ * management: {
+ * ...myType.management,
+ * onExport: async (ctx, objects) => {
+ * const { getScopedClient } = await savedObjectStartContractPromise;
+ * const client = getScopedClient(ctx.request);
+ *
+ * const depResponse = await client.find({
+ * type: 'my-nested-object',
+ * hasReference: objs.map(({ id, type }) => ({ id, type })),
+ * });
+ *
+ * return [...objs, ...depResponse.saved_objects];
+ * }
+ * },
+ * });
+ * }
+ * }
+ * ```
+ *
+ * @remarks Trying to change an object's id or type during the transform will result in
+ * a runtime error during the export process.
+ *
+ * @public
+ */
+export type SavedObjectsExportTransform = (
+ context: SavedObjectsExportTransformContext,
+ objects: Array>
+) => SavedObject[] | Promise;
diff --git a/src/core/server/saved_objects/export/utils.test.ts b/src/core/server/saved_objects/export/utils.test.ts
new file mode 100644
index 0000000000000..c547aa2271cf0
--- /dev/null
+++ b/src/core/server/saved_objects/export/utils.test.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { byIdAscComparator, getPreservedOrderComparator } from './utils';
+import { SavedObject } from '../../../types';
+
+const createObj = (id: string): SavedObject => ({
+ id,
+ type: 'dummy',
+ attributes: {},
+ references: [],
+});
+
+describe('byIdAscComparator', () => {
+ it('sorts the objects by id asc', () => {
+ const objs = [createObj('delta'), createObj('alpha'), createObj('beta')];
+
+ objs.sort(byIdAscComparator);
+
+ expect(objs.map((obj) => obj.id)).toEqual(['alpha', 'beta', 'delta']);
+ });
+});
+
+describe('getPreservedOrderComparator', () => {
+ it('sorts objects depending on the order of the provided list', () => {
+ const objA = createObj('A');
+ const objB = createObj('B');
+ const objC = createObj('C');
+
+ const comparator = getPreservedOrderComparator([objA, objB, objC]);
+
+ const objs = [objC, objA, objB];
+ objs.sort(comparator);
+
+ expect(objs.map((obj) => obj.id)).toEqual(['A', 'B', 'C']);
+ });
+
+ it('appends unknown objects at the end of the list and sort them by id', () => {
+ const objA = createObj('A');
+ const objB = createObj('B');
+ const objC = createObj('C');
+ const addedA = createObj('addedA');
+ const addedB = createObj('addedB');
+
+ const comparator = getPreservedOrderComparator([objA, objB, objC]);
+
+ const objs = [addedB, objC, addedA, objA, objB];
+ objs.sort(comparator);
+
+ expect(objs.map((obj) => obj.id)).toEqual(['A', 'B', 'C', 'addedA', 'addedB']);
+ });
+});
diff --git a/src/core/server/saved_objects/export/utils.ts b/src/core/server/saved_objects/export/utils.ts
new file mode 100644
index 0000000000000..e8567c6da1dca
--- /dev/null
+++ b/src/core/server/saved_objects/export/utils.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedObject } from '../../../types';
+
+export type SavedObjectComparator = (a: SavedObject, b: SavedObject) => number;
+
+export const getObjKey = (obj: SavedObject) => `${obj.type}|${obj.id}`;
+
+export const byIdAscComparator: SavedObjectComparator = (a: SavedObject, b: SavedObject) =>
+ a.id > b.id ? 1 : -1;
+
+/**
+ * Create a comparator that will sort objects depending on their position in the provided array.
+ * Objects not present in the array will be appended at the end of the list, and sorted by id asc.
+ *
+ * @example
+ * ```ts
+ * const comparator = getPreservedOrderComparator([objA, objB, objC]);
+ * const list = [newB, objB, objC, newA, objA]; // with obj.title matching their variable name
+ * list.sort()
+ * // list = [objA, objB, objC, newA, newB]
+ * ```
+ */
+export const getPreservedOrderComparator = (objects: SavedObject[]): SavedObjectComparator => {
+ const orderedKeys = objects.map(getObjKey);
+ return (a: SavedObject, b: SavedObject) => {
+ const indexA = orderedKeys.indexOf(getObjKey(a));
+ const indexB = orderedKeys.indexOf(getObjKey(b));
+ if (indexA > -1 && indexB > -1) {
+ return indexA - indexB > 0 ? 1 : -1;
+ }
+ if (indexA > -1) {
+ return -1;
+ }
+ if (indexB > -1) {
+ return 1;
+ }
+ return byIdAscComparator(a, b);
+ };
+};
diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts
index 57dee5cd51f1d..0544140e0451c 100644
--- a/src/core/server/saved_objects/index.ts
+++ b/src/core/server/saved_objects/index.ts
@@ -38,6 +38,8 @@ export {
SavedObjectsExportByObjectOptions,
SavedObjectsExportResultDetails,
SavedObjectsExportError,
+ SavedObjectsExportTransformContext,
+ SavedObjectsExportTransform,
} from './export';
export {
diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts
index aac3f98898e42..9b40855afec2e 100644
--- a/src/core/server/saved_objects/routes/export.ts
+++ b/src/core/server/saved_objects/routes/export.ts
@@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema';
import stringify from 'json-stable-stringify';
import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils';
-import { IRouter } from '../../http';
+import { IRouter, KibanaRequest } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import {
@@ -78,7 +78,11 @@ const validateOptions = (
includeReferencesDeep,
search,
}: ExportOptions,
- { exportSizeLimit, supportedTypes }: { exportSizeLimit: number; supportedTypes: string[] }
+ {
+ exportSizeLimit,
+ supportedTypes,
+ request,
+ }: { exportSizeLimit: number; supportedTypes: string[]; request: KibanaRequest }
): EitherExportOptions => {
const hasTypes = (types?.length ?? 0) > 0;
const hasObjects = (objects?.length ?? 0) > 0;
@@ -106,6 +110,7 @@ const validateOptions = (
objects: objects!,
excludeExportDetails,
includeReferencesDeep,
+ request,
};
} else {
const validationError = validateTypes(types!, supportedTypes);
@@ -118,6 +123,7 @@ const validateOptions = (
search,
excludeExportDetails,
includeReferencesDeep,
+ request,
};
}
};
@@ -165,6 +171,7 @@ export const registerExportRoute = (
let options: EitherExportOptions;
try {
options = validateOptions(cleaned, {
+ request: req,
exportSizeLimit: maxImportExportSize,
supportedTypes,
});
diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts
index 6db4cf4f781b4..729d669df0b31 100644
--- a/src/core/server/saved_objects/saved_objects_service.ts
+++ b/src/core/server/saved_objects/saved_objects_service.ts
@@ -454,6 +454,7 @@ export class SavedObjectsService
createExporter: (savedObjectsClient) =>
new SavedObjectsExporter({
savedObjectsClient,
+ typeRegistry: this.typeRegistry,
exportSizeLimit: this.config!.maxImportExportSize,
}),
createImporter: (savedObjectsClient) =>
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
index 7c91baa73c676..0186af6e7628d 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
@@ -25,25 +25,68 @@ describe('SavedObjectTypeRegistry', () => {
registry = new SavedObjectTypeRegistry();
});
- it('allows to register types', () => {
- registry.registerType(createType({ name: 'typeA' }));
- registry.registerType(createType({ name: 'typeB' }));
- registry.registerType(createType({ name: 'typeC' }));
-
- expect(
- registry
- .getAllTypes()
- .map((type) => type.name)
- .sort()
- ).toEqual(['typeA', 'typeB', 'typeC']);
- });
+ describe('#registerType', () => {
+ it('allows to register types', () => {
+ registry.registerType(createType({ name: 'typeA' }));
+ registry.registerType(createType({ name: 'typeB' }));
+ registry.registerType(createType({ name: 'typeC' }));
+
+ expect(
+ registry
+ .getAllTypes()
+ .map((type) => type.name)
+ .sort()
+ ).toEqual(['typeA', 'typeB', 'typeC']);
+ });
- it('throws when trying to register the same type twice', () => {
- registry.registerType(createType({ name: 'typeA' }));
- registry.registerType(createType({ name: 'typeB' }));
- expect(() => {
+ it('throws when trying to register the same type twice', () => {
registry.registerType(createType({ name: 'typeA' }));
- }).toThrowErrorMatchingInlineSnapshot(`"Type 'typeA' is already registered"`);
+ registry.registerType(createType({ name: 'typeB' }));
+ expect(() => {
+ registry.registerType(createType({ name: 'typeA' }));
+ }).toThrowErrorMatchingInlineSnapshot(`"Type 'typeA' is already registered"`);
+ });
+
+ it('throws when `management.onExport` is specified but `management.importableAndExportable` is undefined or false', () => {
+ expect(() => {
+ registry.registerType(
+ createType({
+ name: 'typeA',
+ management: {
+ onExport: (ctx, objs) => objs,
+ },
+ })
+ );
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Type typeA: 'management.importableAndExportable' must be 'true' when specifying 'management.onExport'"`
+ );
+ expect(() => {
+ registry.registerType(
+ createType({
+ name: 'typeA',
+ management: {
+ importableAndExportable: false,
+ onExport: (ctx, objs) => objs,
+ },
+ })
+ );
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Type typeA: 'management.importableAndExportable' must be 'true' when specifying 'management.onExport'"`
+ );
+ expect(() => {
+ registry.registerType(
+ createType({
+ name: 'typeA',
+ management: {
+ importableAndExportable: true,
+ onExport: (ctx, objs) => objs,
+ },
+ })
+ );
+ }).not.toThrow();
+ });
+
+ // TODO: same test with 'onImport'
});
describe('#getType', () => {
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts
index 2194353a07583..d2cee700bf66d 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.ts
@@ -32,6 +32,7 @@ export class SavedObjectTypeRegistry {
if (this.types.has(type.name)) {
throw new Error(`Type '${type.name}' is already registered`);
}
+ validateType(type);
this.types.set(type.name, deepFreeze(type));
}
@@ -116,3 +117,13 @@ export class SavedObjectTypeRegistry {
return this.types.get(type)?.management?.importableAndExportable ?? false;
}
}
+
+const validateType = ({ name, management }: SavedObjectsType) => {
+ if (management) {
+ if (management.onExport && !management.importableAndExportable) {
+ throw new Error(
+ `Type ${name}: 'management.importableAndExportable' must be 'true' when specifying 'management.onExport'`
+ );
+ }
+ }
+};
diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts
index cbd8b415d9d31..ef540f548604a 100644
--- a/src/core/server/saved_objects/types.ts
+++ b/src/core/server/saved_objects/types.ts
@@ -9,6 +9,7 @@
import { SavedObjectsClient } from './service/saved_objects_client';
import { SavedObjectsTypeMappingDefinition } from './mappings';
import { SavedObjectMigrationMap } from './migrations';
+import { SavedObjectsExportTransform } from './export';
import { SavedObjectsImportHook } from './import/types';
export {
@@ -285,6 +286,17 @@ export interface SavedObjectsTypeManagementDefinition {
* {@link Capabilities | uiCapabilities} to check if the user has permission to access the object.
*/
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.
+ *
+ * It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.
+ *
+ * See {@link SavedObjectsExportTransform | the transform type documentation} for more info and examples.
+ *
+ * @remarks `importableAndExportable` must be `true` to specify this property.
+ */
+ onExport?: SavedObjectsExportTransform;
/**
* An optional {@link SavedObjectsImportHook | import hook} to use when importing given type.
*
@@ -324,7 +336,8 @@ export interface SavedObjectsTypeManagementDefinition {
* }
* ```
*
- * @remark messages returned in the warnings are user facing and must be translated.
+ * @remarks messages returned in the warnings are user facing and must be translated.
+ * @remarks `importableAndExportable` must be `true` to specify this property.
*/
onImport?: SavedObjectsImportHook;
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 7582dbdef54ab..2630f4e170b09 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2063,6 +2063,7 @@ export interface SavedObjectExportBaseOptions {
excludeExportDetails?: boolean;
includeReferencesDeep?: boolean;
namespace?: string;
+ request: KibanaRequest;
}
// @public
@@ -2384,8 +2385,9 @@ export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOp
export class SavedObjectsExporter {
// (undocumented)
#private;
- constructor({ savedObjectsClient, exportSizeLimit, }: {
+ constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: {
savedObjectsClient: SavedObjectsClientContract;
+ typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
});
exportByObjects(options: SavedObjectsExportByObjectOptions): Promise;
@@ -2399,8 +2401,10 @@ export class SavedObjectsExportError extends Error {
readonly attributes?: Record | undefined;
// (undocumented)
static exportSizeExceeded(limit: number): SavedObjectsExportError;
+ static invalidTransformError(objectKeys: string[]): SavedObjectsExportError;
// (undocumented)
static objectFetchError(objects: SavedObject[]): SavedObjectsExportError;
+ static objectTransformError(objects: SavedObject[], cause: Error): SavedObjectsExportError;
// (undocumented)
readonly type: string;
}
@@ -2415,6 +2419,14 @@ export interface SavedObjectsExportResultDetails {
}>;
}
+// @public
+export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise;
+
+// @public
+export interface SavedObjectsExportTransformContext {
+ request: KibanaRequest;
+}
+
// @public
export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjectsComplexFieldMapping;
@@ -2818,6 +2830,7 @@ export interface SavedObjectsTypeManagementDefinition {
getTitle?: (savedObject: SavedObject) => string;
icon?: string;
importableAndExportable?: boolean;
+ onExport?: SavedObjectsExportTransform;
onImport?: SavedObjectsImportHook;
}
diff --git a/src/core/server/types.ts b/src/core/server/types.ts
index 900b4bd3f4b9f..74f9fb65db54d 100644
--- a/src/core/server/types.ts
+++ b/src/core/server/types.ts
@@ -8,7 +8,34 @@
/** This module is intended for consumption by public to avoid import issues with server-side code */
export { PluginOpaqueId } from './plugins/types';
-export * from './saved_objects/types';
+export type {
+ SavedObjectsImportResponse,
+ SavedObjectsImportSuccess,
+ SavedObjectsImportConflictError,
+ SavedObjectsImportAmbiguousConflictError,
+ SavedObjectsImportUnsupportedTypeError,
+ SavedObjectsImportMissingReferencesError,
+ SavedObjectsImportUnknownError,
+ SavedObjectsImportFailure,
+ SavedObjectsImportRetry,
+ SavedObjectsImportWarning,
+ SavedObjectsImportActionRequiredWarning,
+ SavedObjectsImportSimpleWarning,
+ SavedObjectAttributes,
+ SavedObjectAttribute,
+ SavedObjectAttributeSingle,
+ SavedObject,
+ SavedObjectError,
+ SavedObjectReference,
+ SavedObjectsMigrationVersion,
+ SavedObjectStatusMeta,
+ SavedObjectsFindOptionsReference,
+ SavedObjectsFindOptions,
+ SavedObjectsBaseOptions,
+ MutatingOperationRefreshSetting,
+ SavedObjectsClientContract,
+ SavedObjectsNamespaceType,
+} from './saved_objects/types';
export * from './ui_settings/types';
export * from './legacy/types';
export type { EnvironmentMode, PackageInfo } from '@kbn/config';
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 5e4778d1e4630..6fd29d01a2036 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -24,6 +24,7 @@ import * as CSS from 'csstype';
import { Datatable as Datatable_2 } from 'src/plugins/expressions';
import { Datatable as Datatable_3 } from 'src/plugins/expressions/common';
import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions';
+import { DetailedPeerCertificate } from 'tls';
import { Ensure } from '@kbn/utility-types';
import { EnvironmentMode } from '@kbn/config';
import { ErrorToastOptions } from 'src/core/public/notifications';
@@ -45,6 +46,7 @@ import { History } from 'history';
import { Href } from 'history';
import { HttpSetup } from 'kibana/public';
import { IconType } from '@elastic/eui';
+import { IncomingHttpHeaders } from 'http';
import { InjectedIntl } from '@kbn/i18n/react';
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';
import { ISearchSource as ISearchSource_2 } from 'src/plugins/data/public';
@@ -60,9 +62,11 @@ import { METRIC_TYPE } from '@kbn/analytics';
import { Moment } from 'moment';
import moment from 'moment';
import { NameList } from 'elasticsearch';
+import { ObjectType } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import { Path } from 'history';
+import { PeerCertificate } from 'tls';
import { Plugin as Plugin_2 } from 'src/core/public';
import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public';
import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public';
@@ -75,6 +79,7 @@ import React from 'react';
import * as React_3 from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Reporter } from '@kbn/analytics';
+import { Request as Request_2 } from '@hapi/hapi';
import { RequestAdapter } from 'src/plugins/inspector/common';
import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common';
import { Required } from '@kbn/utility-types';
@@ -85,6 +90,7 @@ import { SavedObjectReference } from 'src/core/types';
import { SavedObjectsClientContract } from 'src/core/public';
import { SavedObjectsFindOptions } from 'kibana/public';
import { SavedObjectsFindResponse } from 'kibana/server';
+import { SchemaTypeError } from '@kbn/config-schema';
import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchResponse } from 'elasticsearch';
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
@@ -94,12 +100,14 @@ import { ToastsSetup } from 'kibana/public';
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
+import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { UiCounterMetricType } from '@kbn/analytics';
import { Unit } from '@elastic/datemath';
import { UnregisterCallback } from 'history';
+import { URL } from 'url';
import { UserProvidedValues } from 'src/core/server/types';
// Warning: (ae-missing-release-tag) "ACTION_GLOBAL_APPLY_FILTER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index f41d92dfb65a4..2f9b43121b45a 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -12,6 +12,7 @@ import { ApplicationStart as ApplicationStart_2 } from 'kibana/public';
import Boom from '@hapi/boom';
import { ConfigDeprecationProvider } from '@kbn/config';
import * as CSS from 'csstype';
+import { DetailedPeerCertificate } from 'tls';
import { EmbeddableStart as EmbeddableStart_2 } from 'src/plugins/embeddable/public/plugin';
import { EnvironmentMode } from '@kbn/config';
import { EuiBreadcrumb } from '@elastic/eui';
@@ -25,6 +26,7 @@ import { History } from 'history';
import { Href } from 'history';
import { I18nStart as I18nStart_2 } from 'src/core/public';
import { IconType } from '@elastic/eui';
+import { IncomingHttpHeaders } from 'http';
import { KibanaClient } from '@elastic/elasticsearch/api/kibana';
import { Location } from 'history';
import { LocationDescriptorObject } from 'history';
@@ -32,30 +34,36 @@ import { Logger } from '@kbn/logging';
import { LogMeta } from '@kbn/logging';
import { MaybePromise } from '@kbn/utility-types';
import { NotificationsStart as NotificationsStart_2 } from 'src/core/public';
+import { ObjectType } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { Optional } from '@kbn/utility-types';
import { OverlayRef as OverlayRef_2 } from 'src/core/public';
import { OverlayStart as OverlayStart_2 } from 'src/core/public';
import { PackageInfo } from '@kbn/config';
import { Path } from 'history';
+import { PeerCertificate } from 'tls';
import { PluginInitializerContext } from 'src/core/public';
import * as PropTypes from 'prop-types';
import { PublicMethodsOf } from '@kbn/utility-types';
import { PublicUiSettingsParams } from 'src/core/server/types';
import React from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
+import { Request } from '@hapi/hapi';
import * as Rx from 'rxjs';
import { SavedObjectAttributes } from 'kibana/server';
import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public';
import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public';
+import { SchemaTypeError } from '@kbn/config-schema';
import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public';
import { Start as Start_2 } from 'src/plugins/inspector/public';
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
+import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiComponent } from 'src/plugins/kibana_utils/public';
import { UnregisterCallback } from 'history';
+import { URL } from 'url';
import { UserProvidedValues } from 'src/core/server/types';
// Warning: (ae-missing-release-tag) "ACTION_ADD_PANEL" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json
new file mode 100644
index 0000000000000..bd3fe654a56c4
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json
@@ -0,0 +1,141 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-transform:type_1-obj_1",
+ "source": {
+ "test-export-transform": {
+ "title": "test_1-obj_1",
+ "enabled": true
+ },
+ "type": "test-export-transform",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-transform:type_1-obj_2",
+ "source": {
+ "test-export-transform": {
+ "title": "test_1-obj_2",
+ "enabled": true
+ },
+ "type": "test-export-transform",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-add:type_2-obj_1",
+ "source": {
+ "test-export-add": {
+ "title": "test_2-obj_1"
+ },
+ "type": "test-export-add",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-add:type_2-obj_2",
+ "source": {
+ "test-export-add": {
+ "title": "test_2-obj_2"
+ },
+ "type": "test-export-add",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-add-dep:type_dep-obj_1",
+ "source": {
+ "test-export-add-dep": {
+ "title": "type_dep-obj_1"
+ },
+ "type": "test-export-add-dep",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z",
+ "references": [
+ {
+ "type": "test-export-add",
+ "id": "type_2-obj_1"
+ }
+ ]
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-add-dep:type_dep-obj_2",
+ "source": {
+ "test-export-add-dep": {
+ "title": "type_dep-obj_2"
+ },
+ "type": "test-export-add-dep",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z",
+ "references": [
+ {
+ "type": "test-export-add",
+ "id": "type_2-obj_2"
+ }
+ ]
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-invalid-transform:type_3-obj_1",
+ "source": {
+ "test-export-invalid-transform": {
+ "title": "test_2-obj_1"
+ },
+ "type": "test-export-invalid-transform",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "test-export-transform-error:type_4-obj_1",
+ "source": {
+ "test-export-transform-error": {
+ "title": "test_2-obj_1"
+ },
+ "type": "test-export-transform-error",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json
new file mode 100644
index 0000000000000..d85125efd672a
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json
@@ -0,0 +1,499 @@
+{
+ "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-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": {
+ "type": "geo_shape",
+ "tree": "quadtree"
+ },
+ "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/kibana.json b/test/plugin_functional/plugins/saved_object_export_transforms/kibana.json
new file mode 100644
index 0000000000000..40b4c12f58e69
--- /dev/null
+++ b/test/plugin_functional/plugins/saved_object_export_transforms/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "savedObjectExportTransforms",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "configPath": ["saved_object_export_transforms"],
+ "server": true,
+ "ui": false
+}
diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/package.json b/test/plugin_functional/plugins/saved_object_export_transforms/package.json
new file mode 100644
index 0000000000000..0ced0a3b21288
--- /dev/null
+++ b/test/plugin_functional/plugins/saved_object_export_transforms/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "saved_object_export_transforms",
+ "version": "1.0.0",
+ "main": "target/test/plugin_functional/plugins/saved_object_export_transforms",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "Apache-2.0",
+ "scripts": {
+ "kbn": "node ../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
+ }
+}
\ No newline at end of file
diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/index.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/index.ts
new file mode 100644
index 0000000000000..f87a7d7d2e6a3
--- /dev/null
+++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedObjectExportTransformsPlugin } from './plugin';
+
+export const plugin = () => new SavedObjectExportTransformsPlugin();
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
new file mode 100644
index 0000000000000..acbf454a93093
--- /dev/null
+++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts
@@ -0,0 +1,141 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Plugin, CoreSetup } from 'kibana/server';
+
+export class SavedObjectExportTransformsPlugin implements Plugin {
+ public setup({ savedObjects, getStartServices }: CoreSetup, deps: {}) {
+ const savedObjectStartContractPromise = getStartServices().then(
+ ([{ savedObjects: savedObjectsStart }]) => savedObjectsStart
+ );
+
+ // example of a SO type that will mutates its properties
+ // during the export transform
+ savedObjects.registerType({
+ name: 'test-export-transform',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: {
+ title: { type: 'text' },
+ enabled: {
+ type: 'boolean',
+ },
+ },
+ },
+ management: {
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj) => obj.attributes.title,
+ onExport: (ctx, objs) => {
+ return objs.map((obj) => ({
+ ...obj,
+ attributes: {
+ ...obj.attributes,
+ enabled: false,
+ },
+ }));
+ },
+ },
+ });
+
+ // example of a SO type that will add additional objects
+ // to the export during the export transform
+ savedObjects.registerType({
+ name: 'test-export-add',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: {
+ title: { type: 'text' },
+ },
+ },
+ management: {
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj) => obj.attributes.title,
+ onExport: async (ctx, objs) => {
+ const { getScopedClient } = await savedObjectStartContractPromise;
+ const client = getScopedClient(ctx.request);
+ const objRefs = objs.map(({ id, type }) => ({ id, type }));
+ const depResponse = await client.find({
+ type: 'test-export-add-dep',
+ hasReference: objRefs,
+ });
+ return [...objs, ...depResponse.saved_objects];
+ },
+ },
+ });
+
+ // dependency of `test_export_transform_2` that will be included
+ // when exporting them
+ savedObjects.registerType({
+ name: 'test-export-add-dep',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: {
+ title: { type: 'text' },
+ },
+ },
+ management: {
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj) => obj.attributes.title,
+ },
+ });
+
+ /////////////
+ /////////////
+ // example of a SO type that will throw an object-transform-error
+ savedObjects.registerType({
+ name: 'test-export-transform-error',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: {
+ title: { type: 'text' },
+ },
+ },
+ management: {
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj) => obj.attributes.title,
+ onExport: (ctx, objs) => {
+ throw new Error('Error during transform');
+ },
+ },
+ });
+
+ // example of a SO type that will throw an invalid-transform-error
+ savedObjects.registerType({
+ name: 'test-export-invalid-transform',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: {
+ title: { type: 'text' },
+ },
+ },
+ management: {
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj) => obj.attributes.title,
+ onExport: (ctx, objs) => {
+ return objs.map((obj) => ({
+ ...obj,
+ id: `${obj.id}-mutated`,
+ }));
+ },
+ },
+ });
+ }
+
+ public start() {}
+ public stop() {}
+}
diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json b/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json
new file mode 100644
index 0000000000000..da457c9ba32fc
--- /dev/null
+++ b/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true
+ },
+ "include": [
+ "index.ts",
+ "server/**/*.ts",
+ "../../../../typings/**/*",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../../../src/core/tsconfig.json" }
+ ]
+}
diff --git a/test/plugin_functional/plugins/saved_object_hooks/kibana.json b/test/plugin_functional/plugins/saved_object_import_warnings/kibana.json
similarity index 50%
rename from test/plugin_functional/plugins/saved_object_hooks/kibana.json
rename to test/plugin_functional/plugins/saved_object_import_warnings/kibana.json
index 1580e1862fac1..947f840560eba 100644
--- a/test/plugin_functional/plugins/saved_object_hooks/kibana.json
+++ b/test/plugin_functional/plugins/saved_object_import_warnings/kibana.json
@@ -1,8 +1,8 @@
{
- "id": "savedObjectHooks",
+ "id": "savedObjectImportWarnings",
"version": "0.0.1",
"kibanaVersion": "kibana",
- "configPath": ["saved_object_hooks"],
+ "configPath": ["saved_object_import_warnings"],
"server": true,
"ui": false
}
diff --git a/test/plugin_functional/plugins/saved_object_hooks/package.json b/test/plugin_functional/plugins/saved_object_import_warnings/package.json
similarity index 68%
rename from test/plugin_functional/plugins/saved_object_hooks/package.json
rename to test/plugin_functional/plugins/saved_object_import_warnings/package.json
index 9e09e5fc94be4..0c3cb50bd0b18 100644
--- a/test/plugin_functional/plugins/saved_object_hooks/package.json
+++ b/test/plugin_functional/plugins/saved_object_import_warnings/package.json
@@ -1,7 +1,7 @@
{
- "name": "saved_object_hooks",
+ "name": "saved_object_import_warnings",
"version": "1.0.0",
- "main": "target/test/plugin_functional/plugins/saved_object_hooks",
+ "main": "target/test/plugin_functional/plugins/saved_object_import_warnings",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
diff --git a/test/plugin_functional/plugins/saved_object_hooks/server/index.ts b/test/plugin_functional/plugins/saved_object_import_warnings/server/index.ts
similarity index 73%
rename from test/plugin_functional/plugins/saved_object_hooks/server/index.ts
rename to test/plugin_functional/plugins/saved_object_import_warnings/server/index.ts
index 28aaa75961ddc..9a7209480cc19 100644
--- a/test/plugin_functional/plugins/saved_object_hooks/server/index.ts
+++ b/test/plugin_functional/plugins/saved_object_import_warnings/server/index.ts
@@ -6,6 +6,6 @@
* Public License, v 1.
*/
-import { SavedObjectHooksPlugin } from './plugin';
+import { SavedObjectImportWarningsPlugin } from './plugin';
-export const plugin = () => new SavedObjectHooksPlugin();
+export const plugin = () => new SavedObjectImportWarningsPlugin();
diff --git a/test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts b/test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts
similarity index 96%
rename from test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts
rename to test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts
index 823d9a90f29e2..5fc4e4aed9b90 100644
--- a/test/plugin_functional/plugins/saved_object_hooks/server/plugin.ts
+++ b/test/plugin_functional/plugins/saved_object_import_warnings/server/plugin.ts
@@ -8,7 +8,7 @@
import { Plugin, CoreSetup } from 'kibana/server';
-export class SavedObjectHooksPlugin implements Plugin {
+export class SavedObjectImportWarningsPlugin implements Plugin {
public setup({ savedObjects }: CoreSetup, deps: {}) {
savedObjects.registerType({
name: 'test_import_warning_1',
diff --git a/test/plugin_functional/plugins/saved_object_hooks/tsconfig.json b/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json
similarity index 100%
rename from test/plugin_functional/plugins/saved_object_hooks/tsconfig.json
rename to test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json
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
new file mode 100644
index 0000000000000..33c4ddc38be07
--- /dev/null
+++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts
@@ -0,0 +1,140 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import type { SavedObject } from '../../../../src/core/types';
+import { PluginFunctionalProviderContext } from '../../services';
+
+function parseNdJson(input: string): Array> {
+ return input.split('\n').map((str) => JSON.parse(str));
+}
+
+export default function ({ getService }: PluginFunctionalProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ describe('export transforms', () => {
+ before(async () => {
+ await esArchiver.load(
+ '../functional/fixtures/es_archiver/saved_objects_management/export_transform'
+ );
+ });
+
+ after(async () => {
+ await esArchiver.unload(
+ '../functional/fixtures/es_archiver/saved_objects_management/export_transform'
+ );
+ });
+
+ it('allows to mutate the objects during an export', async () => {
+ await supertest
+ .post('/api/saved_objects/_export')
+ .set('kbn-xsrf', 'true')
+ .send({
+ type: ['test-export-transform'],
+ excludeExportDetails: true,
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = parseNdJson(resp.text);
+ expect(objects.map((obj) => ({ id: obj.id, enabled: obj.attributes.enabled }))).to.eql([
+ {
+ id: 'type_1-obj_1',
+ enabled: false,
+ },
+ {
+ id: 'type_1-obj_2',
+ enabled: false,
+ },
+ ]);
+ });
+ });
+
+ it('allows to add additional objects to an export', async () => {
+ await supertest
+ .post('/api/saved_objects/_export')
+ .set('kbn-xsrf', 'true')
+ .send({
+ objects: [
+ {
+ type: 'test-export-add',
+ id: 'type_2-obj_1',
+ },
+ ],
+ excludeExportDetails: true,
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = parseNdJson(resp.text);
+ expect(objects.map((obj) => obj.id)).to.eql(['type_2-obj_1', 'type_dep-obj_1']);
+ });
+ });
+
+ it('allows to add additional objects to an export when exporting by type', async () => {
+ await supertest
+ .post('/api/saved_objects/_export')
+ .set('kbn-xsrf', 'true')
+ .send({
+ type: ['test-export-add'],
+ excludeExportDetails: true,
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = parseNdJson(resp.text);
+ expect(objects.map((obj) => obj.id)).to.eql([
+ 'type_2-obj_1',
+ 'type_2-obj_2',
+ 'type_dep-obj_1',
+ 'type_dep-obj_2',
+ ]);
+ });
+ });
+
+ it('returns a 400 when the type causes a transform error', async () => {
+ await supertest
+ .post('/api/saved_objects/_export')
+ .set('kbn-xsrf', 'true')
+ .send({
+ type: ['test-export-transform-error'],
+ excludeExportDetails: true,
+ })
+ .expect(400)
+ .then((resp) => {
+ const { attributes, ...error } = resp.body;
+ expect(error).to.eql({
+ error: 'Bad Request',
+ message: 'Error transforming objects to export',
+ statusCode: 400,
+ });
+ expect(attributes.cause).to.eql('Error during transform');
+ expect(attributes.objects.map((obj: any) => obj.id)).to.eql(['type_4-obj_1']);
+ });
+ });
+
+ it('returns a 400 when the type causes an invalid transform', async () => {
+ await supertest
+ .post('/api/saved_objects/_export')
+ .set('kbn-xsrf', 'true')
+ .send({
+ type: ['test-export-invalid-transform'],
+ excludeExportDetails: true,
+ })
+ .expect(400)
+ .then((resp) => {
+ expect(resp.body).to.eql({
+ error: 'Bad Request',
+ message: 'Invalid transform performed on objects to export',
+ statusCode: 400,
+ attributes: {
+ objectKeys: ['test-export-invalid-transform|type_3-obj_1'],
+ },
+ });
+ });
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/saved_objects_management/index.ts b/test/plugin_functional/test_suites/saved_objects_management/index.ts
index ad89a6605bbc5..38c966901d8a2 100644
--- a/test/plugin_functional/test_suites/saved_objects_management/index.ts
+++ b/test/plugin_functional/test_suites/saved_objects_management/index.ts
@@ -10,6 +10,7 @@ import { PluginFunctionalProviderContext } from '../../services';
export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
describe('Saved Objects Management', function () {
+ loadTestFile(require.resolve('./export_transform'));
loadTestFile(require.resolve('./import_warnings'));
});
}
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
index 8450fdf6b4641..e06ba3499d9a4 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
@@ -167,6 +167,7 @@ describe('copySavedObjectsToSpaces', () => {
`);
expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({
+ request: expect.any(Object),
excludeExportDetails: true,
includeReferencesDeep: true,
namespace,
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
index 852f680b0245a..39f31c5f85178 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
@@ -29,6 +29,7 @@ export function copySavedObjectsToSpacesFactory(
options: Pick
) => {
const objectStream = await savedObjectsExporter.exportByObjects({
+ request,
namespace: spaceIdToNamespace(sourceSpaceId),
includeReferencesDeep: options.includeReferences,
excludeExportDetails: true,
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
index 0f5de232177fd..6c24394b540df 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
@@ -174,6 +174,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
`);
expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({
+ request: expect.any(Object),
excludeExportDetails: true,
includeReferencesDeep: true,
namespace,
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
index 2a671b1423e8c..6033e369d3ea4 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
@@ -29,6 +29,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory(
options: Pick
) => {
const objectStream = await savedObjectsExporter.exportByObjects({
+ request,
namespace: spaceIdToNamespace(sourceSpaceId),
includeReferencesDeep: options.includeReferences,
excludeExportDetails: true,