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