[7.x] Support pit and search_after in server savedObjects.find (#89915) (#91217)

This commit is contained in:
Luke Elmers 2021-02-11 16:00:01 -07:00 committed by GitHub
parent 4a732b31f1
commit ee4b72ee29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 2723 additions and 267 deletions

View file

@ -23,9 +23,11 @@ export interface SavedObjectsFindOptions
| [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | <code>string[]</code> | |
| [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | <code>number</code> | |
| [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | <code>number</code> | |
| [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md) | <code>SavedObjectsPitParams</code> | Search against a specific Point In Time (PIT) that you've opened with . |
| [preference](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | <code>string</code> | An optional ES preference value to be used for the query \* |
| [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | <code>string[]</code> | The fields to perform the parsed query against. Unlike the <code>searchFields</code> argument, these are expected to be root fields and will not be modified. If used in conjunction with <code>searchFields</code>, both are concatenated together. |
| [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | <code>string</code> | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String <code>query</code> argument for more information |
| [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | <code>unknown[]</code> | Use the sort values from the previous page to retrieve the next page of results. |
| [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | <code>string[]</code> | The fields to perform the parsed query against. See Elasticsearch Simple Query String <code>fields</code> argument for more information |
| [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | <code>string</code> | |
| [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | <code>string</code> | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) &gt; [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md)
## SavedObjectsFindOptions.pit property
Search against a specific Point In Time (PIT) that you've opened with .
<b>Signature:</b>
```typescript
pit?: SavedObjectsPitParams;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) &gt; [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md)
## SavedObjectsFindOptions.searchAfter property
Use the sort values from the previous page to retrieve the next page of results.
<b>Signature:</b>
```typescript
searchAfter?: unknown[];
```

View file

@ -155,6 +155,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsCheckConflictsResponse](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.md) | |
| [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. |
| [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. |
| [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) | |
| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. |
| [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. |
| [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | |
@ -188,6 +189,9 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsMappingProperties](./kibana-plugin-core-server.savedobjectsmappingproperties.md) | Describe the fields of a [saved object type](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md)<!-- -->. |
| [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) | |
| [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) | |
| [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md) | |
| [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) | |
| [SavedObjectsRawDoc](./kibana-plugin-core-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. |
| [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) | Options that can be specified when using the saved objects serializer to parse a raw document. |
| [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md) | |
@ -301,6 +305,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. |
| [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md)<!-- -->. |
| [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. |
| [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md) | |
| [SavedObjectsExportTransform](./kibana-plugin-core-server.savedobjectsexporttransform.md) | Transformation function used to mutate the exported objects of the associated type.<!-- -->A type's export transform function will be executed once per user-initiated export, for all objects of that type. |
| [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.<!-- -->Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation |
| [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) | A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.<!-- -->Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. |

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [closePointInTime](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md)
## SavedObjectsClient.closePointInTime() method
Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md)<!-- -->.
<b>Signature:</b>
```typescript
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise<SavedObjectsClosePointInTimeResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| id | <code>string</code> | |
| options | <code>SavedObjectsClosePointInTimeOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsClosePointInTimeResponse>`

View file

@ -30,11 +30,13 @@ The constructor for this class is marked as internal. Third-party code should no
| [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id |
| [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once |
| [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. |
| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md)<!-- -->. |
| [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject |
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject |
| [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object |
| [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned <code>id</code> can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT. |
| [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference. |
| [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists |
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject |

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [openPointInTimeForType](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md)
## SavedObjectsClient.openPointInTimeForType() method
Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned `id` can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT.
<b>Signature:</b>
```typescript
openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise<SavedObjectsOpenPointInTimeResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string &#124; string[]</code> | |
| options | <code>SavedObjectsOpenPointInTimeOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsOpenPointInTimeResponse>`

View file

@ -0,0 +1,12 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md)
## SavedObjectsClosePointInTimeOptions type
<b>Signature:</b>
```typescript
export declare type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md)
## SavedObjectsClosePointInTimeResponse interface
<b>Signature:</b>
```typescript
export interface SavedObjectsClosePointInTimeResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md) | <code>number</code> | The number of search contexts that have been successfully closed. |
| [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md) | <code>boolean</code> | If true, all search contexts associated with the PIT id are successfully closed. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) &gt; [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md)
## SavedObjectsClosePointInTimeResponse.num\_freed property
The number of search contexts that have been successfully closed.
<b>Signature:</b>
```typescript
num_freed: number;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) &gt; [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md)
## SavedObjectsClosePointInTimeResponse.succeeded property
If true, all search contexts associated with the PIT id are successfully closed.
<b>Signature:</b>
```typescript
succeeded: boolean;
```

View file

@ -9,10 +9,11 @@ Constructs a new instance of the `SavedObjectsExporter` class
<b>Signature:</b>
```typescript
constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: {
constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, }: {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
logger: Logger;
});
```
@ -20,5 +21,5 @@ constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: {
| Parameter | Type | Description |
| --- | --- | --- |
| { savedObjectsClient, typeRegistry, exportSizeLimit, } | <code>{</code><br/><code> savedObjectsClient: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</code><br/><code> exportSizeLimit: number;</code><br/><code> }</code> | |
| { savedObjectsClient, typeRegistry, exportSizeLimit, logger, } | <code>{</code><br/><code> savedObjectsClient: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</code><br/><code> exportSizeLimit: number;</code><br/><code> logger: Logger;</code><br/><code> }</code> | |

View file

@ -15,7 +15,7 @@ export declare class SavedObjectsExporter
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the <code>SavedObjectsExporter</code> class |
| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the <code>SavedObjectsExporter</code> class |
## Properties

View file

@ -23,9 +23,11 @@ export interface SavedObjectsFindOptions
| [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | <code>string[]</code> | |
| [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | <code>number</code> | |
| [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | <code>number</code> | |
| [pit](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md) | <code>SavedObjectsPitParams</code> | Search against a specific Point In Time (PIT) that you've opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md)<!-- -->. |
| [preference](./kibana-plugin-core-server.savedobjectsfindoptions.preference.md) | <code>string</code> | An optional ES preference value to be used for the query \* |
| [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | <code>string[]</code> | The fields to perform the parsed query against. Unlike the <code>searchFields</code> argument, these are expected to be root fields and will not be modified. If used in conjunction with <code>searchFields</code>, both are concatenated together. |
| [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | <code>string</code> | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String <code>query</code> argument for more information |
| [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | <code>unknown[]</code> | Use the sort values from the previous page to retrieve the next page of results. |
| [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | <code>string[]</code> | The fields to perform the parsed query against. See Elasticsearch Simple Query String <code>fields</code> argument for more information |
| [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | <code>string</code> | |
| [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | <code>string</code> | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) &gt; [pit](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md)
## SavedObjectsFindOptions.pit property
Search against a specific Point In Time (PIT) that you've opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md)<!-- -->.
<b>Signature:</b>
```typescript
pit?: SavedObjectsPitParams;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) &gt; [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md)
## SavedObjectsFindOptions.searchAfter property
Use the sort values from the previous page to retrieve the next page of results.
<b>Signature:</b>
```typescript
searchAfter?: unknown[];
```

View file

@ -20,6 +20,7 @@ export interface SavedObjectsFindResponse<T = unknown>
| --- | --- | --- |
| [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | <code>number</code> | |
| [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | <code>number</code> | |
| [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md) | <code>string</code> | |
| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | <code>Array&lt;SavedObjectsFindResult&lt;T&gt;&gt;</code> | |
| [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | <code>number</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) &gt; [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md)
## SavedObjectsFindResponse.pit\_id property
<b>Signature:</b>
```typescript
pit_id?: string;
```

View file

@ -16,4 +16,5 @@ export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T>
| Property | Type | Description |
| --- | --- | --- |
| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | <code>number</code> | The Elasticsearch <code>_score</code> of this result. |
| [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | <code>unknown[]</code> | The Elasticsearch <code>sort</code> value of this result. |

View file

@ -0,0 +1,41 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) &gt; [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md)
## SavedObjectsFindResult.sort property
The Elasticsearch `sort` value of this result.
<b>Signature:</b>
```typescript
sort?: unknown[];
```
## Remarks
This can be passed directly to the `searchAfter` param in the [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) in order to page through large numbers of hits. It is recommended you use this alongside a Point In Time (PIT) that was opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md)<!-- -->.
## Example
```ts
const { id } = await savedObjectsClient.openPointInTimeForType('visualization');
const page1 = await savedObjectsClient.find({
type: 'visualization',
sortField: 'updated_at',
sortOrder: 'asc',
pit,
});
const lastHit = page1.saved_objects[page1.saved_objects.length - 1];
const page2 = await savedObjectsClient.find({
type: 'visualization',
sortField: 'updated_at',
sortOrder: 'asc',
pit: { id: page1.pit_id },
searchAfter: lastHit.sort,
});
await savedObjectsClient.closePointInTime(page2.pit_id);
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) &gt; [keepAlive](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md)
## SavedObjectsOpenPointInTimeOptions.keepAlive property
Optionally specify how long ES should keep the PIT alive until the next request. Defaults to `5m`<!-- -->.
<b>Signature:</b>
```typescript
keepAlive?: string;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md)
## SavedObjectsOpenPointInTimeOptions interface
<b>Signature:</b>
```typescript
export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [keepAlive](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md) | <code>string</code> | Optionally specify how long ES should keep the PIT alive until the next request. Defaults to <code>5m</code>. |
| [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md) | <code>string</code> | An optional ES preference value to be used for the query. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) &gt; [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md)
## SavedObjectsOpenPointInTimeOptions.preference property
An optional ES preference value to be used for the query.
<b>Signature:</b>
```typescript
preference?: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md) &gt; [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md)
## SavedObjectsOpenPointInTimeResponse.id property
PIT ID returned from ES.
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md)
## SavedObjectsOpenPointInTimeResponse interface
<b>Signature:</b>
```typescript
export interface SavedObjectsOpenPointInTimeResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md) | <code>string</code> | PIT ID returned from ES. |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) &gt; [id](./kibana-plugin-core-server.savedobjectspitparams.id.md)
## SavedObjectsPitParams.id property
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) &gt; [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md)
## SavedObjectsPitParams.keepAlive property
<b>Signature:</b>
```typescript
keepAlive?: string;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md)
## SavedObjectsPitParams interface
<b>Signature:</b>
```typescript
export interface SavedObjectsPitParams
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-core-server.savedobjectspitparams.id.md) | <code>string</code> | |
| [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md) | <code>string</code> | |

View file

@ -0,0 +1,58 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [closePointInTime](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md)
## SavedObjectsRepository.closePointInTime() method
Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using `openPointInTimeForType`<!-- -->.
<b>Signature:</b>
```typescript
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise<SavedObjectsClosePointInTimeResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| id | <code>string</code> | |
| options | <code>SavedObjectsClosePointInTimeOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsClosePointInTimeResponse>`
{<!-- -->promise<!-- -->} - [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md)
## Remarks
While the `keepAlive` that is provided will cause a PIT to automatically close, it is highly recommended to explicitly close a PIT when you are done with it in order to avoid consuming unneeded resources in Elasticsearch.
## Example
```ts
const repository = coreStart.savedObjects.createInternalRepository();
const { id } = await repository.openPointInTimeForType(
type: 'index-pattern',
{ keepAlive: '2m' },
);
const response = await repository.find({
type: 'index-pattern',
search: 'foo*',
sortField: 'name',
sortOrder: 'desc',
pit: {
id: 'abc123',
keepAlive: '2m',
},
searchAfter: [1234, 'abcd'],
});
await repository.closePointInTime(response.pit_id);
```

View file

@ -20,6 +20,7 @@ export declare class SavedObjectsRepository
| [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id |
| [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk |
| [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. |
| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using <code>openPointInTimeForType</code>. |
| [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object |
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object |
| [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. |
@ -27,6 +28,7 @@ export declare class SavedObjectsRepository
| [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object |
| [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. |
| [openPointInTimeForType(type, { keepAlive, preference })](./kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned <code>id</code> can then be passed to <code>SavedObjects.find</code> to search against that PIT. |
| [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference. |
| [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists |
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object |

View file

@ -0,0 +1,57 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [openPointInTimeForType](./kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md)
## SavedObjectsRepository.openPointInTimeForType() method
Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned `id` can then be passed to `SavedObjects.find` to search against that PIT.
<b>Signature:</b>
```typescript
openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise<SavedObjectsOpenPointInTimeResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string &#124; string[]</code> | |
| { keepAlive, preference } | <code>SavedObjectsOpenPointInTimeOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsOpenPointInTimeResponse>`
{<!-- -->promise<!-- -->} - { id: string }
## Example
```ts
const repository = coreStart.savedObjects.createInternalRepository();
const { id } = await repository.openPointInTimeForType(
type: 'index-pattern',
{ keepAlive: '2m' },
);
const page1 = await savedObjectsClient.find({
type: 'visualization',
sortField: 'updated_at',
sortOrder: 'asc',
pit,
});
const lastHit = page1.saved_objects[page1.saved_objects.length - 1];
const page2 = await savedObjectsClient.find({
type: 'visualization',
sortField: 'updated_at',
sortOrder: 'asc',
pit: { id: page1.pit_id },
searchAfter: lastHit.sort,
});
await savedObjectsClient.closePointInTime(page2.pit_id);
```

View file

@ -22,7 +22,7 @@ hits: {
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: string[];
sort?: unknown[];
}>;
};
```

View file

@ -18,7 +18,8 @@ export interface SearchResponse<T = unknown>
| [\_scroll\_id](./kibana-plugin-core-server.searchresponse._scroll_id.md) | <code>string</code> | |
| [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) | <code>ShardsResponse</code> | |
| [aggregations](./kibana-plugin-core-server.searchresponse.aggregations.md) | <code>any</code> | |
| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | <code>{</code><br/><code> total: number;</code><br/><code> max_score: number;</code><br/><code> hits: Array&lt;{</code><br/><code> _index: string;</code><br/><code> _type: string;</code><br/><code> _id: string;</code><br/><code> _score: number;</code><br/><code> _source: T;</code><br/><code> _version?: number;</code><br/><code> _explanation?: Explanation;</code><br/><code> fields?: any;</code><br/><code> highlight?: any;</code><br/><code> inner_hits?: any;</code><br/><code> matched_queries?: string[];</code><br/><code> sort?: string[];</code><br/><code> }&gt;;</code><br/><code> }</code> | |
| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | <code>{</code><br/><code> total: number;</code><br/><code> max_score: number;</code><br/><code> hits: Array&lt;{</code><br/><code> _index: string;</code><br/><code> _type: string;</code><br/><code> _id: string;</code><br/><code> _score: number;</code><br/><code> _source: T;</code><br/><code> _version?: number;</code><br/><code> _explanation?: Explanation;</code><br/><code> fields?: any;</code><br/><code> highlight?: any;</code><br/><code> inner_hits?: any;</code><br/><code> matched_queries?: string[];</code><br/><code> sort?: unknown[];</code><br/><code> }&gt;;</code><br/><code> }</code> | |
| [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md) | <code>string</code> | |
| [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) | <code>boolean</code> | |
| [took](./kibana-plugin-core-server.searchresponse.took.md) | <code>number</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SearchResponse](./kibana-plugin-core-server.searchresponse.md) &gt; [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md)
## SearchResponse.pit\_id property
<b>Signature:</b>
```typescript
pit_id?: string;
```

View file

@ -85,6 +85,10 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is creating a saved object.
| `failure` | User is not authorized to create a saved object.
.2+| `saved_object_open_point_in_time`
| `unknown` | User is creating a Point In Time to use when querying saved objects.
| `failure` | User is not authorized to create a Point In Time for the provided saved object types.
.2+| `connector_create`
| `unknown` | User is creating a connector.
| `failure` | User is not authorized to create a connector.
@ -171,6 +175,10 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is deleting a saved object.
| `failure` | User is not authorized to delete a saved object.
.2+| `saved_object_close_point_in_time`
| `unknown` | User is deleting a Point In Time that was used to query saved objects.
| `failure` | User is not authorized to delete a Point In Time.
.2+| `connector_delete`
| `unknown` | User is deleting a connector.
| `failure` | User is not authorized to delete a connector.

View file

@ -1204,9 +1204,13 @@ export interface SavedObjectsFindOptions {
page?: number;
// (undocumented)
perPage?: number;
// Warning: (ae-forgotten-export) The symbol "SavedObjectsPitParams" needs to be exported by the entry point index.d.ts
// Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "openPointInTimeForType"
pit?: SavedObjectsPitParams;
preference?: string;
rootSearchFields?: string[];
search?: string;
searchAfter?: unknown[];
searchFields?: string[];
// (undocumented)
sortField?: string;

View file

@ -21,12 +21,14 @@ import {
import { SimpleSavedObject } from './simple_saved_object';
import { HttpFetchOptions, HttpSetup } from '../http';
type PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
type SavedObjectsFindOptions = Omit<
SavedObjectFindOptionsServer,
'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap'
'pit' | 'rootSearchFields' | 'searchAfter' | 'sortOrder' | 'typeToNamespacesMap'
>;
type PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
type SavedObjectsFindResponse = Omit<PromiseType<ReturnType<SavedObjectsApi['find']>>, 'pit_id'>;
/** @public */
export interface SavedObjectsCreateOptions {
@ -345,10 +347,7 @@ export class SavedObjectsClient {
query,
});
return request.then((resp) => {
return renameKeys<
PromiseType<ReturnType<SavedObjectsApi['find']>>,
SavedObjectsFindResponsePublic
>(
return renameKeys<SavedObjectsFindResponse, SavedObjectsFindResponsePublic>(
{
saved_objects: 'savedObjects',
total: 'total',

View file

@ -96,10 +96,11 @@ export interface SearchResponse<T = unknown> {
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: string[];
sort?: unknown[];
}>;
};
aggregations?: any;
pit_id?: string;
}
/**

View file

@ -260,6 +260,8 @@ export {
SavedObjectsClientWrapperOptions,
SavedObjectsClientFactory,
SavedObjectsClientFactoryProvider,
SavedObjectsClosePointInTimeOptions,
SavedObjectsClosePointInTimeResponse,
SavedObjectsCreateOptions,
SavedObjectsErrorHelpers,
SavedObjectsExportResultDetails,
@ -277,6 +279,8 @@ export {
SavedObjectsImportUnsupportedTypeError,
SavedObjectMigrationContext,
SavedObjectsMigrationLogger,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsOpenPointInTimeResponse,
SavedObjectsRawDoc,
SavedObjectsRawDocParseOptions,
SavedObjectSanitizedDoc,
@ -373,6 +377,7 @@ export {
SavedObjectsClientContract,
SavedObjectsFindOptions,
SavedObjectsFindOptionsReference,
SavedObjectsPitParams,
SavedObjectsMigrationVersion,
} from './types';

View file

@ -0,0 +1,321 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { savedObjectsClientMock } from '../service/saved_objects_client.mock';
import { loggerMock, MockedLogger } from '../../logging/logger.mock';
import { SavedObjectsFindOptions } from '../types';
import { SavedObjectsFindResult } from '../service';
import { createPointInTimeFinder } from './point_in_time_finder';
const mockHits = [
{
id: '2',
type: 'search',
attributes: {},
score: 1,
references: [
{
name: 'name',
type: 'visualization',
id: '1',
},
],
sort: [],
},
{
id: '1',
type: 'visualization',
attributes: {},
score: 1,
references: [],
sort: [],
},
];
describe('createPointInTimeFinder()', () => {
let logger: MockedLogger;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
beforeEach(() => {
logger = loggerMock.create();
savedObjectsClient = savedObjectsClientMock.create();
});
describe('#find', () => {
test('throws if a PIT is already open', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'abc123',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: mockHits,
pit_id: 'abc123',
per_page: 1,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: mockHits,
pit_id: 'abc123',
per_page: 1,
page: 1,
});
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
perPage: 1,
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
await finder.find().next();
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
savedObjectsClient.find.mockClear();
expect(async () => {
await finder.find().next();
}).rejects.toThrowErrorMatchingInlineSnapshot(
`"Point In Time has already been opened for this finder instance. Please call \`close()\` before calling \`find()\` again."`
);
expect(savedObjectsClient.find).toHaveBeenCalledTimes(0);
});
test('works with a single page of results', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'abc123',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: mockHits,
pit_id: 'abc123',
per_page: 2,
page: 0,
});
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
expect(hits.length).toBe(2);
expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.find).toHaveBeenCalledWith(
expect.objectContaining({
pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }),
sortField: 'updated_at',
sortOrder: 'desc',
type: ['visualization'],
})
);
});
test('works with multiple pages of results', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'abc123',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [mockHits[0]],
pit_id: 'abc123',
per_page: 1,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [mockHits[1]],
pit_id: 'abc123',
per_page: 1,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [],
per_page: 1,
pit_id: 'abc123',
page: 0,
});
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
perPage: 1,
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
expect(hits.length).toBe(2);
expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1);
// called 3 times since we need a 3rd request to check if we
// are done paginating through results.
expect(savedObjectsClient.find).toHaveBeenCalledTimes(3);
expect(savedObjectsClient.find).toHaveBeenCalledWith(
expect.objectContaining({
pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }),
sortField: 'updated_at',
sortOrder: 'desc',
type: ['visualization'],
})
);
});
});
describe('#close', () => {
test('calls closePointInTime with correct ID', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'test',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 1,
saved_objects: [mockHits[0]],
pit_id: 'test',
per_page: 1,
page: 0,
});
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
perPage: 2,
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
await finder.close();
}
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test');
});
test('causes generator to stop', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'test',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [mockHits[0]],
pit_id: 'test',
per_page: 1,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [mockHits[1]],
pit_id: 'test',
per_page: 1,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [],
per_page: 1,
pit_id: 'test',
page: 0,
});
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
perPage: 1,
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
await finder.close();
}
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1);
expect(hits.length).toBe(1);
});
test('is called if `find` throws an error', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'test',
});
savedObjectsClient.find.mockRejectedValueOnce(new Error('oops'));
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
perPage: 2,
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
const hits: SavedObjectsFindResult[] = [];
try {
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
} catch (e) {
// intentionally empty
}
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test');
});
test('finder can be reused after closing', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'abc123',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: mockHits,
pit_id: 'abc123',
per_page: 1,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: mockHits,
pit_id: 'abc123',
per_page: 1,
page: 1,
});
const findOptions: SavedObjectsFindOptions = {
type: ['visualization'],
search: 'foo*',
perPage: 1,
};
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
const findA = finder.find();
await findA.next();
await finder.close();
const findB = finder.find();
await findB.next();
await finder.close();
expect((await findA.next()).done).toBe(true);
expect((await findB.next()).done).toBe(true);
expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(2);
expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -0,0 +1,192 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Logger } from '../../logging';
import { SavedObjectsClientContract, SavedObjectsFindOptions } from '../types';
import { SavedObjectsFindResponse } from '../service';
/**
* Returns a generator to help page through large sets of saved objects.
*
* The generator wraps calls to `SavedObjects.find` and iterates over
* multiple pages of results using `_pit` and `search_after`. This will
* open a new Point In Time (PIT), and continue paging until a set of
* results is received that's smaller than the designated `perPage`.
*
* Once you have retrieved all of the results you need, it is recommended
* to call `close()` to clean up the PIT and prevent Elasticsearch from
* consuming resources unnecessarily. This will automatically be done for
* you if you reach the last page of results.
*
* @example
* ```ts
* const findOptions: SavedObjectsFindOptions = {
* type: 'visualization',
* search: 'foo*',
* perPage: 100,
* };
*
* const finder = createPointInTimeFinder({
* logger,
* savedObjectsClient,
* findOptions,
* });
*
* const responses: SavedObjectFindResponse[] = [];
* for await (const response of finder.find()) {
* responses.push(...response);
* if (doneSearching) {
* await finder.close();
* }
* }
* ```
*/
export function createPointInTimeFinder({
findOptions,
logger,
savedObjectsClient,
}: {
findOptions: SavedObjectsFindOptions;
logger: Logger;
savedObjectsClient: SavedObjectsClientContract;
}) {
return new PointInTimeFinder({ findOptions, logger, savedObjectsClient });
}
/**
* @internal
*/
export class PointInTimeFinder {
readonly #log: Logger;
readonly #savedObjectsClient: SavedObjectsClientContract;
readonly #findOptions: SavedObjectsFindOptions;
#open: boolean = false;
#pitId?: string;
constructor({
findOptions,
logger,
savedObjectsClient,
}: {
findOptions: SavedObjectsFindOptions;
logger: Logger;
savedObjectsClient: SavedObjectsClientContract;
}) {
this.#log = logger;
this.#savedObjectsClient = savedObjectsClient;
this.#findOptions = {
// Default to 1000 items per page as a tradeoff between
// speed and memory consumption.
perPage: 1000,
...findOptions,
};
}
async *find() {
if (this.#open) {
throw new Error(
'Point In Time has already been opened for this finder instance. ' +
'Please call `close()` before calling `find()` again.'
);
}
// Open PIT and request our first page of hits
await this.open();
let lastResultsCount: number;
let lastHitSortValue: unknown[] | undefined;
do {
const results = await this.findNext({
findOptions: this.#findOptions,
id: this.#pitId,
...(lastHitSortValue ? { searchAfter: lastHitSortValue } : {}),
});
this.#pitId = results.pit_id;
lastResultsCount = results.saved_objects.length;
lastHitSortValue = this.getLastHitSortValue(results);
this.#log.debug(`Collected [${lastResultsCount}] saved objects for export.`);
// Close PIT if this was our last page
if (this.#pitId && lastResultsCount < this.#findOptions.perPage!) {
await this.close();
}
yield results;
// We've reached the end when there are fewer hits than our perPage size,
// or when `close()` has been called.
} while (this.#open && lastResultsCount >= this.#findOptions.perPage!);
return;
}
async close() {
try {
if (this.#pitId) {
this.#log.debug(`Closing PIT for types [${this.#findOptions.type}]`);
await this.#savedObjectsClient.closePointInTime(this.#pitId);
this.#pitId = undefined;
}
this.#open = false;
} catch (e) {
this.#log.error(`Failed to close PIT for types [${this.#findOptions.type}]`);
throw e;
}
}
private async open() {
try {
const { id } = await this.#savedObjectsClient.openPointInTimeForType(this.#findOptions.type);
this.#pitId = id;
this.#open = true;
} catch (e) {
// Since `find` swallows 404s, it is expected that exporter will do the same,
// so we only rethrow non-404 errors here.
if (e.output.statusCode !== 404) {
throw e;
}
this.#log.debug(`Unable to open PIT for types [${this.#findOptions.type}]: 404 ${e}`);
}
}
private async findNext({
findOptions,
id,
searchAfter,
}: {
findOptions: SavedObjectsFindOptions;
id?: string;
searchAfter?: unknown[];
}) {
try {
return await this.#savedObjectsClient.find({
// Sort fields are required to use searchAfter, so we set some defaults here
sortField: 'updated_at',
sortOrder: 'desc',
// Bump keep_alive by 2m on every new request to allow for the ES client
// to make multiple retries in the event of a network failure.
...(id ? { pit: { id, keepAlive: '2m' } } : {}),
...(searchAfter ? { searchAfter } : {}),
...findOptions,
});
} catch (e) {
if (id) {
// Clean up PIT on any errors.
await this.close();
}
throw e;
}
}
private getLastHitSortValue(res: SavedObjectsFindResponse): unknown[] | undefined {
if (res.saved_objects.length < 1) {
return undefined;
}
return res.saved_objects[res.saved_objects.length - 1].sort;
}
}

View file

@ -11,6 +11,7 @@ import { SavedObjectsExporter } from './saved_objects_exporter';
import { savedObjectsClientMock } from '../service/saved_objects_client.mock';
import { SavedObjectTypeRegistry } from '../saved_objects_type_registry';
import { httpServerMock } from '../../http/http_server.mocks';
import { loggerMock, MockedLogger } from '../../logging/logger.mock';
import { Readable } from 'stream';
import { createPromiseFromStreams, createConcatStream } from '@kbn/utils';
@ -18,18 +19,25 @@ async function readStreamToCompletion(stream: Readable): Promise<Array<SavedObje
return createPromiseFromStreams([stream, createConcatStream([])]);
}
const exportSizeLimit = 500;
const exportSizeLimit = 10000;
const request = httpServerMock.createKibanaRequest();
describe('getSortedObjectsForExport()', () => {
let logger: MockedLogger;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
let typeRegistry: SavedObjectTypeRegistry;
let exporter: SavedObjectsExporter;
beforeEach(() => {
logger = loggerMock.create();
typeRegistry = new SavedObjectTypeRegistry();
savedObjectsClient = savedObjectsClientMock.create();
exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit, typeRegistry });
exporter = new SavedObjectsExporter({
exportSizeLimit,
logger,
savedObjectsClient,
typeRegistry,
});
});
describe('#exportByTypes', () => {
@ -58,7 +66,7 @@ describe('getSortedObjectsForExport()', () => {
references: [],
},
],
per_page: 1,
per_page: 1000,
page: 0,
});
const exportStream = await exporter.exportByTypes({
@ -96,30 +104,232 @@ describe('getSortedObjectsForExport()', () => {
]
`);
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": undefined,
"perPage": 500,
"search": undefined,
"type": Array [
"index-pattern",
"search",
],
},
],
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": undefined,
"perPage": 1000,
"pit": Object {
"id": "some_pit_id",
"keepAlive": "2m",
},
"search": undefined,
"sortField": "updated_at",
"sortOrder": "desc",
"type": Array [
"index-pattern",
"search",
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
describe('pages through results with PIT', () => {
function generateHits(
hitCount: number,
{
attributes = {},
sort = [],
type = 'index-pattern',
}: {
attributes?: Record<string, unknown>;
sort?: unknown[];
type?: string;
} = {}
) {
const hits = [];
for (let i = 1; i <= hitCount; i++) {
hits.push({
id: `${i}`,
type,
attributes,
sort,
score: 1,
references: [],
});
}
return hits;
}
describe('<1k hits', () => {
const mockHits = generateHits(20);
test('requests a single page', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 20,
saved_objects: mockHits,
per_page: 1000,
page: 0,
});
const exportStream = await exporter.exportByTypes({
request,
types: ['index-pattern'],
});
const response = await readStreamToCompletion(exportStream);
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(response[response.length - 1]).toMatchInlineSnapshot(`
Object {
"exportedCount": 20,
"missingRefCount": 0,
"missingReferences": Array [],
}
`);
});
test('opens and closes PIT', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 20,
saved_objects: mockHits,
per_page: 1000,
page: 0,
pit_id: 'abc123',
});
const exportStream = await exporter.exportByTypes({
request,
types: ['index-pattern'],
});
await readStreamToCompletion(exportStream);
expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1);
});
test('passes correct PIT ID to `find`', async () => {
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'abc123',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 20,
saved_objects: mockHits,
per_page: 1000,
page: 0,
});
const exportStream = await exporter.exportByTypes({
request,
types: ['index-pattern'],
});
await readStreamToCompletion(exportStream);
expect(savedObjectsClient.find).toHaveBeenCalledWith(
expect.objectContaining({
pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }),
sortField: 'updated_at',
sortOrder: 'desc',
type: ['index-pattern'],
})
);
});
});
describe('>1k hits', () => {
const firstMockHits = generateHits(1000, { sort: ['a', 'b'] });
const secondMockHits = generateHits(500);
test('requests multiple pages', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 1500,
saved_objects: firstMockHits,
per_page: 1000,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 1500,
saved_objects: secondMockHits,
per_page: 500,
page: 1,
});
const exportStream = await exporter.exportByTypes({
request,
types: ['index-pattern'],
});
const response = await readStreamToCompletion(exportStream);
expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
expect(response[response.length - 1]).toMatchInlineSnapshot(`
Object {
"exportedCount": 1500,
"missingRefCount": 0,
"missingReferences": Array [],
}
`);
});
test('opens and closes PIT', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 1500,
saved_objects: firstMockHits,
per_page: 1000,
page: 0,
pit_id: 'abc123',
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 1500,
saved_objects: secondMockHits,
per_page: 500,
page: 1,
pit_id: 'abc123',
});
const exportStream = await exporter.exportByTypes({
request,
types: ['index-pattern'],
});
await readStreamToCompletion(exportStream);
expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1);
});
test('passes sort values to searchAfter', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 1500,
saved_objects: firstMockHits,
per_page: 1000,
page: 0,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 1500,
saved_objects: secondMockHits,
per_page: 500,
page: 1,
});
const exportStream = await exporter.exportByTypes({
request,
types: ['index-pattern'],
});
await readStreamToCompletion(exportStream);
expect(savedObjectsClient.find.mock.calls[1][0]).toEqual(
expect.objectContaining({
searchAfter: ['a', 'b'],
})
);
});
});
});
test('applies the export transforms', async () => {
@ -138,7 +348,12 @@ describe('getSortedObjectsForExport()', () => {
},
},
});
exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit, typeRegistry });
exporter = new SavedObjectsExporter({
exportSizeLimit,
logger,
savedObjectsClient,
typeRegistry,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 1,
@ -233,30 +448,36 @@ describe('getSortedObjectsForExport()', () => {
]
`);
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": undefined,
"perPage": 500,
"search": undefined,
"type": Array [
"index-pattern",
"search",
],
},
],
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": undefined,
"perPage": 1000,
"pit": Object {
"id": "some_pit_id",
"keepAlive": "2m",
},
"search": undefined,
"sortField": "updated_at",
"sortOrder": "desc",
"type": Array [
"index-pattern",
"search",
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('exclude export details if option is specified', async () => {
@ -383,30 +604,36 @@ describe('getSortedObjectsForExport()', () => {
]
`);
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": undefined,
"perPage": 500,
"search": "foo",
"type": Array [
"index-pattern",
"search",
],
},
],
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": undefined,
"perPage": 1000,
"pit": Object {
"id": "some_pit_id",
"keepAlive": "2m",
},
"search": "foo",
"sortField": "updated_at",
"sortOrder": "desc",
"type": Array [
"index-pattern",
"search",
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('exports selected types with references when present', async () => {
@ -465,35 +692,41 @@ describe('getSortedObjectsForExport()', () => {
]
`);
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": Array [
Object {
"id": "1",
"type": "index-pattern",
},
],
"hasReferenceOperator": "OR",
"namespaces": undefined,
"perPage": 500,
"search": undefined,
"type": Array [
"index-pattern",
"search",
],
},
],
],
"results": Array [
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": Array [
Object {
"type": "return",
"value": Promise {},
"id": "1",
"type": "index-pattern",
},
],
}
`);
"hasReferenceOperator": "OR",
"namespaces": undefined,
"perPage": 1000,
"pit": Object {
"id": "some_pit_id",
"keepAlive": "2m",
},
"search": undefined,
"sortField": "updated_at",
"sortOrder": "desc",
"type": Array [
"index-pattern",
"search",
],
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('exports from the provided namespace when present', async () => {
@ -521,7 +754,7 @@ describe('getSortedObjectsForExport()', () => {
references: [],
},
],
per_page: 1,
per_page: 1000,
page: 0,
});
const exportStream = await exporter.exportByTypes({
@ -560,36 +793,56 @@ describe('getSortedObjectsForExport()', () => {
]
`);
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": Array [
"foo",
],
"perPage": 500,
"search": undefined,
"type": Array [
"index-pattern",
"search",
],
},
],
[MockFunction] {
"calls": Array [
Array [
Object {
"hasReference": undefined,
"hasReferenceOperator": undefined,
"namespaces": Array [
"foo",
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
"perPage": 1000,
"pit": Object {
"id": "some_pit_id",
"keepAlive": "2m",
},
"search": undefined,
"sortField": "updated_at",
"sortOrder": "desc",
"type": Array [
"index-pattern",
"search",
],
}
`);
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('export selected types throws error when exceeding exportSizeLimit', async () => {
exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1, typeRegistry });
exporter = new SavedObjectsExporter({
exportSizeLimit: 1,
logger,
savedObjectsClient,
typeRegistry,
});
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
id: 'abc123',
});
savedObjectsClient.closePointInTime.mockResolvedValueOnce({
succeeded: true,
num_freed: 1,
});
savedObjectsClient.find.mockResolvedValueOnce({
total: 2,
@ -617,6 +870,7 @@ describe('getSortedObjectsForExport()', () => {
],
per_page: 1,
page: 0,
pit_id: 'abc123',
});
await expect(
exporter.exportByTypes({
@ -624,12 +878,13 @@ describe('getSortedObjectsForExport()', () => {
types: ['index-pattern', 'search'],
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`);
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1);
});
test('sorts objects within type', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
total: 3,
per_page: 10000,
per_page: 1000,
page: 1,
saved_objects: [
{
@ -836,7 +1091,12 @@ describe('getSortedObjectsForExport()', () => {
});
test('export selected objects throws error when exceeding exportSizeLimit', async () => {
exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1, typeRegistry });
exporter = new SavedObjectsExporter({
exportSizeLimit: 1,
logger,
savedObjectsClient,
typeRegistry,
});
const exportOpts = {
request,

View file

@ -8,7 +8,9 @@
import { createListStream } from '@kbn/utils';
import { PublicMethodsOf } from '@kbn/utility-types';
import { SavedObject, SavedObjectsClientContract } from '../types';
import { Logger } from '../../logging';
import { SavedObject, SavedObjectsClientContract, SavedObjectsFindOptions } from '../types';
import { SavedObjectsFindResult } from '../service';
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
import { fetchNestedDependencies } from './fetch_nested_dependencies';
import { sortObjects } from './sort_objects';
@ -21,6 +23,7 @@ import {
} from './types';
import { SavedObjectsExportError } from './errors';
import { applyExportTransforms } from './apply_export_transforms';
import { createPointInTimeFinder } from './point_in_time_finder';
import { byIdAscComparator, getPreservedOrderComparator, SavedObjectComparator } from './utils';
/**
@ -35,16 +38,20 @@ export class SavedObjectsExporter {
readonly #savedObjectsClient: SavedObjectsClientContract;
readonly #exportTransforms: Record<string, SavedObjectsExportTransform>;
readonly #exportSizeLimit: number;
readonly #log: Logger;
constructor({
savedObjectsClient,
typeRegistry,
exportSizeLimit,
logger,
}: {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
logger: Logger;
}) {
this.#log = logger;
this.#savedObjectsClient = savedObjectsClient;
this.#exportSizeLimit = exportSizeLimit;
this.#exportTransforms = typeRegistry.getAllTypes().reduce((transforms, type) => {
@ -66,6 +73,7 @@ export class SavedObjectsExporter {
* @throws SavedObjectsExportError
*/
public async exportByTypes(options: SavedObjectsExportByTypeOptions) {
this.#log.debug(`Initiating export for types: [${options.types}]`);
const objects = await this.fetchByTypes(options);
return this.processObjects(objects, byIdAscComparator, {
request: options.request,
@ -83,6 +91,7 @@ export class SavedObjectsExporter {
* @throws SavedObjectsExportError
*/
public async exportByObjects(options: SavedObjectsExportByObjectOptions) {
this.#log.debug(`Initiating export of [${options.objects.length}] objects.`);
if (options.objects.length > this.#exportSizeLimit) {
throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit);
}
@ -106,6 +115,7 @@ export class SavedObjectsExporter {
namespace,
}: SavedObjectExportBaseOptions
) {
this.#log.debug(`Processing [${savedObjects.length}] saved objects.`);
let exportedObjects: Array<SavedObject<unknown>>;
let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = [];
@ -117,6 +127,7 @@ export class SavedObjectsExporter {
});
if (includeReferencesDeep) {
this.#log.debug(`Fetching saved objects references.`);
const fetchResult = await fetchNestedDependencies(
savedObjects,
this.#savedObjectsClient,
@ -138,6 +149,7 @@ export class SavedObjectsExporter {
missingRefCount: missingReferences.length,
missingReferences,
};
this.#log.debug(`Exporting [${redactedObjects.length}] saved objects.`);
return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]);
}
@ -156,21 +168,32 @@ export class SavedObjectsExporter {
hasReference,
search,
}: SavedObjectsExportByTypeOptions) {
const findResponse = await this.#savedObjectsClient.find({
const findOptions: SavedObjectsFindOptions = {
type: types,
hasReference,
hasReferenceOperator: hasReference ? 'OR' : undefined,
search,
perPage: this.#exportSizeLimit,
namespaces: namespace ? [namespace] : undefined,
};
const finder = createPointInTimeFinder({
findOptions,
logger: this.#log,
savedObjectsClient: this.#savedObjectsClient,
});
if (findResponse.total > this.#exportSizeLimit) {
throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit);
const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
if (hits.length > this.#exportSizeLimit) {
await finder.close();
throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit);
}
}
// sorts server-side by _id, since it's only available in fielddata
return (
findResponse.saved_objects
hits
// exclude the find-specific `score` property from the exported objects
.map(({ score, ...obj }) => obj)
.sort(byIdAscComparator)

View file

@ -459,6 +459,7 @@ export class SavedObjectsService
savedObjectsClient,
typeRegistry: this.typeRegistry,
exportSizeLimit: this.config!.maxImportExportSize,
logger: this.logger.get('exporter'),
}),
createImporter: (savedObjectsClient) =>
new SavedObjectsImporter({

View file

@ -17,6 +17,8 @@ const create = (): jest.Mocked<ISavedObjectsRepository> => ({
bulkGet: jest.fn(),
find: jest.fn(),
get: jest.fn(),
closePointInTime: jest.fn(),
openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }),
resolve: jest.fn(),
update: jest.fn(),
addToNamespaces: jest.fn(),

View file

@ -2812,6 +2812,13 @@ describe('SavedObjectsRepository', () => {
expect(client.search).not.toHaveBeenCalled();
});
it(`throws when a preference is provided with pit`, async () => {
await expect(
savedObjectsRepository.find({ type: 'foo', pit: { id: 'abc123' }, preference: 'hi' })
).rejects.toThrowError('options.preference must be excluded when options.pit is used');
expect(client.search).not.toHaveBeenCalled();
});
it(`throws when KQL filter syntax is invalid`, async () => {
const findOpts = {
namespaces: [namespace],
@ -2972,6 +2979,32 @@ describe('SavedObjectsRepository', () => {
});
});
it(`accepts searchAfter`, async () => {
const relevantOpts = {
...commonOptions,
searchAfter: [1, 'a'],
};
await findSuccess(relevantOpts, namespace);
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, {
...relevantOpts,
searchAfter: [1, 'a'],
});
});
it(`accepts pit`, async () => {
const relevantOpts = {
...commonOptions,
pit: { id: 'abc123', keepAlive: '2m' },
};
await findSuccess(relevantOpts, namespace);
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, {
...relevantOpts,
pit: { id: 'abc123', keepAlive: '2m' },
});
});
it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => {
const findOpts = {
namespaces: [namespace],
@ -4386,4 +4419,136 @@ describe('SavedObjectsRepository', () => {
});
});
});
describe('#openPointInTimeForType', () => {
const type = 'index-pattern';
const generateResults = (id) => ({ id: id || null });
const successResponse = async (type, options) => {
client.openPointInTime.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults())
);
const result = await savedObjectsRepository.openPointInTimeForType(type, options);
expect(client.openPointInTime).toHaveBeenCalledTimes(1);
return result;
};
describe('client calls', () => {
it(`should use the ES PIT API`, async () => {
await successResponse(type);
expect(client.openPointInTime).toHaveBeenCalledTimes(1);
});
it(`accepts preference`, async () => {
await successResponse(type, { preference: 'pref' });
expect(client.openPointInTime).toHaveBeenCalledWith(
expect.objectContaining({
preference: 'pref',
}),
expect.anything()
);
});
it(`accepts keepAlive`, async () => {
await successResponse(type, { keepAlive: '2m' });
expect(client.openPointInTime).toHaveBeenCalledWith(
expect.objectContaining({
keep_alive: '2m',
}),
expect.anything()
);
});
it(`defaults keepAlive to 5m`, async () => {
await successResponse(type);
expect(client.openPointInTime).toHaveBeenCalledWith(
expect.objectContaining({
keep_alive: '5m',
}),
expect.anything()
);
});
});
describe('errors', () => {
const expectNotFoundError = async (types) => {
await expect(savedObjectsRepository.openPointInTimeForType(types)).rejects.toThrowError(
createGenericNotFoundError()
);
};
it(`throws when ES is unable to find the index`, async () => {
client.openPointInTime.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
);
await expectNotFoundError(type);
expect(client.openPointInTime).toHaveBeenCalledTimes(1);
});
it(`should return generic not found error when attempting to find only invalid or hidden types`, async () => {
const test = async (types) => {
await expectNotFoundError(types);
expect(client.openPointInTime).not.toHaveBeenCalled();
};
await test('unknownType');
await test(HIDDEN_TYPE);
await test(['unknownType', HIDDEN_TYPE]);
});
});
describe('returns', () => {
it(`returns id in the expected format`, async () => {
const id = 'abc123';
const results = generateResults(id);
client.openPointInTime.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(results)
);
const response = await savedObjectsRepository.openPointInTimeForType(type);
expect(response).toEqual({ id });
});
});
});
describe('#closePointInTime', () => {
const generateResults = () => ({ succeeded: true, num_freed: 3 });
const successResponse = async (id) => {
client.closePointInTime.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults())
);
const result = await savedObjectsRepository.closePointInTime(id);
expect(client.closePointInTime).toHaveBeenCalledTimes(1);
return result;
};
describe('client calls', () => {
it(`should use the ES PIT API`, async () => {
await successResponse('abc123');
expect(client.closePointInTime).toHaveBeenCalledTimes(1);
});
it(`accepts id`, async () => {
await successResponse('abc123');
expect(client.closePointInTime).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({
id: 'abc123',
}),
}),
expect.anything()
);
});
});
describe('returns', () => {
it(`returns response body from ES`, async () => {
const results = generateResults('abc123');
client.closePointInTime.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(results)
);
const response = await savedObjectsRepository.closePointInTime('abc123');
expect(response).toEqual(results);
});
});
});
});

View file

@ -36,6 +36,10 @@ import {
SavedObjectsCreateOptions,
SavedObjectsFindResponse,
SavedObjectsFindResult,
SavedObjectsClosePointInTimeOptions,
SavedObjectsClosePointInTimeResponse,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsOpenPointInTimeResponse,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsBulkUpdateObject,
@ -706,11 +710,13 @@ export class SavedObjectsRepository {
* Query field argument for more information
* @property {integer} [options.page=1]
* @property {integer} [options.perPage=20]
* @property {Array<unknown>} [options.searchAfter]
* @property {string} [options.sortField]
* @property {string} [options.sortOrder]
* @property {Array<string>} [options.fields]
* @property {string} [options.namespace]
* @property {object} [options.hasReference] - { type, id }
* @property {string} [options.pit]
* @property {string} [options.preference]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
@ -724,6 +730,8 @@ export class SavedObjectsRepository {
hasReferenceOperator,
page = FIND_DEFAULT_PAGE,
perPage = FIND_DEFAULT_PER_PAGE,
pit,
searchAfter,
sortField,
sortOrder,
fields,
@ -750,6 +758,10 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createBadRequestError(
'options.namespaces must be an empty array when options.typeToNamespacesMap is used'
);
} else if (preference?.length && pit) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'options.preference must be excluded when options.pit is used'
);
}
const types = type
@ -785,20 +797,24 @@ export class SavedObjectsRepository {
}
const esOptions = {
index: this.getIndicesForTypes(allowedTypes),
size: perPage,
from: perPage * (page - 1),
// If `pit` is provided, we drop the `index`, otherwise ES returns 400.
...(pit ? {} : { index: this.getIndicesForTypes(allowedTypes) }),
// If `searchAfter` is provided, we drop `from` as it will not be used for pagination.
...(searchAfter ? {} : { from: perPage * (page - 1) }),
_source: includedFields(type, fields),
rest_total_hits_as_int: true,
preference,
rest_total_hits_as_int: true,
size: perPage,
body: {
seq_no_primary_term: true,
...getSearchDsl(this._mappings, this._registry, {
search,
defaultSearchOperator,
searchFields,
pit,
rootSearchFields,
type: allowedTypes,
searchAfter,
sortField,
sortOrder,
namespaces,
@ -832,8 +848,10 @@ export class SavedObjectsRepository {
(hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({
...this._rawToSavedObject(hit),
score: (hit as any)._score,
...((hit as any).sort && { sort: (hit as any).sort }),
})
),
...(body.pit_id && { pit_id: body.pit_id }),
} as SavedObjectsFindResponse<T>;
}
@ -1759,6 +1777,118 @@ export class SavedObjectsRepository {
};
}
/**
* Opens a Point In Time (PIT) against the indices for the specified Saved Object types.
* The returned `id` can then be passed to `SavedObjects.find` to search against that PIT.
*
* @example
* ```ts
* const { id } = await savedObjectsClient.openPointInTimeForType(
* type: 'visualization',
* { keepAlive: '5m' },
* );
* const page1 = await savedObjectsClient.find({
* type: 'visualization',
* sortField: 'updated_at',
* sortOrder: 'asc',
* pit: { id, keepAlive: '2m' },
* });
* const lastHit = page1.saved_objects[page1.saved_objects.length - 1];
* const page2 = await savedObjectsClient.find({
* type: 'visualization',
* sortField: 'updated_at',
* sortOrder: 'asc',
* pit: { id: page1.pit_id },
* searchAfter: lastHit.sort,
* });
* await savedObjectsClient.closePointInTime(page2.pit_id);
* ```
*
* @param {string|Array<string>} type
* @param {object} [options] - {@link SavedObjectsOpenPointInTimeOptions}
* @property {string} [options.keepAlive]
* @property {string} [options.preference]
* @returns {promise} - { id: string }
*/
async openPointInTimeForType(
type: string | string[],
{ keepAlive = '5m', preference }: SavedObjectsOpenPointInTimeOptions = {}
): Promise<SavedObjectsOpenPointInTimeResponse> {
const types = Array.isArray(type) ? type : [type];
const allowedTypes = types.filter((t) => this._allowedTypes.includes(t));
if (allowedTypes.length === 0) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError();
}
const esOptions = {
index: this.getIndicesForTypes(allowedTypes),
keep_alive: keepAlive,
...(preference ? { preference } : {}),
};
const {
body,
statusCode,
} = await this.client.openPointInTime<SavedObjectsOpenPointInTimeResponse>(esOptions, {
ignore: [404],
});
if (statusCode === 404) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError();
}
return {
id: body.id,
};
}
/**
* Closes a Point In Time (PIT) by ID. This simply proxies the request to ES
* via the Elasticsearch client, and is included in the Saved Objects Client
* as a convenience for consumers who are using `openPointInTimeForType`.
*
* @remarks
* While the `keepAlive` that is provided will cause a PIT to automatically close,
* it is highly recommended to explicitly close a PIT when you are done with it
* in order to avoid consuming unneeded resources in Elasticsearch.
*
* @example
* ```ts
* const repository = coreStart.savedObjects.createInternalRepository();
*
* const { id } = await repository.openPointInTimeForType(
* type: 'index-pattern',
* { keepAlive: '2m' },
* );
*
* const response = await repository.find({
* type: 'index-pattern',
* search: 'foo*',
* sortField: 'name',
* sortOrder: 'desc',
* pit: {
* id: 'abc123',
* keepAlive: '2m',
* },
* searchAfter: [1234, 'abcd'],
* });
*
* await repository.closePointInTime(response.pit_id);
* ```
*
* @param {string} id
* @param {object} [options] - {@link SavedObjectsClosePointInTimeOptions}
* @returns {promise} - {@link SavedObjectsClosePointInTimeResponse}
*/
async closePointInTime(
id: string,
options?: SavedObjectsClosePointInTimeOptions
): Promise<SavedObjectsClosePointInTimeResponse> {
const { body } = await this.client.closePointInTime<SavedObjectsClosePointInTimeResponse>({
body: { id },
});
return body;
}
/**
* Returns index specified by the given type or the default index
*

View file

@ -14,11 +14,13 @@ import { decorateEsError } from './decorate_es_error';
const methods = [
'bulk',
'closePointInTime',
'create',
'delete',
'get',
'index',
'mget',
'openPointInTime',
'search',
'update',
'updateByQuery',

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getPitParams } from './pit_params';
describe('searchDsl/getPitParams', () => {
it('returns only an ID by default', () => {
expect(getPitParams({ id: 'abc123' })).toEqual({
pit: {
id: 'abc123',
},
});
});
it('includes keepAlive if provided and rewrites to snake case', () => {
expect(getPitParams({ id: 'abc123', keepAlive: '2m' })).toEqual({
pit: {
id: 'abc123',
keep_alive: '2m',
},
});
});
});

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SavedObjectsPitParams } from '../../../types';
export function getPitParams(pit: SavedObjectsPitParams) {
return {
pit: {
id: pit.id,
...(pit.keepAlive ? { keep_alive: pit.keepAlive } : {}),
},
};
}

View file

@ -6,14 +6,17 @@
* Side Public License, v 1.
*/
jest.mock('./pit_params');
jest.mock('./query_params');
jest.mock('./sorting_params');
import { typeRegistryMock } from '../../../saved_objects_type_registry.mock';
import * as pitParamsNS from './pit_params';
import * as queryParamsNS from './query_params';
import { getSearchDsl } from './search_dsl';
import * as sortParamsNS from './sorting_params';
const getPitParams = pitParamsNS.getPitParams as jest.Mock;
const getQueryParams = queryParamsNS.getQueryParams as jest.Mock;
const getSortingParams = sortParamsNS.getSortingParams as jest.Mock;
@ -84,6 +87,7 @@ describe('getSearchDsl', () => {
type: 'foo',
sortField: 'bar',
sortOrder: 'baz',
pit: { id: 'abc123' },
};
getSearchDsl(mappings, registry, opts);
@ -92,7 +96,8 @@ describe('getSearchDsl', () => {
mappings,
opts.type,
opts.sortField,
opts.sortOrder
opts.sortOrder,
opts.pit
);
});
@ -101,5 +106,33 @@ describe('getSearchDsl', () => {
getSortingParams.mockReturnValue({ b: 'b' });
expect(getSearchDsl(mappings, registry, { type: 'foo' })).toEqual({ a: 'a', b: 'b' });
});
it('returns searchAfter if provided', () => {
getQueryParams.mockReturnValue({ a: 'a' });
getSortingParams.mockReturnValue({ b: 'b' });
expect(getSearchDsl(mappings, registry, { type: 'foo', searchAfter: [1, 'bar'] })).toEqual({
a: 'a',
b: 'b',
search_after: [1, 'bar'],
});
});
it('returns pit if provided', () => {
getQueryParams.mockReturnValue({ a: 'a' });
getSortingParams.mockReturnValue({ b: 'b' });
getPitParams.mockReturnValue({ pit: { id: 'abc123' } });
expect(
getSearchDsl(mappings, registry, {
type: 'foo',
searchAfter: [1, 'bar'],
pit: { id: 'abc123' },
})
).toEqual({
a: 'a',
b: 'b',
pit: { id: 'abc123' },
search_after: [1, 'bar'],
});
});
});
});

View file

@ -9,7 +9,9 @@
import Boom from '@hapi/boom';
import { IndexMapping } from '../../../mappings';
import { SavedObjectsPitParams } from '../../../types';
import { getQueryParams, HasReferenceQueryParams, SearchOperator } from './query_params';
import { getPitParams } from './pit_params';
import { getSortingParams } from './sorting_params';
import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
@ -21,9 +23,11 @@ interface GetSearchDslOptions {
defaultSearchOperator?: SearchOperator;
searchFields?: string[];
rootSearchFields?: string[];
searchAfter?: unknown[];
sortField?: string;
sortOrder?: string;
namespaces?: string[];
pit?: SavedObjectsPitParams;
typeToNamespacesMap?: Map<string, string[] | undefined>;
hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[];
hasReferenceOperator?: SearchOperator;
@ -41,9 +45,11 @@ export function getSearchDsl(
defaultSearchOperator,
searchFields,
rootSearchFields,
searchAfter,
sortField,
sortOrder,
namespaces,
pit,
typeToNamespacesMap,
hasReference,
hasReferenceOperator,
@ -72,6 +78,8 @@ export function getSearchDsl(
hasReferenceOperator,
kueryNode,
}),
...getSortingParams(mappings, type, sortField, sortOrder),
...getSortingParams(mappings, type, sortField, sortOrder, pit),
...(pit ? getPitParams(pit) : {}),
...(searchAfter ? { search_after: searchAfter } : {}),
};
}

View file

@ -79,6 +79,11 @@ describe('searchDsl/getSortParams', () => {
],
});
});
it('appends tiebreaker when PIT is provided', () => {
expect(getSortingParams(MAPPINGS, 'saved', 'title', undefined, { id: 'abc' }).sort).toEqual(
expect.arrayContaining([{ _shard_doc: 'asc' }])
);
});
});
describe('sortField is simple root property with multiple types', () => {
it('returns correct params', () => {
@ -93,6 +98,11 @@ describe('searchDsl/getSortParams', () => {
],
});
});
it('appends tiebreaker when PIT is provided', () => {
expect(
getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', undefined, { id: 'abc' }).sort
).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
});
});
describe('sortField is simple non-root property with multiple types', () => {
it('returns correct params', () => {
@ -114,6 +124,11 @@ describe('searchDsl/getSortParams', () => {
],
});
});
it('appends tiebreaker when PIT is provided', () => {
expect(
getSortingParams(MAPPINGS, 'saved', 'title.raw', undefined, { id: 'abc' }).sort
).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
});
});
describe('sortField is multi-field with single type as array', () => {
it('returns correct params', () => {
@ -128,6 +143,11 @@ describe('searchDsl/getSortParams', () => {
],
});
});
it('appends tiebreaker when PIT is provided', () => {
expect(
getSortingParams(MAPPINGS, ['saved'], 'title.raw', undefined, { id: 'abc' }).sort
).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
});
});
describe('sortField is root multi-field with multiple types', () => {
it('returns correct params', () => {
@ -142,6 +162,12 @@ describe('searchDsl/getSortParams', () => {
],
});
});
it('appends tiebreaker when PIT is provided', () => {
expect(
getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', undefined, { id: 'abc' })
.sort
).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
});
});
describe('sortField is not-root multi-field with multiple types', () => {
it('returns correct params', () => {

View file

@ -8,6 +8,12 @@
import Boom from '@hapi/boom';
import { getProperty, IndexMapping } from '../../../mappings';
import { SavedObjectsPitParams } from '../../../types';
// TODO: The plan is for ES to automatically add this tiebreaker when
// using PIT. We should remove this logic once that is resolved.
// https://github.com/elastic/elasticsearch/issues/56828
const ES_PROVIDED_TIEBREAKER = { _shard_doc: 'asc' };
const TOP_LEVEL_FIELDS = ['_id', '_score'];
@ -15,7 +21,8 @@ export function getSortingParams(
mappings: IndexMapping,
type: string | string[],
sortField?: string,
sortOrder?: string
sortOrder?: string,
pit?: SavedObjectsPitParams
) {
if (!sortField) {
return {};
@ -31,6 +38,7 @@ export function getSortingParams(
order: sortOrder,
},
},
...(pit ? [ES_PROVIDED_TIEBREAKER] : []),
],
};
}
@ -51,6 +59,7 @@ export function getSortingParams(
unmapped_type: rootField.type,
},
},
...(pit ? [ES_PROVIDED_TIEBREAKER] : []),
],
};
}
@ -75,6 +84,7 @@ export function getSortingParams(
unmapped_type: field.type,
},
},
...(pit ? [ES_PROVIDED_TIEBREAKER] : []),
],
};
}

View file

@ -20,6 +20,8 @@ const create = () =>
bulkGet: jest.fn(),
find: jest.fn(),
get: jest.fn(),
closePointInTime: jest.fn(),
openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }),
resolve: jest.fn(),
update: jest.fn(),
addToNamespaces: jest.fn(),

View file

@ -115,6 +115,36 @@ test(`#get`, async () => {
expect(result).toBe(returnValue);
});
test(`#openPointInTimeForType`, async () => {
const returnValue = Symbol();
const mockRepository = {
openPointInTimeForType: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const options = Symbol();
const result = await client.openPointInTimeForType(type, options);
expect(mockRepository.openPointInTimeForType).toHaveBeenCalledWith(type, options);
expect(result).toBe(returnValue);
});
test(`#closePointInTime`, async () => {
const returnValue = Symbol();
const mockRepository = {
closePointInTime: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const id = Symbol();
const options = Symbol();
const result = await client.closePointInTime(id, options);
expect(mockRepository.closePointInTime).toHaveBeenCalledWith(id, options);
expect(result).toBe(returnValue);
});
test(`#resolve`, async () => {
const returnValue = Symbol();
const mockRepository = {

View file

@ -129,6 +129,35 @@ export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T> {
* The Elasticsearch `_score` of this result.
*/
score: number;
/**
* The Elasticsearch `sort` value of this result.
*
* @remarks
* This can be passed directly to the `searchAfter` param in the {@link SavedObjectsFindOptions}
* in order to page through large numbers of hits. It is recommended you use this alongside
* a Point In Time (PIT) that was opened with {@link SavedObjectsClient.openPointInTimeForType}.
*
* @example
* ```ts
* const { id } = await savedObjectsClient.openPointInTimeForType('visualization');
* const page1 = await savedObjectsClient.find({
* type: 'visualization',
* sortField: 'updated_at',
* sortOrder: 'asc',
* pit: { id },
* });
* const lastHit = page1.saved_objects[page1.saved_objects.length - 1];
* const page2 = await savedObjectsClient.find({
* type: 'visualization',
* sortField: 'updated_at',
* sortOrder: 'asc',
* pit: { id: page1.pit_id },
* searchAfter: lastHit.sort,
* });
* await savedObjectsClient.closePointInTime(page2.pit_id);
* ```
*/
sort?: unknown[];
}
/**
@ -144,6 +173,7 @@ export interface SavedObjectsFindResponse<T = unknown> {
total: number;
per_page: number;
page: number;
pit_id?: string;
}
/**
@ -311,6 +341,50 @@ export interface SavedObjectsResolveResponse<T = unknown> {
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
}
/**
* @public
*/
export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions {
/**
* Optionally specify how long ES should keep the PIT alive until the next request. Defaults to `5m`.
*/
keepAlive?: string;
/**
* An optional ES preference value to be used for the query.
*/
preference?: string;
}
/**
* @public
*/
export interface SavedObjectsOpenPointInTimeResponse {
/**
* PIT ID returned from ES.
*/
id: string;
}
/**
* @public
*/
export type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions;
/**
* @public
*/
export interface SavedObjectsClosePointInTimeResponse {
/**
* If true, all search contexts associated with the PIT id are
* successfully closed.
*/
succeeded: boolean;
/**
* The number of search contexts that have been successfully closed.
*/
num_freed: number;
}
/**
*
* @public
@ -504,4 +578,25 @@ export class SavedObjectsClient {
) {
return await this._repository.removeReferencesTo(type, id, options);
}
/**
* Opens a Point In Time (PIT) against the indices for the specified Saved Object types.
* The returned `id` can then be passed to {@link SavedObjectsClient.find} to search
* against that PIT.
*/
async openPointInTimeForType(
type: string | string[],
options: SavedObjectsOpenPointInTimeOptions = {}
) {
return await this._repository.openPointInTimeForType(type, options);
}
/**
* Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the
* Elasticsearch client, and is included in the Saved Objects Client as a convenience
* for consumers who are using {@link SavedObjectsClient.openPointInTimeForType}.
*/
async closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions) {
return await this._repository.closePointInTime(id, options);
}
}

View file

@ -62,6 +62,14 @@ export interface SavedObjectsFindOptionsReference {
id: string;
}
/**
* @public
*/
export interface SavedObjectsPitParams {
id: string;
keepAlive?: string;
}
/**
*
* @public
@ -82,6 +90,10 @@ export interface SavedObjectsFindOptions {
search?: string;
/** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */
searchFields?: string[];
/**
* Use the sort values from the previous page to retrieve the next page of results.
*/
searchAfter?: unknown[];
/**
* The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not
* be modified. If used in conjunction with `searchFields`, both are concatenated together.
@ -114,6 +126,10 @@ export interface SavedObjectsFindOptions {
typeToNamespacesMap?: Map<string, string[] | undefined>;
/** An optional ES preference value to be used for the query **/
preference?: string;
/**
* Search against a specific Point In Time (PIT) that you've opened with {@link SavedObjectsClient.openPointInTimeForType}.
*/
pit?: SavedObjectsPitParams;
}
/**

View file

@ -2223,6 +2223,7 @@ export class SavedObjectsClient {
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsCheckConflictsResponse>;
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise<SavedObjectsClosePointInTimeResponse>;
create<T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
@ -2232,6 +2233,7 @@ export class SavedObjectsClient {
errors: typeof SavedObjectsErrorHelpers;
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise<SavedObjectsOpenPointInTimeResponse>;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
@ -2270,6 +2272,15 @@ export interface SavedObjectsClientWrapperOptions {
typeRegistry: ISavedObjectTypeRegistry;
}
// @public (undocumented)
export type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions;
// @public (undocumented)
export interface SavedObjectsClosePointInTimeResponse {
num_freed: number;
succeeded: boolean;
}
// @public
export interface SavedObjectsComplexFieldMapping {
// (undocumented)
@ -2414,10 +2425,11 @@ export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOp
export class SavedObjectsExporter {
// (undocumented)
#private;
constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: {
constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, }: {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
logger: Logger;
});
exportByObjects(options: SavedObjectsExportByObjectOptions): Promise<import("stream").Readable>;
exportByTypes(options: SavedObjectsExportByTypeOptions): Promise<import("stream").Readable>;
@ -2475,9 +2487,11 @@ export interface SavedObjectsFindOptions {
page?: number;
// (undocumented)
perPage?: number;
pit?: SavedObjectsPitParams;
preference?: string;
rootSearchFields?: string[];
search?: string;
searchAfter?: unknown[];
searchFields?: string[];
// (undocumented)
sortField?: string;
@ -2503,6 +2517,8 @@ export interface SavedObjectsFindResponse<T = unknown> {
// (undocumented)
per_page: number;
// (undocumented)
pit_id?: string;
// (undocumented)
saved_objects: Array<SavedObjectsFindResult<T>>;
// (undocumented)
total: number;
@ -2511,6 +2527,7 @@ export interface SavedObjectsFindResponse<T = unknown> {
// @public (undocumented)
export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T> {
score: number;
sort?: unknown[];
}
// @public
@ -2737,6 +2754,25 @@ export interface SavedObjectsMigrationVersion {
// @public
export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic';
// @public (undocumented)
export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions {
keepAlive?: string;
preference?: string;
}
// @public (undocumented)
export interface SavedObjectsOpenPointInTimeResponse {
id: string;
}
// @public (undocumented)
export interface SavedObjectsPitParams {
// (undocumented)
id: string;
// (undocumented)
keepAlive?: string;
}
// @public
export interface SavedObjectsRawDoc {
// (undocumented)
@ -2773,6 +2809,7 @@ export class SavedObjectsRepository {
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsCheckConflictsResponse>;
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise<SavedObjectsClosePointInTimeResponse>;
create<T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
// Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts
//
@ -2785,6 +2822,7 @@ export class SavedObjectsRepository {
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
incrementCounter<T = unknown>(type: string, id: string, counterFields: Array<string | SavedObjectsIncrementCounterField>, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise<SavedObjectsOpenPointInTimeResponse>;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
@ -2949,10 +2987,12 @@ export interface SearchResponse<T = unknown> {
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: string[];
sort?: unknown[];
}>;
};
// (undocumented)
pit_id?: string;
// (undocumented)
_scroll_id?: string;
// (undocumented)
_shards: ShardsResponse;

View file

@ -31,6 +31,7 @@ export type {
SavedObjectStatusMeta,
SavedObjectsFindOptionsReference,
SavedObjectsFindOptions,
SavedObjectsPitParams,
SavedObjectsBaseOptions,
MutatingOperationRefreshSetting,
SavedObjectsClientContract,

View file

@ -1142,7 +1142,7 @@ export class Plugin implements Plugin_2<PluginSetup, PluginStart, DataPluginSetu
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "create" | "update" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "closePointInTime" | "create" | "update" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
};

View file

@ -295,43 +295,43 @@ export default function ({ getService }: FtrProviderContext) {
);
expect(resp.header['content-type']).to.eql('application/ndjson');
const objects = ndjsonToObject(resp.text);
expect(objects).to.eql([
{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON:
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
// Sort values aren't deterministic so we need to exclude them
const { sort, ...obj } = objects[0];
expect(obj).to.eql({
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
},
]);
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
});
expect(objects[0].migrationVersion).to.be.ok();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
@ -355,43 +355,43 @@ export default function ({ getService }: FtrProviderContext) {
);
expect(resp.header['content-type']).to.eql('application/ndjson');
const objects = ndjsonToObject(resp.text);
expect(objects).to.eql([
{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON:
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
// Sort values aren't deterministic so we need to exclude them
const { sort, ...obj } = objects[0];
expect(obj).to.eql({
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
},
]);
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
});
expect(objects[0].migrationVersion).to.be.ok();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
@ -420,43 +420,43 @@ export default function ({ getService }: FtrProviderContext) {
);
expect(resp.header['content-type']).to.eql('application/ndjson');
const objects = ndjsonToObject(resp.text);
expect(objects).to.eql([
{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON:
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
// Sort values aren't deterministic so we need to exclude them
const { sort, ...obj } = objects[0];
expect(obj).to.eql({
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
},
]);
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
coreMigrationVersion: KIBANA_VERSION,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
});
expect(objects[0].migrationVersion).to.be.ok();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
@ -511,7 +511,37 @@ export default function ({ getService }: FtrProviderContext) {
await esArchiver.unload('saved_objects/10k');
});
it('should return 400 when exporting more than 10,000', async () => {
it('should allow exporting more than 10,000 objects if permitted by maxImportExportSize', async () => {
await supertest
.post('/api/saved_objects/_export')
.send({
type: ['dashboard', 'visualization', 'search', 'index-pattern'],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
expect(resp.header['content-disposition']).to.eql(
'attachment; filename="export.ndjson"'
);
expect(resp.header['content-type']).to.eql('application/ndjson');
const objects = ndjsonToObject(resp.text);
expect(objects.length).to.eql(10001);
});
});
it('should return 400 when exporting more than allowed by maxImportExportSize', async () => {
let anotherCustomVisId: string;
await supertest
.post('/api/saved_objects/visualization')
.send({
attributes: {
title: 'My other favorite vis',
},
})
.expect(200)
.then((resp) => {
anotherCustomVisId = resp.body.id;
});
await supertest
.post('/api/saved_objects/_export')
.send({
@ -523,9 +553,13 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: `Can't export more than 10000 objects`,
message: `Can't export more than 10001 objects`,
});
});
await supertest
// @ts-expect-error TS complains about using `anotherCustomVisId` before it is assigned
.delete(`/api/saved_objects/visualization/${anotherCustomVisId}`)
.expect(200);
});
});
});

View file

@ -166,7 +166,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should return 400 when trying to import more than 10,000 objects', async () => {
const fileChunks = [];
for (let i = 0; i < 10001; i++) {
for (let i = 0; i <= 10001; i++) {
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
}
await supertest
@ -177,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: "Can't import more than 10000 objects",
message: "Can't import more than 10001 objects",
});
});
});

View file

@ -132,9 +132,9 @@ export default function ({ getService }: FtrProviderContext) {
});
});
it('should return 400 when resolving conflicts with a file containing more than 10,000 objects', async () => {
it('should return 400 when resolving conflicts with a file containing more than 10,001 objects', async () => {
const fileChunks = [];
for (let i = 0; i < 10001; i++) {
for (let i = 0; i <= 10001; i++) {
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
}
await supertest
@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: "Can't import more than 10000 objects",
message: "Can't import more than 10001 objects",
});
});
});

View file

@ -30,6 +30,7 @@ export default async function ({ readConfigFile }) {
'--elasticsearch.healthCheck.delay=3600000',
'--server.xsrf.disableProtection=true',
'--server.compression.referrerWhitelist=["some-host.com"]',
`--savedObjects.maxImportExportSize=10001`,
],
},
};

View file

@ -1757,3 +1757,65 @@ describe('#removeReferencesTo', () => {
expect(mockBaseClient.removeReferencesTo).toHaveBeenCalledTimes(1);
});
});
describe('#openPointInTimeForType', () => {
it('redirects request to underlying base client', async () => {
const options = { keepAlive: '1m' };
await wrapper.openPointInTimeForType('some-type', options);
expect(mockBaseClient.openPointInTimeForType).toHaveBeenCalledTimes(1);
expect(mockBaseClient.openPointInTimeForType).toHaveBeenCalledWith('some-type', options);
});
it('returns response from underlying client', async () => {
const returnValue = {
id: 'abc123',
};
mockBaseClient.openPointInTimeForType.mockResolvedValue(returnValue);
const result = await wrapper.openPointInTimeForType('known-type');
expect(result).toBe(returnValue);
});
it('fails if base client fails', async () => {
const failureReason = new Error('Something bad happened...');
mockBaseClient.openPointInTimeForType.mockRejectedValue(failureReason);
await expect(wrapper.openPointInTimeForType('known-type')).rejects.toThrowError(failureReason);
expect(mockBaseClient.openPointInTimeForType).toHaveBeenCalledTimes(1);
});
});
describe('#closePointInTime', () => {
it('redirects request to underlying base client', async () => {
const id = 'abc123';
await wrapper.closePointInTime(id);
expect(mockBaseClient.closePointInTime).toHaveBeenCalledTimes(1);
expect(mockBaseClient.closePointInTime).toHaveBeenCalledWith(id, undefined);
});
it('returns response from underlying client', async () => {
const returnValue = {
succeeded: true,
num_freed: 1,
};
mockBaseClient.closePointInTime.mockResolvedValue(returnValue);
const result = await wrapper.closePointInTime('abc123');
expect(result).toBe(returnValue);
});
it('fails if base client fails', async () => {
const failureReason = new Error('Something bad happened...');
mockBaseClient.closePointInTime.mockRejectedValue(failureReason);
await expect(wrapper.closePointInTime('abc123')).rejects.toThrowError(failureReason);
expect(mockBaseClient.closePointInTime).toHaveBeenCalledTimes(1);
});
});

View file

@ -15,9 +15,11 @@ import {
SavedObjectsBulkUpdateResponse,
SavedObjectsCheckConflictsObject,
SavedObjectsClientContract,
SavedObjectsClosePointInTimeOptions,
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
SavedObjectsFindResponse,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
@ -249,6 +251,17 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
return await this.options.baseClient.removeReferencesTo(type, id, options);
}
public async openPointInTimeForType(
type: string | string[],
options: SavedObjectsOpenPointInTimeOptions = {}
) {
return await this.options.baseClient.openPointInTimeForType(type, options);
}
public async closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions) {
return await this.options.baseClient.closePointInTime(id, options);
}
/**
* Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't
* registered, response is returned as is.

View file

@ -190,6 +190,8 @@ export enum SavedObjectAction {
ADD_TO_SPACES = 'saved_object_add_to_spaces',
DELETE_FROM_SPACES = 'saved_object_delete_from_spaces',
REMOVE_REFERENCES = 'saved_object_remove_references',
OPEN_POINT_IN_TIME = 'saved_object_open_point_in_time',
CLOSE_POINT_IN_TIME = 'saved_object_close_point_in_time',
}
type VerbsTuple = [string, string, string];
@ -203,6 +205,16 @@ const savedObjectAuditVerbs: Record<SavedObjectAction, VerbsTuple> = {
saved_object_find: ['access', 'accessing', 'accessed'],
saved_object_add_to_spaces: ['update', 'updating', 'updated'],
saved_object_delete_from_spaces: ['update', 'updating', 'updated'],
saved_object_open_point_in_time: [
'open point-in-time',
'opening point-in-time',
'opened point-in-time',
],
saved_object_close_point_in_time: [
'close point-in-time',
'closing point-in-time',
'closed point-in-time',
],
saved_object_remove_references: [
'remove references to',
'removing references to',
@ -219,6 +231,8 @@ const savedObjectAuditTypes: Record<SavedObjectAction, EventType> = {
saved_object_find: EventType.ACCESS,
saved_object_add_to_spaces: EventType.CHANGE,
saved_object_delete_from_spaces: EventType.CHANGE,
saved_object_open_point_in_time: EventType.CREATION,
saved_object_close_point_in_time: EventType.DELETION,
saved_object_remove_references: EventType.CHANGE,
};

View file

@ -9,7 +9,13 @@ import { flatten, uniq } from 'lodash';
import { FeatureKibanaPrivileges } from '../../../../../features/server';
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
const readOperations: string[] = ['bulk_get', 'get', 'find'];
const readOperations: string[] = [
'bulk_get',
'get',
'find',
'open_point_in_time',
'close_point_in_time',
];
const writeOperations: string[] = [
'create',
'bulk_create',

View file

@ -101,6 +101,8 @@ describe('features', () => {
actions.savedObject.get('all-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('all-savedObject-all-1', 'get'),
actions.savedObject.get('all-savedObject-all-1', 'find'),
actions.savedObject.get('all-savedObject-all-1', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-all-1', 'close_point_in_time'),
actions.savedObject.get('all-savedObject-all-1', 'create'),
actions.savedObject.get('all-savedObject-all-1', 'bulk_create'),
actions.savedObject.get('all-savedObject-all-1', 'update'),
@ -110,6 +112,8 @@ describe('features', () => {
actions.savedObject.get('all-savedObject-all-2', 'bulk_get'),
actions.savedObject.get('all-savedObject-all-2', 'get'),
actions.savedObject.get('all-savedObject-all-2', 'find'),
actions.savedObject.get('all-savedObject-all-2', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-all-2', 'close_point_in_time'),
actions.savedObject.get('all-savedObject-all-2', 'create'),
actions.savedObject.get('all-savedObject-all-2', 'bulk_create'),
actions.savedObject.get('all-savedObject-all-2', 'update'),
@ -119,9 +123,13 @@ describe('features', () => {
actions.savedObject.get('all-savedObject-read-1', 'bulk_get'),
actions.savedObject.get('all-savedObject-read-1', 'get'),
actions.savedObject.get('all-savedObject-read-1', 'find'),
actions.savedObject.get('all-savedObject-read-1', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-read-1', 'close_point_in_time'),
actions.savedObject.get('all-savedObject-read-2', 'bulk_get'),
actions.savedObject.get('all-savedObject-read-2', 'get'),
actions.savedObject.get('all-savedObject-read-2', 'find'),
actions.savedObject.get('all-savedObject-read-2', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-read-2', 'close_point_in_time'),
actions.ui.get('foo', 'all-ui-1'),
actions.ui.get('foo', 'all-ui-2'),
];
@ -132,6 +140,8 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-1', 'get'),
actions.savedObject.get('read-savedObject-all-1', 'find'),
actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-all-1', 'create'),
actions.savedObject.get('read-savedObject-all-1', 'bulk_create'),
actions.savedObject.get('read-savedObject-all-1', 'update'),
@ -141,6 +151,8 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-all-2', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-2', 'get'),
actions.savedObject.get('read-savedObject-all-2', 'find'),
actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-all-2', 'create'),
actions.savedObject.get('read-savedObject-all-2', 'bulk_create'),
actions.savedObject.get('read-savedObject-all-2', 'update'),
@ -150,9 +162,13 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-read-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-read-1', 'get'),
actions.savedObject.get('read-savedObject-read-1', 'find'),
actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-read-2', 'bulk_get'),
actions.savedObject.get('read-savedObject-read-2', 'get'),
actions.savedObject.get('read-savedObject-read-2', 'find'),
actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'),
actions.ui.get('foo', 'read-ui-1'),
actions.ui.get('foo', 'read-ui-2'),
];
@ -274,6 +290,8 @@ describe('features', () => {
actions.savedObject.get('all-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('all-savedObject-all-1', 'get'),
actions.savedObject.get('all-savedObject-all-1', 'find'),
actions.savedObject.get('all-savedObject-all-1', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-all-1', 'close_point_in_time'),
actions.savedObject.get('all-savedObject-all-1', 'create'),
actions.savedObject.get('all-savedObject-all-1', 'bulk_create'),
actions.savedObject.get('all-savedObject-all-1', 'update'),
@ -283,6 +301,8 @@ describe('features', () => {
actions.savedObject.get('all-savedObject-all-2', 'bulk_get'),
actions.savedObject.get('all-savedObject-all-2', 'get'),
actions.savedObject.get('all-savedObject-all-2', 'find'),
actions.savedObject.get('all-savedObject-all-2', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-all-2', 'close_point_in_time'),
actions.savedObject.get('all-savedObject-all-2', 'create'),
actions.savedObject.get('all-savedObject-all-2', 'bulk_create'),
actions.savedObject.get('all-savedObject-all-2', 'update'),
@ -292,9 +312,13 @@ describe('features', () => {
actions.savedObject.get('all-savedObject-read-1', 'bulk_get'),
actions.savedObject.get('all-savedObject-read-1', 'get'),
actions.savedObject.get('all-savedObject-read-1', 'find'),
actions.savedObject.get('all-savedObject-read-1', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-read-1', 'close_point_in_time'),
actions.savedObject.get('all-savedObject-read-2', 'bulk_get'),
actions.savedObject.get('all-savedObject-read-2', 'get'),
actions.savedObject.get('all-savedObject-read-2', 'find'),
actions.savedObject.get('all-savedObject-read-2', 'open_point_in_time'),
actions.savedObject.get('all-savedObject-read-2', 'close_point_in_time'),
actions.ui.get('foo', 'all-ui-1'),
actions.ui.get('foo', 'all-ui-2'),
actions.ui.get('catalogue', 'read-catalogue-1'),
@ -304,6 +328,8 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-1', 'get'),
actions.savedObject.get('read-savedObject-all-1', 'find'),
actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-all-1', 'create'),
actions.savedObject.get('read-savedObject-all-1', 'bulk_create'),
actions.savedObject.get('read-savedObject-all-1', 'update'),
@ -313,6 +339,8 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-all-2', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-2', 'get'),
actions.savedObject.get('read-savedObject-all-2', 'find'),
actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-all-2', 'create'),
actions.savedObject.get('read-savedObject-all-2', 'bulk_create'),
actions.savedObject.get('read-savedObject-all-2', 'update'),
@ -322,9 +350,13 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-read-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-read-1', 'get'),
actions.savedObject.get('read-savedObject-read-1', 'find'),
actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-read-2', 'bulk_get'),
actions.savedObject.get('read-savedObject-read-2', 'get'),
actions.savedObject.get('read-savedObject-read-2', 'find'),
actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'),
actions.ui.get('foo', 'read-ui-1'),
actions.ui.get('foo', 'read-ui-2'),
]);
@ -388,6 +420,8 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-1', 'get'),
actions.savedObject.get('read-savedObject-all-1', 'find'),
actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-all-1', 'create'),
actions.savedObject.get('read-savedObject-all-1', 'bulk_create'),
actions.savedObject.get('read-savedObject-all-1', 'update'),
@ -397,6 +431,8 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-all-2', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-2', 'get'),
actions.savedObject.get('read-savedObject-all-2', 'find'),
actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-all-2', 'create'),
actions.savedObject.get('read-savedObject-all-2', 'bulk_create'),
actions.savedObject.get('read-savedObject-all-2', 'update'),
@ -406,9 +442,13 @@ describe('features', () => {
actions.savedObject.get('read-savedObject-read-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-read-1', 'get'),
actions.savedObject.get('read-savedObject-read-1', 'find'),
actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'),
actions.savedObject.get('read-savedObject-read-2', 'bulk_get'),
actions.savedObject.get('read-savedObject-read-2', 'get'),
actions.savedObject.get('read-savedObject-read-2', 'find'),
actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'),
actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'),
actions.ui.get('foo', 'read-ui-1'),
actions.ui.get('foo', 'read-ui-2'),
]);
@ -691,6 +731,8 @@ describe('reserved', () => {
actions.savedObject.get('savedObject-all-1', 'bulk_get'),
actions.savedObject.get('savedObject-all-1', 'get'),
actions.savedObject.get('savedObject-all-1', 'find'),
actions.savedObject.get('savedObject-all-1', 'open_point_in_time'),
actions.savedObject.get('savedObject-all-1', 'close_point_in_time'),
actions.savedObject.get('savedObject-all-1', 'create'),
actions.savedObject.get('savedObject-all-1', 'bulk_create'),
actions.savedObject.get('savedObject-all-1', 'update'),
@ -700,6 +742,8 @@ describe('reserved', () => {
actions.savedObject.get('savedObject-all-2', 'bulk_get'),
actions.savedObject.get('savedObject-all-2', 'get'),
actions.savedObject.get('savedObject-all-2', 'find'),
actions.savedObject.get('savedObject-all-2', 'open_point_in_time'),
actions.savedObject.get('savedObject-all-2', 'close_point_in_time'),
actions.savedObject.get('savedObject-all-2', 'create'),
actions.savedObject.get('savedObject-all-2', 'bulk_create'),
actions.savedObject.get('savedObject-all-2', 'update'),
@ -709,9 +753,13 @@ describe('reserved', () => {
actions.savedObject.get('savedObject-read-1', 'bulk_get'),
actions.savedObject.get('savedObject-read-1', 'get'),
actions.savedObject.get('savedObject-read-1', 'find'),
actions.savedObject.get('savedObject-read-1', 'open_point_in_time'),
actions.savedObject.get('savedObject-read-1', 'close_point_in_time'),
actions.savedObject.get('savedObject-read-2', 'bulk_get'),
actions.savedObject.get('savedObject-read-2', 'get'),
actions.savedObject.get('savedObject-read-2', 'find'),
actions.savedObject.get('savedObject-read-2', 'open_point_in_time'),
actions.savedObject.get('savedObject-read-2', 'close_point_in_time'),
actions.ui.get('foo', 'ui-1'),
actions.ui.get('foo', 'ui-2'),
]);
@ -823,6 +871,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -832,6 +882,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -952,6 +1004,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -961,6 +1015,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -970,6 +1026,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -979,6 +1037,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -995,6 +1055,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1004,6 +1066,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1026,6 +1090,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1035,6 +1101,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1044,6 +1112,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1053,6 +1123,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1063,6 +1135,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1072,6 +1146,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1081,6 +1157,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1090,6 +1168,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1160,6 +1240,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1169,6 +1251,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1178,6 +1262,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1187,6 +1273,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1203,6 +1291,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1212,6 +1302,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1304,6 +1396,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1313,6 +1407,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1322,6 +1418,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1331,6 +1429,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1365,6 +1465,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1374,6 +1476,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1389,6 +1493,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1398,6 +1504,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1473,6 +1581,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1482,6 +1592,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1491,6 +1603,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1500,6 +1614,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1606,6 +1722,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1615,6 +1733,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1627,6 +1747,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1636,6 +1758,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1654,6 +1778,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1663,6 +1789,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1672,6 +1800,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1681,6 +1811,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1691,6 +1823,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1700,6 +1834,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1709,6 +1845,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1718,6 +1856,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1808,6 +1948,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1817,6 +1959,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1833,6 +1977,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1842,6 +1988,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1864,6 +2012,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1873,6 +2023,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1882,6 +2034,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1891,6 +2045,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1901,6 +2057,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1910,6 +2068,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -1919,6 +2079,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -1928,6 +2090,8 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
@ -2018,6 +2182,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -2027,6 +2193,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
@ -2036,9 +2204,13 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
@ -2056,6 +2228,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -2065,6 +2239,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
@ -2074,9 +2250,13 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
@ -2100,6 +2280,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -2109,6 +2291,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
@ -2118,9 +2302,13 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
@ -2131,6 +2319,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -2140,6 +2330,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
@ -2149,9 +2341,13 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
@ -2163,6 +2359,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -2172,6 +2370,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
@ -2181,9 +2381,13 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
@ -2194,6 +2398,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
@ -2203,6 +2409,8 @@ describe('subFeatures', () => {
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
@ -2212,9 +2420,13 @@ describe('subFeatures', () => {
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'open_point_in_time'),
actions.savedObject.get('read-licensed-sub-feature-type', 'close_point_in_time'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),

View file

@ -905,6 +905,17 @@ describe('#find', () => {
);
});
test(`throws BadRequestError when searching across namespaces when pit is provided`, async () => {
const options = {
type: [type1, type2],
pit: { id: 'abc123' },
namespaces: ['some-ns', 'another-ns'],
};
await expect(client.find(options)).rejects.toThrowErrorMatchingInlineSnapshot(
`"_find across namespaces is not permitted when using the \`pit\` option."`
);
});
test(`checks privileges for user, actions, and namespaces`, async () => {
const options = { type: [type1, type2], namespaces };
await expectPrivilegeCheck(client.find, { options }, namespaces);
@ -987,6 +998,64 @@ describe('#get', () => {
});
});
describe('#openPointInTimeForType', () => {
const type = 'foo';
const namespace = 'some-ns';
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
await expectGeneralError(client.openPointInTimeForType, { type });
});
test(`returns result of baseClient.openPointInTimeForType when authorized`, async () => {
const apiCallReturnValue = Symbol();
clientOpts.baseClient.openPointInTimeForType.mockReturnValue(apiCallReturnValue as any);
const options = { namespace };
const result = await expectSuccess(client.openPointInTimeForType, { type, options });
expect(result).toBe(apiCallReturnValue);
});
test(`adds audit event when successful`, async () => {
const apiCallReturnValue = Symbol();
clientOpts.baseClient.openPointInTimeForType.mockReturnValue(apiCallReturnValue as any);
const options = { namespace };
await expectSuccess(client.openPointInTimeForType, { type, options });
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
expectAuditEvent('saved_object_open_point_in_time', EventOutcome.UNKNOWN);
});
test(`adds audit event when not successful`, async () => {
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
await expect(() => client.openPointInTimeForType(type, { namespace })).rejects.toThrow();
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
expectAuditEvent('saved_object_open_point_in_time', EventOutcome.FAILURE);
});
});
describe('#closePointInTime', () => {
const id = 'abc123';
const namespace = 'some-ns';
test(`returns result of baseClient.closePointInTime`, async () => {
const apiCallReturnValue = Symbol();
clientOpts.baseClient.closePointInTime.mockReturnValue(apiCallReturnValue as any);
const options = { namespace };
const result = await client.closePointInTime(id, options);
expect(result).toBe(apiCallReturnValue);
});
test(`adds audit event`, async () => {
const apiCallReturnValue = Symbol();
clientOpts.baseClient.closePointInTime.mockReturnValue(apiCallReturnValue as any);
const options = { namespace };
await client.closePointInTime(id, options);
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
expectAuditEvent('saved_object_close_point_in_time', EventOutcome.UNKNOWN);
});
});
describe('#resolve', () => {
const type = 'foo';
const id = `${type}-id`;

View file

@ -17,6 +17,8 @@ import {
SavedObjectsCreateOptions,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsFindOptions,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsClosePointInTimeOptions,
SavedObjectsRemoveReferencesToOptions,
SavedObjectsUpdateOptions,
SavedObjectsUtils,
@ -223,6 +225,11 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
`_find across namespaces is not permitted when the Spaces plugin is disabled.`
);
}
if (options.pit && Array.isArray(options.namespaces) && options.namespaces.length > 1) {
throw this.errors.createBadRequestError(
'_find across namespaces is not permitted when using the `pit` option.'
);
}
const args = { options };
const { status, typeMap } = await this.ensureAuthorized(
@ -562,6 +569,57 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
return await this.baseClient.removeReferencesTo(type, id, options);
}
public async openPointInTimeForType(
type: string | string[],
options: SavedObjectsOpenPointInTimeOptions
) {
try {
const args = { type, options };
await this.ensureAuthorized(type, 'open_point_in_time', options?.namespace, {
args,
// Partial authorization is acceptable in this case because this method is only designed
// to be used with `find`, which already allows for partial authorization.
requireFullAuthorization: false,
});
} catch (error) {
this.auditLogger.log(
savedObjectEvent({
action: SavedObjectAction.OPEN_POINT_IN_TIME,
error,
})
);
throw error;
}
this.auditLogger.log(
savedObjectEvent({
action: SavedObjectAction.OPEN_POINT_IN_TIME,
outcome: EventOutcome.UNKNOWN,
})
);
return await this.baseClient.openPointInTimeForType(type, options);
}
public async closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions) {
// We are intentionally omitting a call to `ensureAuthorized` here, because `closePointInTime`
// doesn't take in `types`, which are required to perform authorization. As there is no way
// to know what index/indices a PIT was created against, we have no practical means of
// authorizing users. We've decided we are okay with this because:
// (a) Elasticsearch only requires `read` privileges on an index in order to open/close
// a PIT against it, and;
// (b) By the time a user is accessing this service, they are already authenticated
// to Kibana, which is our closest equivalent to Elasticsearch's `read`.
this.auditLogger.log(
savedObjectEvent({
action: SavedObjectAction.CLOSE_POINT_IN_TIME,
outcome: EventOutcome.UNKNOWN,
})
);
return await this.baseClient.closePointInTime(id, options);
}
private async checkPrivileges(
actions: string | string[],
namespaceOrNamespaces?: string | Array<undefined | string>

View file

@ -589,5 +589,57 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
});
});
});
describe('#openPointInTimeForType', () => {
test(`throws error if options.namespace is specified`, async () => {
const { client } = createSpacesSavedObjectsClient();
await expect(client.openPointInTimeForType('foo', { namespace: 'bar' })).rejects.toThrow(
ERROR_NAMESPACE_SPECIFIED
);
});
test(`supplements options with the current namespace`, async () => {
const { client, baseClient } = createSpacesSavedObjectsClient();
const expectedReturnValue = { id: 'abc123' };
baseClient.openPointInTimeForType.mockReturnValue(Promise.resolve(expectedReturnValue));
const options = Object.freeze({ foo: 'bar' });
// @ts-expect-error
const actualReturnValue = await client.openPointInTimeForType('foo', options);
expect(actualReturnValue).toBe(expectedReturnValue);
expect(baseClient.openPointInTimeForType).toHaveBeenCalledWith('foo', {
foo: 'bar',
namespace: currentSpace.expectedNamespace,
});
});
});
describe('#closePointInTime', () => {
test(`throws error if options.namespace is specified`, async () => {
const { client } = createSpacesSavedObjectsClient();
await expect(client.closePointInTime('foo', { namespace: 'bar' })).rejects.toThrow(
ERROR_NAMESPACE_SPECIFIED
);
});
test(`supplements options with the current namespace`, async () => {
const { client, baseClient } = createSpacesSavedObjectsClient();
const expectedReturnValue = { succeeded: true, num_freed: 1 };
baseClient.closePointInTime.mockReturnValue(Promise.resolve(expectedReturnValue));
const options = Object.freeze({ foo: 'bar' });
// @ts-expect-error
const actualReturnValue = await client.closePointInTime('foo', options);
expect(actualReturnValue).toBe(expectedReturnValue);
expect(baseClient.closePointInTime).toHaveBeenCalledWith('foo', {
foo: 'bar',
namespace: currentSpace.expectedNamespace,
});
});
});
});
});

View file

@ -15,6 +15,8 @@ import {
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
SavedObjectsClosePointInTimeOptions,
SavedObjectsOpenPointInTimeOptions,
SavedObjectsUpdateOptions,
SavedObjectsAddToNamespacesOptions,
SavedObjectsDeleteFromNamespacesOptions,
@ -378,4 +380,42 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
namespace: spaceIdToNamespace(this.spaceId),
});
}
/**
* Opens a Point In Time (PIT) against the indices for the specified Saved Object types.
* The returned `id` can then be passed to `SavedObjects.find` to search against that PIT.
*
* @param {string|Array<string>} type
* @param {object} [options] - {@link SavedObjectsOpenPointInTimeOptions}
* @property {string} [options.keepAlive]
* @property {string} [options.preference]
* @returns {promise} - { id: string }
*/
async openPointInTimeForType(
type: string | string[],
options: SavedObjectsOpenPointInTimeOptions = {}
) {
throwErrorIfNamespaceSpecified(options);
return await this.client.openPointInTimeForType(type, {
...options,
namespace: spaceIdToNamespace(this.spaceId),
});
}
/**
* Closes a Point In Time (PIT) by ID. This simply proxies the request to ES
* via the Elasticsearch client, and is included in the Saved Objects Client
* as a convenience for consumers who are using `openPointInTimeForType`.
*
* @param {string} id - ID returned from `openPointInTimeForType`
* @param {object} [options] - {@link SavedObjectsClosePointInTimeOptions}
* @returns {promise} - { succeeded: boolean; num_freed: number }
*/
async closePointInTime(id: string, options: SavedObjectsClosePointInTimeOptions = {}) {
throwErrorIfNamespaceSpecified(options);
return await this.client.closePointInTime(id, {
...options,
namespace: spaceIdToNamespace(this.spaceId),
});
}
}