diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md
index 8bd87c2f6ea3..69cfb818561e 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md
@@ -23,9 +23,11 @@ export interface SavedObjectsFindOptions
| [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | string[]
| |
| [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number
| |
| [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | number
| |
+| [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams
| Search against a specific Point In Time (PIT) that you've opened with . |
| [preference](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | string
| An optional ES preference value to be used for the query \* |
| [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string[]
| 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. |
| [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string
| Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query
argument for more information |
+| [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | unknown[]
| Use the sort values from the previous page to retrieve the next page of results. |
| [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string[]
| The fields to perform the parsed query against. See Elasticsearch Simple Query String fields
argument for more information |
| [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string
| |
| [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | string
| |
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.pit.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.pit.md
new file mode 100644
index 000000000000..2284a4d8d210
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.pit.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md)
+
+## SavedObjectsFindOptions.pit property
+
+Search against a specific Point In Time (PIT) that you've opened with .
+
+Signature:
+
+```typescript
+pit?: SavedObjectsPitParams;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md
new file mode 100644
index 000000000000..99ca2c34e77b
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [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.
+
+Signature:
+
+```typescript
+searchAfter?: unknown[];
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index 5fe5eda7a817..1791335d58fe 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -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. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md
new file mode 100644
index 000000000000..dc765260a08c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [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).
+
+Signature:
+
+```typescript
+closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| id | string
| |
+| options | SavedObjectsClosePointInTimeOptions
| |
+
+Returns:
+
+`Promise`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md
index da1f4d029ea2..887f7f7d93a8 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md
@@ -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 id
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 |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md
new file mode 100644
index 000000000000..56c1d6d1ddc3
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [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.
+
+Signature:
+
+```typescript
+openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| type | string | string[]
| |
+| options | SavedObjectsOpenPointInTimeOptions
| |
+
+Returns:
+
+`Promise`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md
new file mode 100644
index 000000000000..27432a8805b0
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md
@@ -0,0 +1,12 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md)
+
+## SavedObjectsClosePointInTimeOptions type
+
+
+Signature:
+
+```typescript
+export declare type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md
new file mode 100644
index 000000000000..43ecd1298d5d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md)
+
+## SavedObjectsClosePointInTimeResponse interface
+
+
+Signature:
+
+```typescript
+export interface SavedObjectsClosePointInTimeResponse
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md) | number
| The number of search contexts that have been successfully closed. |
+| [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md) | boolean
| If true, all search contexts associated with the PIT id are successfully closed. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md
new file mode 100644
index 000000000000..b64932fcee8f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) > [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md)
+
+## SavedObjectsClosePointInTimeResponse.num\_freed property
+
+The number of search contexts that have been successfully closed.
+
+Signature:
+
+```typescript
+num_freed: number;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md
new file mode 100644
index 000000000000..225a549a4cf5
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) > [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md)
+
+## SavedObjectsClosePointInTimeResponse.succeeded property
+
+If true, all search contexts associated with the PIT id are successfully closed.
+
+Signature:
+
+```typescript
+succeeded: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md
index 5e959bbee7be..3f3d708c590e 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md
@@ -9,10 +9,11 @@ Constructs a new instance of the `SavedObjectsExporter` class
Signature:
```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, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
}
| |
+| { savedObjectsClient, typeRegistry, exportSizeLimit, logger, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
logger: Logger;
}
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md
index 727108b824c8..ce23e91633b0 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md
@@ -15,7 +15,7 @@ export declare class SavedObjectsExporter
| Constructor | Modifiers | Description |
| --- | --- | --- |
-| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter
class |
+| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter
class |
## Properties
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md
index d393d579dbdd..6f7c05ea469b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md
@@ -23,9 +23,11 @@ export interface SavedObjectsFindOptions
| [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | string[]
| |
| [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number
| |
| [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | number
| |
+| [pit](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams
| 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) | string
| An optional ES preference value to be used for the query \* |
| [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string[]
| 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. |
| [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string
| Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query
argument for more information |
+| [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | unknown[]
| Use the sort values from the previous page to retrieve the next page of results. |
| [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string[]
| The fields to perform the parsed query against. See Elasticsearch Simple Query String fields
argument for more information |
| [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string
| |
| [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | string
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.pit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.pit.md
new file mode 100644
index 000000000000..fac333227088
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.pit.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [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).
+
+Signature:
+
+```typescript
+pit?: SavedObjectsPitParams;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md
new file mode 100644
index 000000000000..636437094897
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [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.
+
+Signature:
+
+```typescript
+searchAfter?: unknown[];
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md
index 4ed069d1598f..fd56e8ce40e2 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md
@@ -20,6 +20,7 @@ export interface SavedObjectsFindResponse
| --- | --- | --- |
| [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number
| |
| [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number
| |
+| [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md) | string
| |
| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>>
| |
| [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md
new file mode 100644
index 000000000000..dc4f9b509d60
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) > [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md)
+
+## SavedObjectsFindResponse.pit\_id property
+
+Signature:
+
+```typescript
+pit_id?: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md
index e455074a7d11..0f8e9c59236b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md
@@ -16,4 +16,5 @@ export interface SavedObjectsFindResult extends SavedObject
| Property | Type | Description |
| --- | --- | --- |
| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number
| The Elasticsearch _score
of this result. |
+| [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | unknown[]
| The Elasticsearch sort
value of this result. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md
new file mode 100644
index 000000000000..3cc02c404c8d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md
@@ -0,0 +1,41 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) > [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md)
+
+## SavedObjectsFindResult.sort property
+
+The Elasticsearch `sort` value of this result.
+
+Signature:
+
+```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);
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md
new file mode 100644
index 000000000000..57752318cb96
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) > [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`.
+
+Signature:
+
+```typescript
+keepAlive?: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md
new file mode 100644
index 000000000000..46516be2329e
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md)
+
+## SavedObjectsOpenPointInTimeOptions interface
+
+
+Signature:
+
+```typescript
+export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [keepAlive](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md) | string
| Optionally specify how long ES should keep the PIT alive until the next request. Defaults to 5m
. |
+| [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md) | string
| An optional ES preference value to be used for the query. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md
new file mode 100644
index 000000000000..7a9f3a49e866
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) > [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md)
+
+## SavedObjectsOpenPointInTimeOptions.preference property
+
+An optional ES preference value to be used for the query.
+
+Signature:
+
+```typescript
+preference?: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md
new file mode 100644
index 000000000000..66387e5b3b89
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md) > [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md)
+
+## SavedObjectsOpenPointInTimeResponse.id property
+
+PIT ID returned from ES.
+
+Signature:
+
+```typescript
+id: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md
new file mode 100644
index 000000000000..c4be2692763a
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md)
+
+## SavedObjectsOpenPointInTimeResponse interface
+
+
+Signature:
+
+```typescript
+export interface SavedObjectsOpenPointInTimeResponse
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md) | string
| PIT ID returned from ES. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.id.md
new file mode 100644
index 000000000000..cb4d4a65727d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.id.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) > [id](./kibana-plugin-core-server.savedobjectspitparams.id.md)
+
+## SavedObjectsPitParams.id property
+
+Signature:
+
+```typescript
+id: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.keepalive.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.keepalive.md
new file mode 100644
index 000000000000..1011a908f210
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.keepalive.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) > [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md)
+
+## SavedObjectsPitParams.keepAlive property
+
+Signature:
+
+```typescript
+keepAlive?: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md
new file mode 100644
index 000000000000..7bffca7cda28
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md)
+
+## SavedObjectsPitParams interface
+
+
+Signature:
+
+```typescript
+export interface SavedObjectsPitParams
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [id](./kibana-plugin-core-server.savedobjectspitparams.id.md) | string
| |
+| [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md) | string
| |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md
new file mode 100644
index 000000000000..8f9dca35fa36
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md
@@ -0,0 +1,58 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [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`.
+
+Signature:
+
+```typescript
+closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| id | string
| |
+| options | SavedObjectsClosePointInTimeOptions
| |
+
+Returns:
+
+`Promise`
+
+{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);
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md
index 4d13fea12572..632d9c279cb8 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md
@@ -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 openPointInTimeForType
. |
| [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 id
can then be passed to SavedObjects.find
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 |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md
new file mode 100644
index 000000000000..63956ebee68f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md
@@ -0,0 +1,57 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [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.
+
+Signature:
+
+```typescript
+openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| type | string | string[]
| |
+| { keepAlive, preference } | SavedObjectsOpenPointInTimeOptions
| |
+
+Returns:
+
+`Promise`
+
+{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);
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md
index 1629e7742552..599c4e3ad631 100644
--- a/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md
+++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md
@@ -22,7 +22,7 @@ hits: {
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
- sort?: string[];
+ sort?: unknown[];
}>;
};
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md
index b53cbf0d87f2..cbaab4632014 100644
--- a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md
+++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md
@@ -18,7 +18,8 @@ export interface SearchResponse
| [\_scroll\_id](./kibana-plugin-core-server.searchresponse._scroll_id.md) | string
| |
| [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) | ShardsResponse
| |
| [aggregations](./kibana-plugin-core-server.searchresponse.aggregations.md) | any
| |
-| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | {
total: number;
max_score: number;
hits: Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: T;
_version?: number;
_explanation?: Explanation;
fields?: any;
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: string[];
}>;
}
| |
+| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | {
total: number;
max_score: number;
hits: Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: T;
_version?: number;
_explanation?: Explanation;
fields?: any;
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: unknown[];
}>;
}
| |
+| [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md) | string
| |
| [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) | boolean
| |
| [took](./kibana-plugin-core-server.searchresponse.took.md) | number
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.pit_id.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.pit_id.md
new file mode 100644
index 000000000000..f214bc053804
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.pit_id.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md)
+
+## SearchResponse.pit\_id property
+
+Signature:
+
+```typescript
+pit_id?: string;
+```
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index 12a87b1422c5..b9fc0c9c4ac4 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -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.
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index b4a2c40f3003..0cf3e476d330 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -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;
diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts
index 9c0a44b2d3da..44466025de7e 100644
--- a/src/core/public/saved_objects/saved_objects_client.ts
+++ b/src/core/public/saved_objects/saved_objects_client.ts
@@ -21,12 +21,14 @@ import {
import { SimpleSavedObject } from './simple_saved_object';
import { HttpFetchOptions, HttpSetup } from '../http';
+type PromiseType> = T extends Promise ? U : never;
+
type SavedObjectsFindOptions = Omit<
SavedObjectFindOptionsServer,
- 'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap'
+ 'pit' | 'rootSearchFields' | 'searchAfter' | 'sortOrder' | 'typeToNamespacesMap'
>;
-type PromiseType> = T extends Promise ? U : never;
+type SavedObjectsFindResponse = Omit>, 'pit_id'>;
/** @public */
export interface SavedObjectsCreateOptions {
@@ -345,10 +347,7 @@ export class SavedObjectsClient {
query,
});
return request.then((resp) => {
- return renameKeys<
- PromiseType>,
- SavedObjectsFindResponsePublic
- >(
+ return renameKeys(
{
saved_objects: 'savedObjects',
total: 'total',
diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts
index 2e99398efdfb..f5a6fa1f0b1f 100644
--- a/src/core/server/elasticsearch/client/types.ts
+++ b/src/core/server/elasticsearch/client/types.ts
@@ -96,10 +96,11 @@ export interface SearchResponse {
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
- sort?: string[];
+ sort?: unknown[];
}>;
};
aggregations?: any;
+ pit_id?: string;
}
/**
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 6f478004c204..dac2d210eb39 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -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';
diff --git a/src/core/server/saved_objects/export/point_in_time_finder.test.ts b/src/core/server/saved_objects/export/point_in_time_finder.test.ts
new file mode 100644
index 000000000000..cd79c7a4b81e
--- /dev/null
+++ b/src/core/server/saved_objects/export/point_in_time_finder.test.ts
@@ -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;
+
+ 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);
+ });
+ });
+});
diff --git a/src/core/server/saved_objects/export/point_in_time_finder.ts b/src/core/server/saved_objects/export/point_in_time_finder.ts
new file mode 100644
index 000000000000..dc0bac6b6bfd
--- /dev/null
+++ b/src/core/server/saved_objects/export/point_in_time_finder.ts
@@ -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;
+ }
+}
diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts
index c16623f785b0..cf60ada5ba90 100644
--- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts
+++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts
@@ -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 {
+ let logger: MockedLogger;
let savedObjectsClient: ReturnType;
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;
+ 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,
diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts
index 295a3d7a119d..c1c0ea73f0bd 100644
--- a/src/core/server/saved_objects/export/saved_objects_exporter.ts
+++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts
@@ -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;
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>;
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)
diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts
index 4ad0a34acc2e..fce7f1238445 100644
--- a/src/core/server/saved_objects/saved_objects_service.ts
+++ b/src/core/server/saved_objects/saved_objects_service.ts
@@ -459,6 +459,7 @@ export class SavedObjectsService
savedObjectsClient,
typeRegistry: this.typeRegistry,
exportSizeLimit: this.config!.maxImportExportSize,
+ logger: this.logger.get('exporter'),
}),
createImporter: (savedObjectsClient) =>
new SavedObjectsImporter({
diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts
index c853e208f27a..a3610b1e437e 100644
--- a/src/core/server/saved_objects/service/lib/repository.mock.ts
+++ b/src/core/server/saved_objects/service/lib/repository.mock.ts
@@ -17,6 +17,8 @@ const create = (): jest.Mocked => ({
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(),
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 0a1c18c01ad8..c31c0350fe04 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -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);
+ });
+ });
+ });
});
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index a662a374b063..a368060024d3 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -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} [options.searchAfter]
* @property {string} [options.sortField]
* @property {string} [options.sortOrder]
* @property {Array} [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;
}
@@ -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} 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 {
+ 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(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 {
+ const { body } = await this.client.closePointInTime({
+ body: { id },
+ });
+ return body;
+ }
+
/**
* Returns index specified by the given type or the default index
*
diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.ts b/src/core/server/saved_objects/service/lib/repository_es_client.ts
index dae72819726a..6a601b1ed0c8 100644
--- a/src/core/server/saved_objects/service/lib/repository_es_client.ts
+++ b/src/core/server/saved_objects/service/lib/repository_es_client.ts
@@ -14,11 +14,13 @@ import { decorateEsError } from './decorate_es_error';
const methods = [
'bulk',
+ 'closePointInTime',
'create',
'delete',
'get',
'index',
'mget',
+ 'openPointInTime',
'search',
'update',
'updateByQuery',
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/pit_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.test.ts
new file mode 100644
index 000000000000..5a99168792e8
--- /dev/null
+++ b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.test.ts
@@ -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',
+ },
+ });
+ });
+});
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/pit_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.ts
new file mode 100644
index 000000000000..1a8dcb5cca2e
--- /dev/null
+++ b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.ts
@@ -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 } : {}),
+ },
+ };
+}
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
index 9e91e585f74f..fc26c837d5e5 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
@@ -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'],
+ });
+ });
});
});
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
index 4b4fa8865ee9..cae5e43897bc 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
@@ -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;
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 } : {}),
};
}
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts
index 1376f0d50a9d..73c7065705fc 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts
@@ -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', () => {
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts
index e3bfba6a80f5..abef9bfa0a30 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts
@@ -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] : []),
],
};
}
diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts
index 72f5561aa702..ecca652cace3 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts
@@ -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(),
diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js
index 45b0cf70b0dc..7cbddaf195dc 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.test.js
+++ b/src/core/server/saved_objects/service/saved_objects_client.test.js
@@ -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 = {
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index b90540fbfa97..b93f3022e423 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -129,6 +129,35 @@ export interface SavedObjectsFindResult extends SavedObject {
* 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 {
total: number;
per_page: number;
page: number;
+ pit_id?: string;
}
/**
@@ -311,6 +341,50 @@ export interface SavedObjectsResolveResponse {
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);
+ }
}
diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts
index d122e92aba39..66110d096213 100644
--- a/src/core/server/saved_objects/types.ts
+++ b/src/core/server/saved_objects/types.ts
@@ -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;
/** 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;
}
/**
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 459e4d42eaf1..71ce1ccba4f6 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2223,6 +2223,7 @@ export class SavedObjectsClient {
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
+ closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise;
@@ -2232,6 +2233,7 @@ export class SavedObjectsClient {
errors: typeof SavedObjectsErrorHelpers;
find(options: SavedObjectsFindOptions): Promise>;
get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
+ openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise;
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
@@ -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;
exportByTypes(options: SavedObjectsExportByTypeOptions): Promise;
@@ -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 {
// (undocumented)
per_page: number;
// (undocumented)
+ pit_id?: string;
+ // (undocumented)
saved_objects: Array>;
// (undocumented)
total: number;
@@ -2511,6 +2527,7 @@ export interface SavedObjectsFindResponse {
// @public (undocumented)
export interface SavedObjectsFindResult extends SavedObject {
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(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
+ closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
// 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(options: SavedObjectsFindOptions): Promise>;
get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>;
+ openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise;
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
@@ -2949,10 +2987,12 @@ export interface SearchResponse {
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
- sort?: string[];
+ sort?: unknown[];
}>;
};
// (undocumented)
+ pit_id?: string;
+ // (undocumented)
_scroll_id?: string;
// (undocumented)
_shards: ShardsResponse;
diff --git a/src/core/server/types.ts b/src/core/server/types.ts
index 1839ee68190a..2ae51d4452a4 100644
--- a/src/core/server/types.ts
+++ b/src/core/server/types.ts
@@ -31,6 +31,7 @@ export type {
SavedObjectStatusMeta,
SavedObjectsFindOptionsReference,
SavedObjectsFindOptions,
+ SavedObjectsPitParams,
SavedObjectsBaseOptions,
MutatingOperationRefreshSetting,
SavedObjectsClientContract,
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index d579663eeca4..7fa1d727259f 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -1142,7 +1142,7 @@ export class Plugin implements Plugin_2 Promise;
};
indexPatterns: {
- indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise;
+ indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise;
};
search: ISearchStart>;
};
diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts
index 32a72f374cbe..2be86bda774f 100644
--- a/test/api_integration/apis/saved_objects/export.ts
+++ b/test/api_integration/apis/saved_objects/export.ts
@@ -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);
});
});
});
diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts
index b0aa9b0eef8f..d463b9498a52 100644
--- a/test/api_integration/apis/saved_objects/import.ts
+++ b/test/api_integration/apis/saved_objects/import.ts
@@ -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",
});
});
});
diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts
index 4fcce29905be..8412d3cce5c3 100644
--- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts
+++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts
@@ -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",
});
});
});
diff --git a/test/api_integration/config.js b/test/api_integration/config.js
index bd8f10606a45..1c19dd24fa96 100644
--- a/test/api_integration/config.js
+++ b/test/api_integration/config.js
@@ -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`,
],
},
};
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
index 3405f196960c..474a283b5e3c 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
@@ -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);
+ });
+});
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
index 73414e855919..a602f3606e0a 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
@@ -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.
diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts
index 25bcfd683b0d..f353362e3351 100644
--- a/x-pack/plugins/security/server/audit/audit_events.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.ts
@@ -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 = {
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 = {
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,
};
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts
index 571d588037f3..3a0d9f4a5a10 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts
@@ -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',
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
index 6c2a57e57dcd..da2639aba1c6 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
@@ -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'),
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
index aeddba051a18..1293d3f2c84a 100644
--- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
@@ -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`;
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
index 4a886e5addb4..73bee302363a 100644
--- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
@@ -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
diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
index 8a749b500933..f5917e78135e 100644
--- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
+++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
@@ -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,
+ });
+ });
+ });
});
});
diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
index 9316d86b19bd..433f95d2b5cf 100644
--- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
+++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
@@ -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} 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),
+ });
+ }
}