Sharing saved objects phase 3 (#94383)

This commit is contained in:
Joe Portner 2021-05-14 14:46:17 -04:00 committed by GitHub
parent 97cc6ddb6b
commit b2d36b863b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 6575 additions and 3271 deletions

View file

@ -1867,7 +1867,7 @@
"section": "def-server.SavedObjectsRepository",
"text": "SavedObjectsRepository"
},
", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">"
", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"collectMultiNamespaceReferences\" | \"updateObjectsSpaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">"
],
"source": {
"path": "x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts",

View file

@ -103,12 +103,14 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectAttributes](./kibana-plugin-core-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
| [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) | |
| [SavedObjectReference](./kibana-plugin-core-public.savedobjectreference.md) | A reference to another saved object. |
| [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. |
| [SavedObjectsBaseOptions](./kibana-plugin-core-public.savedobjectsbaseoptions.md) | |
| [SavedObjectsBatchResponse](./kibana-plugin-core-public.savedobjectsbatchresponse.md) | |
| [SavedObjectsBulkCreateObject](./kibana-plugin-core-public.savedobjectsbulkcreateobject.md) | |
| [SavedObjectsBulkCreateOptions](./kibana-plugin-core-public.savedobjectsbulkcreateoptions.md) | |
| [SavedObjectsBulkUpdateObject](./kibana-plugin-core-public.savedobjectsbulkupdateobject.md) | |
| [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.md) | |
| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. |
| [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) | |
| [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | |
| [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) &gt; [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md)
## SavedObjectReferenceWithContext.id property
The ID of the referenced object
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) &gt; [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md)
## SavedObjectReferenceWithContext.inboundReferences property
References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation
<b>Signature:</b>
```typescript
inboundReferences: Array<{
type: string;
id: string;
name: string;
}>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) &gt; [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md)
## SavedObjectReferenceWithContext.isMissing property
Whether or not this object or reference is missing
<b>Signature:</b>
```typescript
isMissing?: boolean;
```

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md)
## SavedObjectReferenceWithContext interface
A returned input object or one of its references, with additional context.
<b>Signature:</b>
```typescript
export interface SavedObjectReferenceWithContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | <code>string</code> | The ID of the referenced object |
| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | <code>Array&lt;{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> name: string;</code><br/><code> }&gt;</code> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation |
| [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | <code>boolean</code> | Whether or not this object or reference is missing |
| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | <code>string[]</code> | The space(s) that the referenced object exists in |
| [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | <code>string[]</code> | The space(s) that legacy URL aliases matching this type/id exist in |
| [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) | <code>string</code> | The type of the referenced object |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) &gt; [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md)
## SavedObjectReferenceWithContext.spaces property
The space(s) that the referenced object exists in
<b>Signature:</b>
```typescript
spaces: string[];
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) &gt; [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md)
## SavedObjectReferenceWithContext.spacesWithMatchingAliases property
The space(s) that legacy URL aliases matching this type/id exist in
<b>Signature:</b>
```typescript
spacesWithMatchingAliases?: string[];
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) &gt; [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md)
## SavedObjectReferenceWithContext.type property
The type of the referenced object
<b>Signature:</b>
```typescript
type: string;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md)
## SavedObjectsCollectMultiNamespaceReferencesResponse interface
The response when object references are collected.
<b>Signature:</b>
```typescript
export interface SavedObjectsCollectMultiNamespaceReferencesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) | <code>SavedObjectReferenceWithContext[]</code> | |

View file

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

View file

@ -9,5 +9,5 @@ An async generator which wraps calls to `savedObjectsClient.find` and iterates o
<b>Signature:</b>
```typescript
find: () => AsyncGenerator<SavedObjectsFindResponse>;
find: () => AsyncGenerator<SavedObjectsFindResponse<T, A>>;
```

View file

@ -8,7 +8,7 @@
<b>Signature:</b>
```typescript
export interface ISavedObjectsPointInTimeFinder
export interface ISavedObjectsPointInTimeFinder<T, A>
```
## Properties
@ -16,5 +16,5 @@ export interface ISavedObjectsPointInTimeFinder
| Property | Type | Description |
| --- | --- | --- |
| [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | <code>() =&gt; Promise&lt;void&gt;</code> | Closes the Point-In-Time associated with this finder instance.<!-- -->Once you have retrieved all of the results you need, it is recommended to call <code>close()</code> to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to <code>find</code> fails for any reason. |
| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | <code>() =&gt; AsyncGenerator&lt;SavedObjectsFindResponse&gt;</code> | An async generator which wraps calls to <code>savedObjectsClient.find</code> and iterates over multiple pages of results using <code>_pit</code> and <code>search_after</code>. 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 <code>perPage</code> size. |
| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | <code>() =&gt; AsyncGenerator&lt;SavedObjectsFindResponse&lt;T, A&gt;&gt;</code> | An async generator which wraps calls to <code>savedObjectsClient.find</code> and iterates over multiple pages of results using <code>_pit</code> and <code>search_after</code>. 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 <code>perPage</code> size. |

View file

@ -144,8 +144,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) |
| [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions, and they cannot exceed the current Kibana version.<!-- -->For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. |
| [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. |
| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | |
| [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) | |
| [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. |
| [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | |
| [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | |
| [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | |
@ -158,13 +157,14 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [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) | |
| [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) | An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the <code>namespaceType: 'multi'</code> or <code>namespaceType: 'multi-isolated'</code> option).<!-- -->Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the <code>namespaceType: 'multi'</code>). |
| [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) | Options for collecting references. |
| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. |
| [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) | |
| [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) | |
| [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | |
| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | |
| [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | |
| [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | |
| [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) |
| [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) |
@ -208,6 +208,10 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) | |
| [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-core-server.savedobjectstype.md)<!-- -->'s management section. |
| [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. |
| [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) | An object that should have its spaces updated. |
| [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) | Options for the update operation. |
| [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) | The response when objects' spaces are updated. |
| [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) | Details about a specific object's update result. |
| [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | |
| [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | |
| [SearchResponse](./kibana-plugin-core-server.searchresponse.md) | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) &gt; [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md)
## SavedObjectExportBaseOptions.includeNamespaces property
Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports.
<b>Signature:</b>
```typescript
includeNamespaces?: boolean;
```

View file

@ -16,6 +16,7 @@ export interface SavedObjectExportBaseOptions
| Property | Type | Description |
| --- | --- | --- |
| [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | <code>boolean</code> | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. |
| [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) | <code>boolean</code> | Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. |
| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | <code>boolean</code> | flag to also include all related saved objects in the export stream. |
| [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | <code>string</code> | optional namespace to override the namespace used by the savedObjectsClient. |
| [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | <code>KibanaRequest</code> | The http request initiating the export. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) &gt; [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md)
## SavedObjectReferenceWithContext.id property
The ID of the referenced object
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) &gt; [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md)
## SavedObjectReferenceWithContext.inboundReferences property
References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation
<b>Signature:</b>
```typescript
inboundReferences: Array<{
type: string;
id: string;
name: string;
}>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) &gt; [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md)
## SavedObjectReferenceWithContext.isMissing property
Whether or not this object or reference is missing
<b>Signature:</b>
```typescript
isMissing?: boolean;
```

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md)
## SavedObjectReferenceWithContext interface
A returned input object or one of its references, with additional context.
<b>Signature:</b>
```typescript
export interface SavedObjectReferenceWithContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | <code>string</code> | The ID of the referenced object |
| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | <code>Array&lt;{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> name: string;</code><br/><code> }&gt;</code> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation |
| [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | <code>boolean</code> | Whether or not this object or reference is missing |
| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | <code>string[]</code> | The space(s) that the referenced object exists in |
| [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | <code>string[]</code> | The space(s) that legacy URL aliases matching this type/id exist in |
| [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) | <code>string</code> | The type of the referenced object |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) &gt; [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md)
## SavedObjectReferenceWithContext.spaces property
The space(s) that the referenced object exists in
<b>Signature:</b>
```typescript
spaces: string[];
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) &gt; [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md)
## SavedObjectReferenceWithContext.spacesWithMatchingAliases property
The space(s) that legacy URL aliases matching this type/id exist in
<b>Signature:</b>
```typescript
spacesWithMatchingAliases?: string[];
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) &gt; [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md)
## SavedObjectReferenceWithContext.type property
The type of the referenced object
<b>Signature:</b>
```typescript
type: string;
```

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md)
## SavedObjectsAddToNamespacesOptions interface
<b>Signature:</b>
```typescript
export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | <code>MutatingOperationRefreshSetting</code> | The Elasticsearch Refresh setting for this operation |
| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | <code>string</code> | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. |

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) &gt; [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md)
## SavedObjectsAddToNamespacesOptions.refresh property
The Elasticsearch Refresh setting for this operation
<b>Signature:</b>
```typescript
refresh?: MutatingOperationRefreshSetting;
```

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) &gt; [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md)
## SavedObjectsAddToNamespacesOptions.version property
An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control.
<b>Signature:</b>
```typescript
version?: string;
```

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md)
## SavedObjectsAddToNamespacesResponse interface
<b>Signature:</b>
```typescript
export interface SavedObjectsAddToNamespacesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) | <code>string[]</code> | The namespaces the object exists in after this operation is complete. |

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) &gt; [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md)
## SavedObjectsAddToNamespacesResponse.namespaces property
The namespaces the object exists in after this operation is complete.
<b>Signature:</b>
```typescript
namespaces: string[];
```

View file

@ -1,27 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md)
## SavedObjectsClient.addToNamespaces() method
Adds namespaces to a SavedObject
<b>Signature:</b>
```typescript
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsAddToNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsAddToNamespacesResponse>`

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md)
## SavedObjectsClient.collectMultiNamespaceReferences() method
Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type.
<b>Signature:</b>
```typescript
collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise<SavedObjectsCollectMultiNamespaceReferencesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| objects | <code>SavedObjectsCollectMultiNamespaceReferencesObject[]</code> | |
| options | <code>SavedObjectsCollectMultiNamespaceReferencesOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsCollectMultiNamespaceReferencesResponse>`

View file

@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call `
<b>Signature:</b>
```typescript
createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder;
createPointInTimeFinder<T = unknown, A = unknown>(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder<T, A>;
```
## Parameters
@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions,
<b>Returns:</b>
`ISavedObjectsPointInTimeFinder`
`ISavedObjectsPointInTimeFinder<T, A>`
## Example

View file

@ -1,27 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md)
## SavedObjectsClient.deleteFromNamespaces() method
Removes namespaces from a SavedObject
<b>Signature:</b>
```typescript
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsDeleteFromNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsDeleteFromNamespacesResponse>`

View file

@ -25,20 +25,20 @@ The constructor for this class is marked as internal. Third-party code should no
| Method | Modifiers | Description |
| --- | --- | --- |
| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject |
| [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request |
| [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)<!-- -->.<!-- -->Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. |
| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) | | Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. |
| [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject |
| [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any <code>find</code> queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.<!-- -->Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.<!-- -->The generator wraps calls to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) and iterates over multiple pages of results using <code>_pit</code> and <code>search_after</code>. 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 <code>perPage</code>.<!-- -->Once you have retrieved all of the results you need, it is recommended to call <code>close()</code> to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to <code>find</code> fails for any reason. |
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject |
| [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object |
| [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned <code>id</code> can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT.<!-- -->Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. |
| [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 |
| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. |

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md)
## SavedObjectsClient.updateObjectsSpaces() method
Updates one or more objects to add and/or remove them from specified spaces.
<b>Signature:</b>
```typescript
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise<import("./lib").SavedObjectsUpdateObjectsSpacesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| objects | <code>SavedObjectsUpdateObjectsSpacesObject[]</code> | |
| spacesToAdd | <code>string[]</code> | |
| spacesToRemove | <code>string[]</code> | |
| options | <code>SavedObjectsUpdateObjectsSpacesOptions</code> | |
<b>Returns:</b>
`Promise<import("./lib").SavedObjectsUpdateObjectsSpacesResponse>`

View file

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

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md)
## SavedObjectsCollectMultiNamespaceReferencesObject interface
An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the `namespaceType: 'multi'` or `namespaceType: 'multi-isolated'` option).
Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the `namespaceType: 'multi'`<!-- -->).
<b>Signature:</b>
```typescript
export interface SavedObjectsCollectMultiNamespaceReferencesObject
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) | <code>string</code> | |
| [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) | <code>string</code> | |

View file

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

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md)
## SavedObjectsCollectMultiNamespaceReferencesOptions interface
Options for collecting references.
<b>Signature:</b>
```typescript
export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) | <code>'collectMultiNamespaceReferences' &#124; 'updateObjectsSpaces'</code> | Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) &gt; [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md)
## SavedObjectsCollectMultiNamespaceReferencesOptions.purpose property
Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences'
<b>Signature:</b>
```typescript
purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces';
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md)
## SavedObjectsCollectMultiNamespaceReferencesResponse interface
The response when object references are collected.
<b>Signature:</b>
```typescript
export interface SavedObjectsCollectMultiNamespaceReferencesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) | <code>SavedObjectReferenceWithContext[]</code> | |

View file

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

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md)
## SavedObjectsDeleteFromNamespacesOptions interface
<b>Signature:</b>
```typescript
export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | <code>MutatingOperationRefreshSetting</code> | The Elasticsearch Refresh setting for this operation |

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) &gt; [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md)
## SavedObjectsDeleteFromNamespacesOptions.refresh property
The Elasticsearch Refresh setting for this operation
<b>Signature:</b>
```typescript
refresh?: MutatingOperationRefreshSetting;
```

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md)
## SavedObjectsDeleteFromNamespacesResponse interface
<b>Signature:</b>
```typescript
export interface SavedObjectsDeleteFromNamespacesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) | <code>string[]</code> | The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. |

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) &gt; [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md)
## SavedObjectsDeleteFromNamespacesResponse.namespaces property
The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted.
<b>Signature:</b>
```typescript
namespaces: string[];
```

View file

@ -1,27 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md)
## SavedObjectsRepository.addToNamespaces() method
Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`<!-- -->\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to.
<b>Signature:</b>
```typescript
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsAddToNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsAddToNamespacesResponse>`

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md)
## SavedObjectsRepository.collectMultiNamespaceReferences() method
Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type.
<b>Signature:</b>
```typescript
collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise<import("./collect_multi_namespace_references").SavedObjectsCollectMultiNamespaceReferencesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| objects | <code>SavedObjectsCollectMultiNamespaceReferencesObject[]</code> | |
| options | <code>SavedObjectsCollectMultiNamespaceReferencesOptions</code> | |
<b>Returns:</b>
`Promise<import("./collect_multi_namespace_references").SavedObjectsCollectMultiNamespaceReferencesResponse>`

View file

@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call `
<b>Signature:</b>
```typescript
createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder;
createPointInTimeFinder<T = unknown, A = unknown>(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder<T, A>;
```
## Parameters
@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions,
<b>Returns:</b>
`ISavedObjectsPointInTimeFinder`
`ISavedObjectsPointInTimeFinder<T, A>`
## Example

View file

@ -1,27 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md)
## SavedObjectsRepository.deleteFromNamespaces() method
Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[`addToNamespaces`<!-- -->\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to.
<b>Signature:</b>
```typescript
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsDeleteFromNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<SavedObjectsDeleteFromNamespacesResponse>`

View file

@ -15,17 +15,16 @@ export declare class SavedObjectsRepository
| Method | Modifiers | Description |
| --- | --- | --- |
| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[<code>deleteFromNamespaces</code>\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once |
| [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id |
| [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk |
| [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. |
| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using <code>openPointInTimeForType</code>.<!-- -->Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. |
| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) | | Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. |
| [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object |
| [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any <code>find</code> queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.<!-- -->Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.<!-- -->This generator wraps calls to [SavedObjectsRepository.find()](./kibana-plugin-core-server.savedobjectsrepository.find.md) and iterates over multiple pages of results using <code>_pit</code> and <code>search_after</code>. 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 <code>perPage</code>.<!-- -->Once you have retrieved all of the results you need, it is recommended to call <code>close()</code> to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to <code>find</code> fails for any reason. |
| [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. |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[<code>addToNamespaces</code>\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [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. |
@ -33,4 +32,5 @@ export declare class SavedObjectsRepository
| [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 |
| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. |

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md)
## SavedObjectsRepository.updateObjectsSpaces() method
Updates one or more objects to add and/or remove them from specified spaces.
<b>Signature:</b>
```typescript
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise<import("./update_objects_spaces").SavedObjectsUpdateObjectsSpacesResponse>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| objects | <code>SavedObjectsUpdateObjectsSpacesObject[]</code> | |
| spacesToAdd | <code>string[]</code> | |
| spacesToRemove | <code>string[]</code> | |
| options | <code>SavedObjectsUpdateObjectsSpacesOptions</code> | |
<b>Returns:</b>
`Promise<import("./update_objects_spaces").SavedObjectsUpdateObjectsSpacesResponse>`

View file

@ -9,7 +9,7 @@ Converts a document from the format that is stored in elasticsearch to the saved
<b>Signature:</b>
```typescript
rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc;
rawToSavedObject<T = unknown>(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc<T>;
```
## Parameters
@ -21,5 +21,5 @@ rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptio
<b>Returns:</b>
`SavedObjectSanitizedDoc`
`SavedObjectSanitizedDoc<T>`

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) &gt; [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md)
## SavedObjectsUpdateObjectsSpacesObject.id property
The type of the object to update
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md)
## SavedObjectsUpdateObjectsSpacesObject interface
An object that should have its spaces updated.
<b>Signature:</b>
```typescript
export interface SavedObjectsUpdateObjectsSpacesObject
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) | <code>string</code> | The type of the object to update |
| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) | <code>string</code> | The ID of the object to update |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) &gt; [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md)
## SavedObjectsUpdateObjectsSpacesObject.type property
The ID of the object to update
<b>Signature:</b>
```typescript
type: string;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md)
## SavedObjectsUpdateObjectsSpacesOptions interface
Options for the update operation.
<b>Signature:</b>
```typescript
export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) | <code>MutatingOperationRefreshSetting</code> | The Elasticsearch Refresh setting for this operation |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) &gt; [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md)
## SavedObjectsUpdateObjectsSpacesOptions.refresh property
The Elasticsearch Refresh setting for this operation
<b>Signature:</b>
```typescript
refresh?: MutatingOperationRefreshSetting;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md)
## SavedObjectsUpdateObjectsSpacesResponse interface
The response when objects' spaces are updated.
<b>Signature:</b>
```typescript
export interface SavedObjectsUpdateObjectsSpacesResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) | <code>SavedObjectsUpdateObjectsSpacesResponseObject[]</code> | |

View file

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

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) &gt; [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md)
## SavedObjectsUpdateObjectsSpacesResponseObject.error property
Included if there was an error updating this object's spaces
<b>Signature:</b>
```typescript
error?: SavedObjectError;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) &gt; [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md)
## SavedObjectsUpdateObjectsSpacesResponseObject.id property
The ID of the referenced object
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md)
## SavedObjectsUpdateObjectsSpacesResponseObject interface
Details about a specific object's update result.
<b>Signature:</b>
```typescript
export interface SavedObjectsUpdateObjectsSpacesResponseObject
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) | <code>SavedObjectError</code> | Included if there was an error updating this object's spaces |
| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) | <code>string</code> | The ID of the referenced object |
| [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) | <code>string[]</code> | The space(s) that the referenced object exists in |
| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) | <code>string</code> | The type of the referenced object |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) &gt; [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md)
## SavedObjectsUpdateObjectsSpacesResponseObject.spaces property
The space(s) that the referenced object exists in
<b>Signature:</b>
```typescript
spaces: string[];
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) &gt; [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md)
## SavedObjectsUpdateObjectsSpacesResponseObject.type property
The type of the referenced object
<b>Signature:</b>
```typescript
type: string;
```

View file

@ -8,7 +8,7 @@
```typescript
start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: ElasticsearchClient) => Promise<IndexPatternsCommonService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "collectMultiNamespaceReferences" | "updateObjectsSpaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: ElasticsearchClient) => Promise<IndexPatternsCommonService>;
};
```
@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps):
<b>Returns:</b>
`{
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: ElasticsearchClient) => Promise<IndexPatternsCommonService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "collectMultiNamespaceReferences" | "updateObjectsSpaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: ElasticsearchClient) => Promise<IndexPatternsCommonService>;
}`

View file

@ -12,7 +12,7 @@ start(core: CoreStart): {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "collectMultiNamespaceReferences" | "updateObjectsSpaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
};
@ -31,7 +31,7 @@ start(core: CoreStart): {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "collectMultiNamespaceReferences" | "updateObjectsSpaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
}`

View file

@ -144,6 +144,8 @@ export type {
SavedObjectsImportSimpleWarning,
SavedObjectsImportActionRequiredWarning,
SavedObjectsImportWarning,
SavedObjectReferenceWithContext,
SavedObjectsCollectMultiNamespaceReferencesResponse,
} from './saved_objects';
export { HttpFetchError } from './http';

View file

@ -1173,6 +1173,20 @@ export interface SavedObjectReference {
type: string;
}
// @public
export interface SavedObjectReferenceWithContext {
id: string;
inboundReferences: Array<{
type: string;
id: string;
name: string;
}>;
isMissing?: boolean;
spaces: string[];
spacesWithMatchingAliases?: string[];
type: string;
}
// @public (undocumented)
export interface SavedObjectsBaseOptions {
namespace?: string;
@ -1240,6 +1254,12 @@ export class SavedObjectsClient {
// @public
export type SavedObjectsClientContract = PublicMethodsOf<SavedObjectsClient>;
// @public
export interface SavedObjectsCollectMultiNamespaceReferencesResponse {
// (undocumented)
objects: SavedObjectReferenceWithContext[];
}
// @public (undocumented)
export interface SavedObjectsCreateOptions {
coreMigrationVersion?: string;

View file

@ -39,6 +39,8 @@ export type {
SavedObjectsImportSimpleWarning,
SavedObjectsImportActionRequiredWarning,
SavedObjectsImportWarning,
SavedObjectReferenceWithContext,
SavedObjectsCollectMultiNamespaceReferencesResponse,
} from '../../server/types';
export type {

View file

@ -320,12 +320,16 @@ export type {
SavedObjectsResolveResponse,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsDeleteFromNamespacesResponse,
SavedObjectsRemoveReferencesToOptions,
SavedObjectsRemoveReferencesToResponse,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
SavedObjectReferenceWithContext,
SavedObjectsCollectMultiNamespaceReferencesResponse,
SavedObjectsUpdateObjectsSpacesObject,
SavedObjectsUpdateObjectsSpacesOptions,
SavedObjectsUpdateObjectsSpacesResponse,
SavedObjectsUpdateObjectsSpacesResponseObject,
SavedObjectsServiceStart,
SavedObjectsServiceSetup,
SavedObjectStatusMeta,

View file

@ -1149,6 +1149,29 @@ describe('getSortedObjectsForExport()', () => {
]);
});
test('return results including the `namespaces` attribute when includeNamespaces option is used', async () => {
const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] });
const objectResults = [
createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }),
createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }),
createSavedObject({ type: 'other', id: '3' }),
];
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: objectResults,
});
const exportStream = await exporter.exportByObjects({
request,
objects: [
{ type: 'multi', id: '1' },
{ type: 'multi', id: '2' },
{ type: 'other', id: '3' },
],
includeNamespaces: true,
});
const response = await readStreamToCompletion(exportStream);
expect(response).toEqual([...objectResults, expect.objectContaining({ exportedCount: 3 })]);
});
test('includes nested dependencies when passed in', async () => {
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [

View file

@ -77,6 +77,7 @@ export class SavedObjectsExporter {
return this.processObjects(objects, byIdAscComparator, {
request: options.request,
includeReferencesDeep: options.includeReferencesDeep,
includeNamespaces: options.includeNamespaces,
excludeExportDetails: options.excludeExportDetails,
namespace: options.namespace,
});
@ -99,6 +100,7 @@ export class SavedObjectsExporter {
return this.processObjects(objects, comparator, {
request: options.request,
includeReferencesDeep: options.includeReferencesDeep,
includeNamespaces: options.includeNamespaces,
excludeExportDetails: options.excludeExportDetails,
namespace: options.namespace,
});
@ -111,6 +113,7 @@ export class SavedObjectsExporter {
request,
excludeExportDetails = false,
includeReferencesDeep = false,
includeNamespaces = false,
namespace,
}: SavedObjectExportBaseOptions
) {
@ -139,9 +142,9 @@ export class SavedObjectsExporter {
}
// redact attributes that should not be exported
const redactedObjects = exportedObjects.map<SavedObject<unknown>>(
({ namespaces, ...object }) => object
);
const redactedObjects = includeNamespaces
? exportedObjects
: exportedObjects.map<SavedObject<unknown>>(({ namespaces, ...object }) => object);
const exportDetails: SavedObjectsExportResultDetails = {
exportedCount: exportedObjects.length,

View file

@ -15,6 +15,12 @@ export interface SavedObjectExportBaseOptions {
request: KibanaRequest;
/** flag to also include all related saved objects in the export stream. */
includeReferencesDeep?: boolean;
/**
* Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects.
* This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP
* route for exports.
*/
includeNamespaces?: boolean;
/** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */
excludeExportDetails?: boolean;
/** optional namespace to override the namespace used by the savedObjectsClient. */

View file

@ -982,6 +982,7 @@ describe('DocumentMigrator', () => {
id: 'foo-namespace:dog:loud',
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: 'loud',
targetNamespace: 'foo-namespace',
targetType: 'dog',
targetId: 'uuidv5',
@ -1046,6 +1047,7 @@ describe('DocumentMigrator', () => {
id: 'foo-namespace:dog:cute',
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: 'cute',
targetNamespace: 'foo-namespace',
targetType: 'dog',
targetId: 'uuidv5',
@ -1168,6 +1170,7 @@ describe('DocumentMigrator', () => {
id: 'foo-namespace:dog:hungry',
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: 'hungry',
targetNamespace: 'foo-namespace',
targetType: 'dog',
targetId: 'uuidv5',
@ -1240,6 +1243,7 @@ describe('DocumentMigrator', () => {
id: 'foo-namespace:dog:pretty',
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: 'pretty',
targetNamespace: 'foo-namespace',
targetType: 'dog',
targetId: 'uuidv5',

View file

@ -560,6 +560,7 @@ function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) {
id: `${namespace}:${type}:${originId}`,
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: originId,
targetNamespace: namespace,
targetType: type,
targetId: id,

View file

@ -194,6 +194,7 @@ describe('migration v2', () => {
id: 'legacy-url-alias:spacex:foo:1',
type: 'legacy-url-alias',
'legacy-url-alias': {
sourceId: '1',
targetId: newFooId,
targetNamespace: 'spacex',
targetType: 'foo',
@ -226,6 +227,7 @@ describe('migration v2', () => {
id: 'legacy-url-alias:spacex:bar:1',
type: 'legacy-url-alias',
'legacy-url-alias': {
sourceId: '1',
targetId: newBarId,
targetNamespace: 'spacex',
targetType: 'bar',

View file

@ -13,10 +13,15 @@ const legacyUrlAliasType: SavedObjectsType = {
name: LEGACY_URL_ALIAS_TYPE,
namespaceType: 'agnostic',
mappings: {
dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields
properties: {},
dynamic: false,
properties: {
sourceId: { type: 'keyword' },
targetType: { type: 'keyword' },
disabled: { type: 'boolean' },
// other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above)
},
},
hidden: true,
hidden: false,
};
/**

View file

@ -7,13 +7,49 @@
*/
/**
* A legacy URL alias is created for an object when it is converted from a single-namespace type to a multi-namespace type. This enables us
* to preserve functionality of existing URLs for objects whose IDs have been changed during the conversion process, by way of the new
* `SavedObjectsClient.resolve()` API.
*
* Legacy URL aliases are only created by the `DocumentMigrator`, and will always have a saved object ID as follows:
*
* ```
* `${targetNamespace}:${targetType}:${sourceId}`
* ```
*
* This predictable object ID allows aliases to be easily looked up during the resolve operation, and ensures that exactly one alias will
* exist for a given source per space.
*
* @internal
*/
export interface LegacyUrlAlias {
/**
* The original ID of the object, before it was converted.
*/
sourceId: string;
/**
* The namespace that the object existed in when it was converted.
*/
targetNamespace: string;
/**
* The type of the object when it was converted.
*/
targetType: string;
/**
* The new ID of the object when it was converted.
*/
targetId: string;
/**
* The last time this alias was used with `SavedObjectsClient.resolve()`.
*/
lastResolved?: string;
/**
* How many times this alias was used with `SavedObjectsClient.resolve()`.
*/
resolveCounter?: number;
/**
* If true, this alias is disabled and it will be ignored in `SavedObjectsClient.resolve()` and
* `SavedObjectsClient.collectMultiNamespaceReferences()`.
*/
disabled?: boolean;
}

View file

@ -76,10 +76,10 @@ export class SavedObjectsSerializer {
* @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format.
* @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document.
*/
public rawToSavedObject(
public rawToSavedObject<T = unknown>(
doc: SavedObjectsRawDoc,
options: SavedObjectsRawDocParseOptions = {}
): SavedObjectSanitizedDoc {
): SavedObjectSanitizedDoc<T> {
this.checkIsRawSavedObject(doc, options); // throws a descriptive error if the document is not a saved object
const { namespaceTreatment = 'strict' } = options;

View file

@ -17,6 +17,14 @@ export type {
SavedObjectsClientWrapperOptions,
SavedObjectsClientFactory,
SavedObjectsClientFactoryProvider,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
SavedObjectReferenceWithContext,
SavedObjectsCollectMultiNamespaceReferencesResponse,
SavedObjectsUpdateObjectsSpacesObject,
SavedObjectsUpdateObjectsSpacesOptions,
SavedObjectsUpdateObjectsSpacesResponse,
SavedObjectsUpdateObjectsSpacesResponseObject,
} from './lib';
export * from './saved_objects_client';

View file

@ -0,0 +1,21 @@
/*
* 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 type * as InternalUtils from './internal_utils';
export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction<
typeof InternalUtils['rawDocExistsInNamespace']
>;
jest.mock('./internal_utils', () => {
const actual = jest.requireActual('./internal_utils');
return {
...actual,
rawDocExistsInNamespace: mockRawDocExistsInNamespace,
};
});

View file

@ -0,0 +1,444 @@
/*
* 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 { mockRawDocExistsInNamespace } from './collect_multi_namespace_references.test.mock';
import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest';
import type { ElasticsearchClient } from 'src/core/server/elasticsearch';
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
import { SavedObjectsSerializer } from '../../serialization';
import type {
CollectMultiNamespaceReferencesParams,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
} from './collect_multi_namespace_references';
import { collectMultiNamespaceReferences } from './collect_multi_namespace_references';
import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock';
import { savedObjectsRepositoryMock } from './repository.mock';
import { PointInTimeFinder } from './point_in_time_finder';
import { ISavedObjectsRepository } from './repository';
const SPACES = ['default', 'another-space'];
const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 };
const MULTI_NAMESPACE_OBJ_TYPE_1 = 'type-a';
const MULTI_NAMESPACE_OBJ_TYPE_2 = 'type-b';
const NON_MULTI_NAMESPACE_OBJ_TYPE = 'type-c';
const MULTI_NAMESPACE_HIDDEN_OBJ_TYPE = 'type-d';
beforeEach(() => {
mockRawDocExistsInNamespace.mockReset();
mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default
});
describe('collectMultiNamespaceReferences', () => {
let client: DeeplyMockedKeys<ElasticsearchClient>;
let savedObjectsMock: jest.Mocked<ISavedObjectsRepository>;
let createPointInTimeFinder: jest.MockedFunction<
CollectMultiNamespaceReferencesParams['createPointInTimeFinder']
>;
let pointInTimeFinder: DeeplyMockedKeys<PointInTimeFinder>;
/** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `collectMultiNamespaceReferences` */
function setup(
objects: SavedObjectsCollectMultiNamespaceReferencesObject[],
options: SavedObjectsCollectMultiNamespaceReferencesOptions = {}
): CollectMultiNamespaceReferencesParams {
const registry = typeRegistryMock.create();
registry.isMultiNamespace.mockImplementation(
(type) =>
[
MULTI_NAMESPACE_OBJ_TYPE_1,
MULTI_NAMESPACE_OBJ_TYPE_2,
MULTI_NAMESPACE_HIDDEN_OBJ_TYPE,
].includes(type) // NON_MULTI_NAMESPACE_TYPE is omitted
);
registry.isShareable.mockImplementation(
(type) => [MULTI_NAMESPACE_OBJ_TYPE_1, MULTI_NAMESPACE_HIDDEN_OBJ_TYPE].includes(type) // MULTI_NAMESPACE_OBJ_TYPE_2 and NON_MULTI_NAMESPACE_TYPE are omitted
);
client = elasticsearchClientMock.createElasticsearchClient();
const serializer = new SavedObjectsSerializer(registry);
savedObjectsMock = savedObjectsRepositoryMock.create();
savedObjectsMock.find.mockResolvedValue({
pit_id: 'foo',
saved_objects: [],
// the rest of these fields don't matter but are included for type safety
total: 0,
page: 1,
per_page: 100,
});
createPointInTimeFinder = jest.fn();
createPointInTimeFinder.mockImplementation((params) => {
pointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ savedObjectsMock })(params);
return pointInTimeFinder;
});
return {
registry,
allowedTypes: [
MULTI_NAMESPACE_OBJ_TYPE_1,
MULTI_NAMESPACE_OBJ_TYPE_2,
NON_MULTI_NAMESPACE_OBJ_TYPE,
], // MULTI_NAMESPACE_HIDDEN_TYPE is omitted
client,
serializer,
getIndexForType: (type: string) => `index-for-${type}`,
createPointInTimeFinder,
objects,
options,
};
}
/** Mocks the saved objects client so it returns the expected results */
function mockMgetResults(
...results: Array<{
found: boolean;
references?: Array<{ type: string; id: string }>;
}>
) {
client.mget.mockReturnValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
docs: results.map((x) => {
const references =
x.references?.map(({ type, id }) => ({ type, id, name: 'ref-name' })) ?? [];
return x.found
? {
_id: 'doesnt-matter',
_index: 'doesnt-matter',
_source: {
namespaces: SPACES,
references,
},
...VERSION_PROPS,
found: true,
}
: {
_id: 'doesnt-matter',
_index: 'doesnt-matter',
found: false,
};
}),
})
);
}
function mockFindResults(...results: LegacyUrlAlias[]) {
savedObjectsMock.find.mockResolvedValueOnce({
pit_id: 'foo',
saved_objects: results.map((attributes) => ({
id: 'doesnt-matter',
type: LEGACY_URL_ALIAS_TYPE,
attributes,
references: [],
score: 0, // doesn't matter
})),
// the rest of these fields don't matter but are included for type safety
total: 0,
page: 1,
per_page: 100,
});
}
/** Asserts that mget is called for the given objects */
function expectMgetArgs(
n: number,
...objects: SavedObjectsCollectMultiNamespaceReferencesObject[]
) {
const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` }));
expect(client.mget).toHaveBeenNthCalledWith(n, { body: { docs } }, expect.anything());
}
it('returns an empty array if no object args are passed in', async () => {
const params = setup([]);
const result = await collectMultiNamespaceReferences(params);
expect(client.mget).not.toHaveBeenCalled();
expect(result.objects).toEqual([]);
});
it('excludes args that have unsupported types', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' };
const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' };
const params = setup([obj1, obj2, obj3]);
mockMgetResults({ found: true }); // results for obj1
const result = await collectMultiNamespaceReferences(params);
expect(client.mget).toHaveBeenCalledTimes(1);
expectMgetArgs(1, obj1); // the non-multi-namespace type and the hidden type are excluded
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [] },
// even though they are excluded from the cluster call, obj2 and obj3 are included in the results
{ ...obj2, spaces: [], inboundReferences: [] },
{ ...obj3, spaces: [], inboundReferences: [] },
]);
});
it('excludes references that have unsupported types', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' };
const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' };
const params = setup([obj1]);
mockMgetResults({ found: true, references: [obj2, obj3] }); // results for obj1
const result = await collectMultiNamespaceReferences(params);
expect(client.mget).toHaveBeenCalledTimes(1);
expectMgetArgs(1, obj1);
// obj2 and obj3 are not retrieved in a second cluster call
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [] },
// obj2 and obj3 are excluded from the results
]);
});
it('handles circular references', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const params = setup([obj1]);
mockMgetResults({ found: true, references: [obj1] }); // results for obj1
const result = await collectMultiNamespaceReferences(params);
expect(params.client.mget).toHaveBeenCalledTimes(1);
expectMgetArgs(1, obj1); // obj1 is retrieved once, and it is not retrieved again in a second cluster call
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj1 reflects the inbound reference to itself
]);
});
it('handles a reference graph more than 20 layers deep (circuit-breaker)', async () => {
const type = MULTI_NAMESPACE_OBJ_TYPE_1;
const params = setup([{ type, id: 'id-1' }]);
for (let i = 1; i < 100; i++) {
mockMgetResults({ found: true, references: [{ type, id: `id-${i + 1}` }] });
}
await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow(
/Exceeded maximum reference graph depth/
);
expect(params.client.mget).toHaveBeenCalledTimes(20);
});
it('handles multiple inbound references', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' };
const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' };
const params = setup([obj1, obj2]);
mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [obj3] }); // results for obj1 and obj2
mockMgetResults({ found: true }); // results for obj3
const result = await collectMultiNamespaceReferences(params);
expect(params.client.mget).toHaveBeenCalledTimes(2);
expectMgetArgs(1, obj1, obj2);
expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [] },
{ ...obj2, spaces: SPACES, inboundReferences: [] },
{
...obj3,
spaces: SPACES,
inboundReferences: [
// obj3 reflects both inbound references
{ ...obj1, name: 'ref-name' },
{ ...obj2, name: 'ref-name' },
],
},
]);
});
it('handles transitive references', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' };
const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' };
const params = setup([obj1]);
mockMgetResults({ found: true, references: [obj2] }); // results for obj1
mockMgetResults({ found: true, references: [obj3] }); // results for obj2
mockMgetResults({ found: true }); // results for obj3
const result = await collectMultiNamespaceReferences(params);
expect(params.client.mget).toHaveBeenCalledTimes(3);
expectMgetArgs(1, obj1);
expectMgetArgs(2, obj2); // obj2 is retrieved in a second cluster call
expectMgetArgs(3, obj3); // obj3 is retrieved in a third cluster call
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [] },
{ ...obj2, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj2 reflects the inbound reference
{ ...obj3, spaces: SPACES, inboundReferences: [{ ...obj2, name: 'ref-name' }] }, // obj3 reflects the inbound reference
]);
});
it('handles missing objects and missing references', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; // found, with missing references to obj4 and obj5
const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; // missing object (found, but doesn't exist in the current space))
const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; // missing object (not found
const obj4 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-4' }; // missing reference (found but doesn't exist in the current space)
const obj5 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-5' }; // missing reference (not found)
const params = setup([obj1, obj2, obj3]);
mockMgetResults({ found: true, references: [obj4, obj5] }, { found: true }, { found: false }); // results for obj1, obj2, and obj3
mockMgetResults({ found: true }, { found: false }); // results for obj4 and obj5
mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj1
mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj2
mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4
const result = await collectMultiNamespaceReferences(params);
expect(params.client.mget).toHaveBeenCalledTimes(2);
expectMgetArgs(1, obj1, obj2, obj3);
expectMgetArgs(2, obj4, obj5);
expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3);
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [] },
{ ...obj2, spaces: [], inboundReferences: [], isMissing: true },
{ ...obj3, spaces: [], inboundReferences: [], isMissing: true },
{ ...obj4, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true },
{ ...obj5, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true },
]);
});
it('handles the purpose="updateObjectsSpaces" option', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-2' };
const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-3' };
const params = setup([obj1, obj2], { purpose: 'updateObjectsSpaces' });
mockMgetResults({ found: true, references: [obj3] }); // results for obj1
const result = await collectMultiNamespaceReferences(params);
expect(client.mget).toHaveBeenCalledTimes(1);
expectMgetArgs(1, obj1); // obj2 is excluded
// obj3 is not retrieved in a second cluster call
expect(result.objects).toEqual([
{ ...obj1, spaces: SPACES, inboundReferences: [] },
// even though it is excluded from the cluster call, obj2 is included in the results
{ ...obj2, spaces: [], inboundReferences: [] },
// obj3 is excluded from the results
]);
});
describe('legacy URL aliases', () => {
it('uses the PointInTimeFinder to search for legacy URL aliases', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' };
const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' };
const params = setup([obj1, obj2], {});
mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [] }); // results for obj1 and obj2
mockMgetResults({ found: true, references: [] }); // results for obj3
mockFindResults(
// mock search results for four aliases for obj1, and none for obj2 or obj3
...[1, 2, 3, 4].map((i) => ({
sourceId: obj1.id,
targetId: 'doesnt-matter',
targetType: obj1.type,
targetNamespace: `space-${i}`,
}))
);
const result = await collectMultiNamespaceReferences(params);
expect(client.mget).toHaveBeenCalledTimes(2);
expectMgetArgs(1, obj1, obj2);
expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call
expect(createPointInTimeFinder).toHaveBeenCalledTimes(1);
const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments;
expect(kueryFilterArgs).toHaveLength(2);
const typeAndIdFilters = kueryFilterArgs[1].arguments;
expect(typeAndIdFilters).toHaveLength(3);
[obj1, obj2, obj3].forEach(({ type, id }, i) => {
const typeAndIdFilter = typeAndIdFilters[i].arguments;
expect(typeAndIdFilter).toEqual([
expect.objectContaining({
arguments: expect.arrayContaining([{ type: 'literal', value: type }]),
}),
expect.objectContaining({
arguments: expect.arrayContaining([{ type: 'literal', value: id }]),
}),
]);
});
expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1);
expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2);
expect(result.objects).toEqual([
{
...obj1,
spaces: SPACES,
inboundReferences: [],
spacesWithMatchingAliases: ['space-1', 'space-2', 'space-3', 'space-4'],
},
{ ...obj2, spaces: SPACES, inboundReferences: [] },
{ ...obj3, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] },
]);
});
it('does not create a PointInTimeFinder if no objects are passed in', async () => {
const params = setup([]);
await collectMultiNamespaceReferences(params);
expect(params.createPointInTimeFinder).not.toHaveBeenCalled();
});
it('does not search for objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' };
const params = setup([obj1, obj2]);
mockMgetResults({ found: true }, { found: false }); // results for obj1 and obj2
await collectMultiNamespaceReferences(params);
expect(createPointInTimeFinder).toHaveBeenCalledTimes(1);
const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments;
expect(kueryFilterArgs).toHaveLength(2);
const typeAndIdFilters = kueryFilterArgs[1].arguments;
expect(typeAndIdFilters).toHaveLength(1);
const typeAndIdFilter = typeAndIdFilters[0].arguments;
expect(typeAndIdFilter).toEqual([
expect.objectContaining({
arguments: expect.arrayContaining([{ type: 'literal', value: obj1.type }]),
}),
expect.objectContaining({
arguments: expect.arrayContaining([{ type: 'literal', value: obj1.id }]),
}),
]);
expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1);
expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2);
});
it('does not search at all if all objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const params = setup([obj1]);
mockMgetResults({ found: false }); // results for obj1
await collectMultiNamespaceReferences(params);
expect(params.createPointInTimeFinder).not.toHaveBeenCalled();
});
it('handles PointInTimeFinder.find errors', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const params = setup([obj1]);
mockMgetResults({ found: true }); // results for obj1
savedObjectsMock.find.mockRejectedValue(new Error('Oh no!'));
await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow(
'Failed to retrieve legacy URL aliases: Oh no!'
);
expect(createPointInTimeFinder).toHaveBeenCalledTimes(1);
expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1);
expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); // we still close the point-in-time, even though the search failed
});
it('handles PointInTimeFinder.close errors', async () => {
const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' };
const params = setup([obj1]);
mockMgetResults({ found: true }); // results for obj1
savedObjectsMock.closePointInTime.mockRejectedValue(new Error('Oh no!'));
await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow(
'Failed to retrieve legacy URL aliases: Oh no!'
);
expect(createPointInTimeFinder).toHaveBeenCalledTimes(1);
expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1);
expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -0,0 +1,310 @@
/*
* 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.
*/
// @ts-expect-error no ts
import { esKuery } from '../../es_query';
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import type { SavedObjectsSerializer } from '../../serialization';
import type { SavedObject, SavedObjectsBaseOptions } from '../../types';
import { getRootFields } from './included_fields';
import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils';
import type {
ISavedObjectsPointInTimeFinder,
SavedObjectsCreatePointInTimeFinderOptions,
} from './point_in_time_finder';
import type { RepositoryEsClient } from './repository_es_client';
/**
* When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error.
*/
const MAX_REFERENCE_GRAPH_DEPTH = 20;
/**
* How many aliases to search for per page. This is smaller than the PointInTimeFinder's default of 1000. We specify 100 for the page count
* because this is a relatively unimportant operation, and we want to avoid blocking the Elasticsearch thread pool for longer than
* necessary.
*/
const ALIAS_SEARCH_PER_PAGE = 100;
/**
* An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the
* `namespaceType: 'multiple'` or `namespaceType: 'multiple-isolated'` option).
*
* Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with
* the `namespaceType: 'multiple'`).
*
* @public
*/
export interface SavedObjectsCollectMultiNamespaceReferencesObject {
id: string;
type: string;
}
/**
* Options for collecting references.
*
* @public
*/
export interface SavedObjectsCollectMultiNamespaceReferencesOptions
extends SavedObjectsBaseOptions {
/** Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' */
purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces';
}
/**
* A returned input object or one of its references, with additional context.
*
* @public
*/
export interface SavedObjectReferenceWithContext {
/** The type of the referenced object */
type: string;
/** The ID of the referenced object */
id: string;
/** The space(s) that the referenced object exists in */
spaces: string[];
/**
* References to this object; note that this does not contain _all inbound references everywhere for this object_, it only contains
* inbound references for the scope of this operation
*/
inboundReferences: Array<{
/** The type of the object that has the inbound reference */
type: string;
/** The ID of the object that has the inbound reference */
id: string;
/** The name of the inbound reference */
name: string;
}>;
/** Whether or not this object or reference is missing */
isMissing?: boolean;
/** The space(s) that legacy URL aliases matching this type/id exist in */
spacesWithMatchingAliases?: string[];
}
/**
* The response when object references are collected.
*
* @public
*/
export interface SavedObjectsCollectMultiNamespaceReferencesResponse {
objects: SavedObjectReferenceWithContext[];
}
/**
* Parameters for the collectMultiNamespaceReferences function.
*
* @internal
*/
export interface CollectMultiNamespaceReferencesParams {
registry: ISavedObjectTypeRegistry;
allowedTypes: string[];
client: RepositoryEsClient;
serializer: SavedObjectsSerializer;
getIndexForType: (type: string) => string;
createPointInTimeFinder: <T = unknown, A = unknown>(
findOptions: SavedObjectsCreatePointInTimeFinderOptions
) => ISavedObjectsPointInTimeFinder<T, A>;
objects: SavedObjectsCollectMultiNamespaceReferencesObject[];
options?: SavedObjectsCollectMultiNamespaceReferencesOptions;
}
/**
* Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace
* type.
*/
export async function collectMultiNamespaceReferences(
params: CollectMultiNamespaceReferencesParams
): Promise<SavedObjectsCollectMultiNamespaceReferencesResponse> {
const { createPointInTimeFinder, objects } = params;
if (!objects.length) {
return { objects: [] };
}
const { objectMap, inboundReferencesMap } = await getObjectsAndReferences(params);
const objectsWithContext = Array.from(
inboundReferencesMap.entries()
).map<SavedObjectReferenceWithContext>(([referenceKey, referenceVal]) => {
const inboundReferences = Array.from(referenceVal.entries()).map(([objectKey, name]) => {
const { type, id } = parseKey(objectKey);
return { type, id, name };
});
const { type, id } = parseKey(referenceKey);
const object = objectMap.get(referenceKey);
const spaces = object?.namespaces ?? [];
return { type, id, spaces, inboundReferences, ...(object === null && { isMissing: true }) };
});
const aliasesMap = await checkLegacyUrlAliases(createPointInTimeFinder, objectsWithContext);
const results = objectsWithContext.map((obj) => {
const key = getKey(obj);
const val = aliasesMap.get(key);
const spacesWithMatchingAliases = val && Array.from(val);
return { ...obj, spacesWithMatchingAliases };
});
return {
objects: results,
};
}
/**
* Recursively fetches objects and their references, returning a map of the retrieved objects and a map of all inbound references.
*/
async function getObjectsAndReferences({
registry,
allowedTypes,
client,
serializer,
getIndexForType,
objects,
options = {},
}: CollectMultiNamespaceReferencesParams) {
const { namespace, purpose } = options;
const inboundReferencesMap = objects.reduce(
// Add the input objects to the references map so they are returned with the results, even if they have no inbound references
(acc, cur) => acc.set(getKey(cur), new Map()),
new Map<string, Map<string, string>>()
);
const objectMap = new Map<string, SavedObject | null>();
const rootFields = getRootFields();
const makeBulkGetDocs = (objectsToGet: SavedObjectsCollectMultiNamespaceReferencesObject[]) =>
objectsToGet.map(({ type, id }) => ({
_id: serializer.generateRawId(undefined, type, id),
_index: getIndexForType(type),
_source: rootFields, // Optimized to only retrieve root fields (ignoring type-specific fields)
}));
const validObjectTypesFilter = ({ type }: SavedObjectsCollectMultiNamespaceReferencesObject) =>
allowedTypes.includes(type) &&
(purpose === 'updateObjectsSpaces'
? registry.isShareable(type)
: registry.isMultiNamespace(type));
let bulkGetObjects = objects.filter(validObjectTypesFilter);
let count = 0; // this is a circuit-breaker to ensure we don't hog too many resources; we should never have an object graph this deep
while (bulkGetObjects.length) {
if (count >= MAX_REFERENCE_GRAPH_DEPTH) {
throw new Error(
`Exceeded maximum reference graph depth of ${MAX_REFERENCE_GRAPH_DEPTH} objects!`
);
}
const bulkGetResponse = await client.mget(
{ body: { docs: makeBulkGetDocs(bulkGetObjects) } },
{ ignore: [404] }
);
const newObjectsToGet = new Set<string>();
for (let i = 0; i < bulkGetObjects.length; i++) {
// For every element in bulkGetObjects, there should be a matching element in bulkGetResponse.body.docs
const { type, id } = bulkGetObjects[i];
const objectKey = getKey({ type, id });
const doc = bulkGetResponse.body.docs[i];
// @ts-expect-error MultiGetHit._source is optional
if (!doc.found || !rawDocExistsInNamespace(registry, doc, namespace)) {
objectMap.set(objectKey, null);
continue;
}
// @ts-expect-error MultiGetHit._source is optional
const object = getSavedObjectFromSource(registry, type, id, doc);
objectMap.set(objectKey, object);
for (const reference of object.references) {
if (!validObjectTypesFilter(reference)) {
continue;
}
const referenceKey = getKey(reference);
const referenceVal = inboundReferencesMap.get(referenceKey) ?? new Map<string, string>();
if (!referenceVal.has(objectKey)) {
inboundReferencesMap.set(referenceKey, referenceVal.set(objectKey, reference.name));
}
if (!objectMap.has(referenceKey)) {
newObjectsToGet.add(referenceKey);
}
}
}
bulkGetObjects = Array.from(newObjectsToGet).map((key) => parseKey(key));
count++;
}
return { objectMap, inboundReferencesMap };
}
/**
* Fetches all legacy URL aliases that match the given objects, returning a map of the matching aliases and what space(s) they exist in.
*/
async function checkLegacyUrlAliases(
createPointInTimeFinder: <T = unknown, A = unknown>(
findOptions: SavedObjectsCreatePointInTimeFinderOptions
) => ISavedObjectsPointInTimeFinder<T, A>,
objects: SavedObjectReferenceWithContext[]
) {
const filteredObjects = objects.filter(({ spaces }) => spaces.length !== 0);
if (!filteredObjects.length) {
return new Map<string, Set<string>>();
}
const filter = createAliasKueryFilter(filteredObjects);
const finder = createPointInTimeFinder<LegacyUrlAlias>({
type: LEGACY_URL_ALIAS_TYPE,
perPage: ALIAS_SEARCH_PER_PAGE,
filter,
});
const aliasesMap = new Map<string, Set<string>>();
let error: Error | undefined;
try {
for await (const { saved_objects: savedObjects } of finder.find()) {
for (const alias of savedObjects) {
const { sourceId, targetType, targetNamespace } = alias.attributes;
const key = getKey({ type: targetType, id: sourceId });
const val = aliasesMap.get(key) ?? new Set<string>();
val.add(targetNamespace);
aliasesMap.set(key, val);
}
}
} catch (e) {
error = e;
}
try {
await finder.close();
} catch (e) {
if (!error) {
error = e;
}
}
if (error) {
throw new Error(`Failed to retrieve legacy URL aliases: ${error.message}`);
}
return aliasesMap;
}
function createAliasKueryFilter(objects: SavedObjectReferenceWithContext[]) {
const { buildNode } = esKuery.nodeTypes.function;
const kueryNodes = objects.reduce<unknown[]>((acc, { type, id }) => {
const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.targetType`, type);
const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.sourceId`, id);
acc.push(buildNode('and', [match1, match2]));
return acc;
}, []);
return buildNode('and', [
buildNode('not', buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.disabled`, true)), // ignore aliases that have been disabled
buildNode('or', kueryNodes),
]);
}
/** Takes an object with a `type` and `id` field and returns a key string */
function getKey({ type, id }: { type: string; id: string }) {
return `${type}:${id}`;
}
/** Parses a 'type:id' key string and returns an object with a `type` field and an `id` field */
function parseKey(key: string) {
const type = key.slice(0, key.indexOf(':'));
const id = key.slice(type.length + 1);
return { type, id };
}

View file

@ -6,125 +6,63 @@
* Side Public License, v 1.
*/
import { includedFields } from './included_fields';
import { getRootFields, includedFields } from './included_fields';
const BASE_FIELD_COUNT = 10;
describe('getRootFields', () => {
it('returns copy of root fields', () => {
const fields = getRootFields();
expect(fields).toMatchInlineSnapshot(`
Array [
"namespace",
"namespaces",
"type",
"references",
"migrationVersion",
"coreMigrationVersion",
"updated_at",
"originId",
]
`);
});
});
describe('includedFields', () => {
const rootFields = getRootFields();
it('returns undefined if fields are not provided', () => {
expect(includedFields()).toBe(undefined);
});
it('accepts type string', () => {
it('accepts type and field as string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('type');
expect(fields).toEqual(['config.foo', ...rootFields, 'foo']);
});
it('accepts type as string array', () => {
it('accepts type as array and field as string', () => {
const fields = includedFields(['config', 'secret'], 'foo');
expect(fields).toMatchInlineSnapshot(`
Array [
"config.foo",
"secret.foo",
"namespace",
"namespaces",
"type",
"references",
"migrationVersion",
"coreMigrationVersion",
"updated_at",
"originId",
"foo",
]
`);
expect(fields).toEqual(['config.foo', 'secret.foo', ...rootFields, 'foo']);
});
it('accepts field as string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('config.foo');
});
it('accepts fields as an array', () => {
it('accepts type as string and field as array', () => {
const fields = includedFields('config', ['foo', 'bar']);
expect(fields).toHaveLength(BASE_FIELD_COUNT + 2);
expect(fields).toContain('config.foo');
expect(fields).toContain('config.bar');
expect(fields).toEqual(['config.foo', 'config.bar', ...rootFields, 'foo', 'bar']);
});
it('accepts type as string array and fields as string array', () => {
it('accepts type as array and field as array', () => {
const fields = includedFields(['config', 'secret'], ['foo', 'bar']);
expect(fields).toMatchInlineSnapshot(`
Array [
"config.foo",
"config.bar",
"secret.foo",
"secret.bar",
"namespace",
"namespaces",
"type",
"references",
"migrationVersion",
"coreMigrationVersion",
"updated_at",
"originId",
"foo",
"bar",
]
`);
});
it('includes namespace', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('namespace');
});
it('includes namespaces', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('namespaces');
});
it('includes references', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('references');
});
it('includes migrationVersion', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('migrationVersion');
});
it('includes updated_at', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('updated_at');
});
it('includes originId', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('originId');
expect(fields).toEqual([
'config.foo',
'config.bar',
'secret.foo',
'secret.bar',
...rootFields,
'foo',
'bar',
]);
});
it('uses wildcard when type is not provided', () => {
const fields = includedFields(undefined, 'foo');
expect(fields).toHaveLength(BASE_FIELD_COUNT);
expect(fields).toContain('*.foo');
});
describe('v5 compatibility', () => {
it('includes legacy field path', () => {
const fields = includedFields('config', ['foo', 'bar']);
expect(fields).toHaveLength(BASE_FIELD_COUNT + 2);
expect(fields).toContain('foo');
expect(fields).toContain('bar');
});
expect(fields).toEqual(['*.foo', ...rootFields, 'foo']);
});
});

View file

@ -9,6 +9,22 @@
function toArray(value: string | string[]): string[] {
return typeof value === 'string' ? [value] : value;
}
const ROOT_FIELDS = [
'namespace',
'namespaces',
'type',
'references',
'migrationVersion',
'coreMigrationVersion',
'updated_at',
'originId',
];
export function getRootFields() {
return [...ROOT_FIELDS];
}
/**
* Provides an array of paths for ES source filtering
*/
@ -28,13 +44,6 @@ export function includedFields(
.reduce((acc: string[], t) => {
return [...acc, ...sourceFields.map((f) => `${t}.${f}`)];
}, [])
.concat('namespace')
.concat('namespaces')
.concat('type')
.concat('references')
.concat('migrationVersion')
.concat('coreMigrationVersion')
.concat('updated_at')
.concat('originId')
.concat(ROOT_FIELDS)
.concat(fields); // v5 compatibility
}

View file

@ -27,3 +27,17 @@ export type {
export { SavedObjectsErrorHelpers } from './errors';
export { SavedObjectsUtils } from './utils';
export type {
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
SavedObjectReferenceWithContext,
SavedObjectsCollectMultiNamespaceReferencesResponse,
} from './collect_multi_namespace_references';
export type {
SavedObjectsUpdateObjectsSpacesObject,
SavedObjectsUpdateObjectsSpacesOptions,
SavedObjectsUpdateObjectsSpacesResponse,
SavedObjectsUpdateObjectsSpacesResponseObject,
} from './update_objects_spaces';

View file

@ -0,0 +1,243 @@
/*
* 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 { typeRegistryMock } from '../../saved_objects_type_registry.mock';
import type { SavedObjectsRawDoc } from '../../serialization';
import { encodeHitVersion } from '../../version';
import {
getBulkOperationError,
getSavedObjectFromSource,
rawDocExistsInNamespace,
} from './internal_utils';
import { ALL_NAMESPACES_STRING } from './utils';
describe('#getBulkOperationError', () => {
const type = 'obj-type';
const id = 'obj-id';
it('returns index not found error', () => {
const rawResponse = {
status: 404,
error: { type: 'index_not_found_exception', reason: 'some-reason', index: 'some-index' },
};
const result = getBulkOperationError(type, id, rawResponse);
expect(result).toEqual({
error: 'Internal Server Error',
message: 'An internal server error occurred', // TODO: this error payload is not very helpful to consumers, can we change it?
statusCode: 500,
});
});
it('returns generic not found error', () => {
const rawResponse = {
status: 404,
error: { type: 'anything', reason: 'some-reason', index: 'some-index' },
};
const result = getBulkOperationError(type, id, rawResponse);
expect(result).toEqual({
error: 'Not Found',
message: `Saved object [${type}/${id}] not found`,
statusCode: 404,
});
});
it('returns conflict error', () => {
const rawResponse = {
status: 409,
error: { type: 'anything', reason: 'some-reason', index: 'some-index' },
};
const result = getBulkOperationError(type, id, rawResponse);
expect(result).toEqual({
error: 'Conflict',
message: `Saved object [${type}/${id}] conflict`,
statusCode: 409,
});
});
it('returns an unexpected result error', () => {
const rawResponse = {
status: 123, // any status
error: { type: 'anything', reason: 'some-reason', index: 'some-index' },
};
const result = getBulkOperationError(type, id, rawResponse);
expect(result).toEqual({
error: 'Internal Server Error',
message: `Unexpected bulk response [${rawResponse.status}] ${rawResponse.error.type}: ${rawResponse.error.reason}`,
statusCode: 500,
});
});
});
describe('#getSavedObjectFromSource', () => {
const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type';
const NON_NAMESPACE_AGNOSTIC_TYPE = 'other-type';
const registry = typeRegistryMock.create();
registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE);
const id = 'obj-id';
const _seq_no = 1;
const _primary_term = 1;
const attributes = { foo: 'bar' };
const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }];
const migrationVersion = { foo: 'migrationVersion' };
const coreMigrationVersion = 'coreMigrationVersion';
const originId = 'originId';
// eslint-disable-next-line @typescript-eslint/naming-convention
const updated_at = 'updatedAt';
function createRawDoc(
type: string,
namespaceAttrs?: { namespace?: string; namespaces?: string[] }
) {
return {
// other fields exist on the raw document, but they are not relevant to these test cases
_seq_no,
_primary_term,
_source: {
type,
[type]: attributes,
references,
migrationVersion,
coreMigrationVersion,
originId,
updated_at,
...namespaceAttrs,
},
};
}
it('returns object with expected attributes', () => {
const type = 'any-type';
const doc = createRawDoc(type);
const result = getSavedObjectFromSource(registry, type, id, doc);
expect(result).toEqual({
attributes,
coreMigrationVersion,
id,
migrationVersion,
namespaces: expect.anything(), // see specific test cases below
originId,
references,
type,
updated_at,
version: encodeHitVersion(doc),
});
});
it('returns object with empty namespaces array when type is namespace-agnostic', () => {
const type = NAMESPACE_AGNOSTIC_TYPE;
const doc = createRawDoc(type);
const result = getSavedObjectFromSource(registry, type, id, doc);
expect(result).toEqual(expect.objectContaining({ namespaces: [] }));
});
it('returns object with namespaces when type is not namespace-agnostic and namespaces array is defined', () => {
const type = NON_NAMESPACE_AGNOSTIC_TYPE;
const namespaces = ['foo-ns', 'bar-ns'];
const doc = createRawDoc(type, { namespaces });
const result = getSavedObjectFromSource(registry, type, id, doc);
expect(result).toEqual(expect.objectContaining({ namespaces }));
});
it('derives namespaces from namespace attribute when type is not namespace-agnostic and namespaces array is not defined', () => {
// Deriving namespaces from the namespace attribute is an implementation detail of SavedObjectsUtils.namespaceIdToString().
// However, these test cases assertions are written out anyway for clarity.
const type = NON_NAMESPACE_AGNOSTIC_TYPE;
const doc1 = createRawDoc(type, { namespace: undefined });
const doc2 = createRawDoc(type, { namespace: 'foo-ns' });
const result1 = getSavedObjectFromSource(registry, type, id, doc1);
const result2 = getSavedObjectFromSource(registry, type, id, doc2);
expect(result1).toEqual(expect.objectContaining({ namespaces: ['default'] }));
expect(result2).toEqual(expect.objectContaining({ namespaces: ['foo-ns'] }));
});
});
describe('#rawDocExistsInNamespace', () => {
const SINGLE_NAMESPACE_TYPE = 'single-type';
const MULTI_NAMESPACE_TYPE = 'multi-type';
const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type';
const registry = typeRegistryMock.create();
registry.isSingleNamespace.mockImplementation((type) => type === SINGLE_NAMESPACE_TYPE);
registry.isMultiNamespace.mockImplementation((type) => type === MULTI_NAMESPACE_TYPE);
registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE);
function createRawDoc(
type: string,
namespaceAttrs: { namespace?: string; namespaces?: string[] }
) {
return {
// other fields exist on the raw document, but they are not relevant to these test cases
_source: {
type,
...namespaceAttrs,
},
} as SavedObjectsRawDoc;
}
describe('single-namespace type', () => {
it('returns true regardless of namespace or namespaces fields', () => {
// Technically, a single-namespace type does not exist in a space unless it has a namespace prefix in its raw ID and a matching
// 'namespace' field. However, historically we have not enforced the latter, we have just relied on searching for and deserializing
// documents with the correct namespace prefix. We may revisit this in the future.
const doc1 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespace: 'some-space' }); // the namespace field is ignored
const doc2 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored
expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true);
expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true);
expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true);
});
});
describe('multi-namespace type', () => {
const docInDefaultSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['default'] });
const docInSomeSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['some-space'] });
const docInAllSpaces = createRawDoc(MULTI_NAMESPACE_TYPE, {
namespaces: [ALL_NAMESPACES_STRING],
});
const docInNoSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: [] });
it('returns true when the document namespaces matches', () => {
expect(rawDocExistsInNamespace(registry, docInDefaultSpace, undefined)).toBe(true);
expect(rawDocExistsInNamespace(registry, docInAllSpaces, undefined)).toBe(true);
expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'some-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'some-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'other-space')).toBe(true);
});
it('returns false when the document namespace does not match', () => {
expect(rawDocExistsInNamespace(registry, docInDefaultSpace, 'other-space')).toBe(false);
expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'other-space')).toBe(false);
expect(rawDocExistsInNamespace(registry, docInNoSpace, 'other-space')).toBe(false);
});
});
describe('namespace-agnostic type', () => {
it('returns true regardless of namespace or namespaces fields', () => {
const doc1 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespace: 'some-space' }); // the namespace field is ignored
const doc2 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored
expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true);
expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true);
expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true);
expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true);
});
});
});

View file

@ -0,0 +1,143 @@
/*
* 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 type { Payload } from '@hapi/boom';
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import type { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization';
import type { SavedObject } from '../../types';
import { decodeRequestVersion, encodeHitVersion } from '../../version';
import { SavedObjectsErrorHelpers } from './errors';
import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from './utils';
/**
* Checks the raw response of a bulk operation and returns an error if necessary.
*
* @param type
* @param id
* @param rawResponse
*
* @internal
*/
export function getBulkOperationError(
type: string,
id: string,
rawResponse: {
status: number;
error?: { type: string; reason: string; index: string };
// Other fields are present on a bulk operation result but they are irrelevant for this function
}
): Payload | undefined {
const { status, error } = rawResponse;
if (error) {
switch (status) {
case 404:
return error.type === 'index_not_found_exception'
? SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index).output.payload
: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload;
case 409:
return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload;
default:
return {
error: 'Internal Server Error',
message: `Unexpected bulk response [${status}] ${error.type}: ${error.reason}`,
statusCode: 500,
};
}
}
}
/**
* Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control.
*
* @param version Optional version specified by the consumer.
* @param document Optional existing document that was obtained in a preflight operation.
*
* @internal
*/
export function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) {
if (version) {
return decodeRequestVersion(version);
} else if (document) {
return {
if_seq_no: document._seq_no,
if_primary_term: document._primary_term,
};
}
return {};
}
/**
* Gets a saved object from a raw ES document.
*
* @param registry
* @param type
* @param id
* @param doc
*
* @internal
*/
export function getSavedObjectFromSource<T>(
registry: ISavedObjectTypeRegistry,
type: string,
id: string,
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource }
): SavedObject<T> {
const { originId, updated_at: updatedAt } = doc._source;
let namespaces: string[] = [];
if (!registry.isNamespaceAgnostic(type)) {
namespaces = doc._source.namespaces ?? [
SavedObjectsUtils.namespaceIdToString(doc._source.namespace),
];
}
return {
id,
type,
namespaces,
...(originId && { originId }),
...(updatedAt && { updated_at: updatedAt }),
version: encodeHitVersion(doc),
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
coreMigrationVersion: doc._source.coreMigrationVersion,
};
}
/**
* Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as
* we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the
* document's `namespaces` value includes the string representation of the given namespace.
*
* WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID
* format mentioned above do not apply.
*
* @param registry
* @param raw
* @param namespace
*/
export function rawDocExistsInNamespace(
registry: ISavedObjectTypeRegistry,
raw: SavedObjectsRawDoc,
namespace: string | undefined
) {
const rawDocType = raw._source.type;
// if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees
// of the document ID format and don't need to check this
if (!registry.isMultiNamespace(rawDocType)) {
return true;
}
const namespaces = raw._source.namespaces;
const existsInNamespace =
namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) ||
namespaces?.includes(ALL_NAMESPACES_STRING);
return existsInNamespace ?? false;
}

View file

@ -39,14 +39,14 @@ export interface PointInTimeFinderDependencies
}
/** @public */
export interface ISavedObjectsPointInTimeFinder {
export interface ISavedObjectsPointInTimeFinder<T, A> {
/**
* An async generator which wraps calls to `savedObjectsClient.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` size.
*/
find: () => AsyncGenerator<SavedObjectsFindResponse>;
find: () => AsyncGenerator<SavedObjectsFindResponse<T, A>>;
/**
* Closes the Point-In-Time associated with this finder instance.
*
@ -63,7 +63,8 @@ export interface ISavedObjectsPointInTimeFinder {
/**
* @internal
*/
export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder {
export class PointInTimeFinder<T = unknown, A = unknown>
implements ISavedObjectsPointInTimeFinder<T, A> {
readonly #log: Logger;
readonly #client: PointInTimeFinderClient;
readonly #findOptions: SavedObjectsFindOptions;
@ -162,7 +163,7 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder {
searchAfter?: estypes.Id[];
}) {
try {
return await this.#client.find({
return await this.#client.find<T, A>({
// Sort fields are required to use searchAfter, so we set some defaults here
sortField: 'updated_at',
sortOrder: 'desc',

View file

@ -24,11 +24,11 @@ const create = () => {
openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }),
resolve: jest.fn(),
update: jest.fn(),
addToNamespaces: jest.fn(),
deleteFromNamespaces: jest.fn(),
deleteByNamespace: jest.fn(),
incrementCounter: jest.fn(),
removeReferencesTo: jest.fn(),
collectMultiNamespaceReferences: jest.fn(),
updateObjectsSpaces: jest.fn(),
};
mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({

View file

@ -6,7 +6,12 @@
* Side Public License, v 1.
*/
import { pointInTimeFinderMock } from './repository.test.mock';
import {
pointInTimeFinderMock,
mockCollectMultiNamespaceReferences,
mockGetBulkOperationError,
mockUpdateObjectsSpaces,
} from './repository.test.mock';
import { SavedObjectsRepository } from './repository';
import * as getSearchDslNS from './search_dsl/search_dsl';
@ -67,9 +72,9 @@ describe('SavedObjectsRepository', () => {
* This type has namespaceType: 'multiple-isolated'.
*
* That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is NOT shareable
* across namespaces. This distinction only matters when using the `addToNamespaces` and `deleteFromNamespaces` APIs, or when using the
* `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what namespaces an object
* exists in.
* across namespaces. This distinction only matters when using the `collectMultiNamespaceReferences` or `updateObjectsSpaces` APIs, or
* when using the `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what
* namespaces an object exists in.
*
* In a nutshell, this type is more restrictive than `MULTI_NAMESPACE_TYPE`, so we use `MULTI_NAMESPACE_ISOLATED_TYPE` for any test cases
* where `MULTI_NAMESPACE_TYPE` would also satisfy the test case.
@ -295,164 +300,6 @@ describe('SavedObjectsRepository', () => {
references: [{ name: 'search_0', type: 'search', id: '123' }],
});
describe('#addToNamespaces', () => {
const id = 'some-id';
const type = MULTI_NAMESPACE_TYPE;
const currentNs1 = 'default';
const currentNs2 = 'foo-namespace';
const newNs1 = 'bar-namespace';
const newNs2 = 'baz-namespace';
const mockGetResponse = (type, id) => {
// mock a document that exists in two namespaces
const mockResponse = getMockGetResponse({ type, id });
mockResponse._source.namespaces = [currentNs1, currentNs2];
client.get.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse)
);
};
const addToNamespacesSuccess = async (type, id, namespaces, options) => {
mockGetResponse(type, id);
client.update.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
_id: `${type}:${id}`,
...mockVersionProps,
result: 'updated',
})
);
const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options);
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.update).toHaveBeenCalledTimes(1);
return result;
};
describe('client calls', () => {
it(`should use ES get action then update action`, async () => {
await addToNamespacesSuccess(type, id, [newNs1, newNs2]);
});
it(`defaults to the version of the existing document`, async () => {
await addToNamespacesSuccess(type, id, [newNs1, newNs2]);
const versionProperties = {
if_seq_no: mockVersionProps._seq_no,
if_primary_term: mockVersionProps._primary_term,
};
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining(versionProperties),
expect.anything()
);
});
it(`accepts version`, async () => {
await addToNamespacesSuccess(type, id, [newNs1, newNs2], {
version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }),
});
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }),
expect.anything()
);
});
it(`defaults to a refresh setting of wait_for`, async () => {
await addToNamespacesSuccess(type, id, [newNs1, newNs2]);
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({ refresh: 'wait_for' }),
expect.anything()
);
});
});
describe('errors', () => {
const expectNotFoundError = async (type, id, namespaces, options) => {
await expect(
savedObjectsRepository.addToNamespaces(type, id, namespaces, options)
).rejects.toThrowError(createGenericNotFoundError(type, id));
};
const expectBadRequestError = async (type, id, namespaces, message) => {
await expect(
savedObjectsRepository.addToNamespaces(type, id, namespaces)
).rejects.toThrowError(createBadRequestError(message));
};
it(`throws when type is invalid`, async () => {
await expectNotFoundError('unknownType', id, [newNs1, newNs2]);
expect(client.update).not.toHaveBeenCalled();
});
it(`throws when type is hidden`, async () => {
await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]);
expect(client.update).not.toHaveBeenCalled();
});
it(`throws when type is not shareable`, async () => {
const test = async (type) => {
const message = `${type} doesn't support multiple namespaces`;
await expectBadRequestError(type, id, [newNs1, newNs2], message);
expect(client.update).not.toHaveBeenCalled();
};
await test('index-pattern');
await test(MULTI_NAMESPACE_ISOLATED_TYPE);
await test(NAMESPACE_AGNOSTIC_TYPE);
});
it(`throws when namespaces is an empty array`, async () => {
const test = async (namespaces) => {
const message = 'namespaces must be a non-empty array of strings';
await expectBadRequestError(type, id, namespaces, message);
expect(client.update).not.toHaveBeenCalled();
};
await test([]);
});
it(`throws when ES is unable to find the document during get`, async () => {
client.get.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false })
);
await expectNotFoundError(type, id, [newNs1, newNs2]);
expect(client.get).toHaveBeenCalledTimes(1);
});
it(`throws when ES is unable to find the index during get`, async () => {
client.get.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
);
await expectNotFoundError(type, id, [newNs1, newNs2]);
expect(client.get).toHaveBeenCalledTimes(1);
});
it(`throws when the document exists, but not in this namespace`, async () => {
mockGetResponse(type, id);
await expectNotFoundError(type, id, [newNs1, newNs2], {
namespace: 'some-other-namespace',
});
expect(client.get).toHaveBeenCalledTimes(1);
});
it(`throws when ES is unable to find the document during update`, async () => {
mockGetResponse(type, id);
client.update.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
);
await expectNotFoundError(type, id, [newNs1, newNs2]);
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.update).toHaveBeenCalledTimes(1);
});
});
describe('returns', () => {
it(`returns all existing and new namespaces on success`, async () => {
const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]);
expect(result).toEqual({ namespaces: [currentNs1, currentNs2, newNs1, newNs2] });
});
it(`succeeds when adding existing namespaces`, async () => {
const result = await addToNamespacesSuccess(type, id, [currentNs1]);
expect(result).toEqual({ namespaces: [currentNs1, currentNs2] });
});
});
});
describe('#bulkCreate', () => {
const obj1 = {
type: 'config',
@ -757,6 +604,10 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
afterEach(() => {
mockGetBulkOperationError.mockReset();
});
const obj3 = {
type: 'dashboard',
id: 'three',
@ -764,11 +615,13 @@ describe('SavedObjectsRepository', () => {
references: [{ name: 'ref_0', type: 'test', id: '2' }],
};
const bulkCreateError = async (obj, esError, expectedError) => {
const bulkCreateError = async (obj, isBulkError, expectedErrorResult) => {
let response;
if (esError) {
if (isBulkError) {
// mock the bulk error for only the second object
mockGetBulkOperationError.mockReturnValueOnce(undefined);
mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error);
response = getMockBulkCreateResponse([obj1, obj, obj2]);
response.items[1].create = { error: esError };
} else {
response = getMockBulkCreateResponse([obj1, obj2]);
}
@ -779,14 +632,14 @@ describe('SavedObjectsRepository', () => {
const objects = [obj1, obj, obj2];
const result = await savedObjectsRepository.bulkCreate(objects);
expect(client.bulk).toHaveBeenCalled();
const objCall = esError ? expectObjArgs(obj) : [];
const objCall = isBulkError ? expectObjArgs(obj) : [];
const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
);
expect(result).toEqual({
saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)],
saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)],
});
};
@ -878,25 +731,9 @@ describe('SavedObjectsRepository', () => {
});
});
it(`returns error when there is a version conflict (bulk)`, async () => {
const esError = { type: 'version_conflict_engine_exception' };
await bulkCreateError(obj3, esError, expectErrorConflict(obj3));
});
it(`returns error when document is missing`, async () => {
const esError = { type: 'document_missing_exception' };
await bulkCreateError(obj3, esError, expectErrorNotFound(obj3));
});
it(`returns error reason for other errors`, async () => {
const esError = { reason: 'some_other_error' };
await bulkCreateError(obj3, esError, expectErrorResult(obj3, { message: esError.reason }));
});
it(`returns error string for other errors if no reason is defined`, async () => {
const esError = { foo: 'some_other_error' };
const expectedError = expectErrorResult(obj3, { message: JSON.stringify(esError) });
await bulkCreateError(obj3, esError, expectedError);
it(`returns bulk error`, async () => {
const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' };
await bulkCreateError(obj3, true, expectedErrorResult);
});
});
@ -1530,16 +1367,22 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
afterEach(() => {
mockGetBulkOperationError.mockReset();
});
const obj = {
type: 'dashboard',
id: 'three',
};
const bulkUpdateError = async (obj, esError, expectedError) => {
const bulkUpdateError = async (obj, isBulkError, expectedErrorResult) => {
const objects = [obj1, obj, obj2];
const mockResponse = getMockBulkUpdateResponse(objects);
if (esError) {
mockResponse.items[1].update = { error: esError };
if (isBulkError) {
// mock the bulk error for only the second object
mockGetBulkOperationError.mockReturnValueOnce(undefined);
mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error);
}
client.bulk.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse)
@ -1547,14 +1390,14 @@ describe('SavedObjectsRepository', () => {
const result = await savedObjectsRepository.bulkUpdate(objects);
expect(client.bulk).toHaveBeenCalled();
const objCall = esError ? expectObjArgs(obj) : [];
const objCall = isBulkError ? expectObjArgs(obj) : [];
const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
);
expect(result).toEqual({
saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)],
saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)],
});
};
@ -1592,19 +1435,19 @@ describe('SavedObjectsRepository', () => {
it(`returns error when type is invalid`, async () => {
const _obj = { ...obj, type: 'unknownType' };
await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj));
await bulkUpdateError(_obj, false, expectErrorNotFound(_obj));
});
it(`returns error when type is hidden`, async () => {
const _obj = { ...obj, type: HIDDEN_TYPE };
await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj));
await bulkUpdateError(_obj, false, expectErrorNotFound(_obj));
});
it(`returns error when object namespace is '*'`, async () => {
const _obj = { ...obj, namespace: '*' };
await bulkUpdateError(
_obj,
undefined,
false,
expectErrorResult(obj, createBadRequestError('"namespace" cannot be "*"'))
);
});
@ -1627,25 +1470,9 @@ describe('SavedObjectsRepository', () => {
await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse);
});
it(`returns error when there is a version conflict (bulk)`, async () => {
const esError = { type: 'version_conflict_engine_exception' };
await bulkUpdateError(obj, esError, expectErrorConflict(obj));
});
it(`returns error when document is missing (bulk)`, async () => {
const esError = { type: 'document_missing_exception' };
await bulkUpdateError(obj, esError, expectErrorNotFound(obj));
});
it(`returns error reason for other errors (bulk)`, async () => {
const esError = { reason: 'some_other_error' };
await bulkUpdateError(obj, esError, expectErrorResult(obj, { message: esError.reason }));
});
it(`returns error string for other errors if no reason is defined (bulk)`, async () => {
const esError = { foo: 'some_other_error' };
const expectedError = expectErrorResult(obj, { message: JSON.stringify(esError) });
await bulkUpdateError(obj, esError, expectedError);
it(`returns bulk error`, async () => {
const expectedErrorResult = { type: obj.type, id: obj.id, error: 'Oh no, a bulk error!' };
await bulkUpdateError(obj, true, expectedErrorResult);
});
});
@ -3898,352 +3725,6 @@ describe('SavedObjectsRepository', () => {
});
});
describe('#deleteFromNamespaces', () => {
const id = 'some-id';
const type = MULTI_NAMESPACE_TYPE;
const namespace1 = 'default';
const namespace2 = 'foo-namespace';
const namespace3 = 'bar-namespace';
const mockGetResponse = (type, id, namespaces) => {
// mock a document that exists in two namespaces
const mockResponse = getMockGetResponse({ type, id });
mockResponse._source.namespaces = namespaces;
client.get.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse)
);
};
const deleteFromNamespacesSuccess = async (
type,
id,
namespaces,
currentNamespaces,
options
) => {
mockGetResponse(type, id, currentNamespaces);
client.delete.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
_id: `${type}:${id}`,
...mockVersionProps,
result: 'deleted',
})
);
client.update.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
_id: `${type}:${id}`,
...mockVersionProps,
result: 'updated',
})
);
return await savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options);
};
describe('client calls', () => {
describe('delete action', () => {
const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => {
const test = async (namespaces) => {
await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options);
expectFn();
client.delete.mockClear();
client.get.mockClear();
};
await test([namespace1]);
await test([namespace1, namespace2]);
};
it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => {
const expectFn = () => {
expect(client.delete).toHaveBeenCalledTimes(1);
expect(client.get).toHaveBeenCalledTimes(1);
};
await deleteFromNamespacesSuccessDelete(expectFn);
});
it(`formats the ES requests`, async () => {
const expectFn = () => {
expect(client.delete).toHaveBeenCalledWith(
expect.objectContaining({
id: `${type}:${id}`,
}),
expect.anything()
);
const versionProperties = {
if_seq_no: mockVersionProps._seq_no,
if_primary_term: mockVersionProps._primary_term,
};
expect(client.delete).toHaveBeenCalledWith(
expect.objectContaining({
id: `${type}:${id}`,
...versionProperties,
}),
expect.anything()
);
};
await deleteFromNamespacesSuccessDelete(expectFn);
});
it(`defaults to a refresh setting of wait_for`, async () => {
await deleteFromNamespacesSuccessDelete(() =>
expect(client.delete).toHaveBeenCalledWith(
expect.objectContaining({
refresh: 'wait_for',
}),
expect.anything()
)
);
});
it(`should use default index`, async () => {
const expectFn = () =>
expect(client.delete).toHaveBeenCalledWith(
expect.objectContaining({ index: '.kibana-test' }),
expect.anything()
);
await deleteFromNamespacesSuccessDelete(expectFn);
});
it(`should use custom index`, async () => {
const expectFn = () =>
expect(client.delete).toHaveBeenCalledWith(
expect.objectContaining({ index: 'custom' }),
expect.anything()
);
await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE);
});
});
describe('update action', () => {
const deleteFromNamespacesSuccessUpdate = async (expectFn, options, _type = type) => {
const test = async (remaining) => {
const currentNamespaces = [namespace1].concat(remaining);
await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options);
expectFn();
client.get.mockClear();
client.update.mockClear();
};
await test([namespace2]);
await test([namespace2, namespace3]);
};
it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => {
const expectFn = () => {
expect(client.update).toHaveBeenCalledTimes(1);
expect(client.get).toHaveBeenCalledTimes(1);
};
await deleteFromNamespacesSuccessUpdate(expectFn);
});
it(`formats the ES requests`, async () => {
let ctr = 0;
const expectFn = () => {
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({
id: `${type}:${id}`,
}),
expect.anything()
);
const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3];
const versionProperties = {
if_seq_no: mockVersionProps._seq_no,
if_primary_term: mockVersionProps._primary_term,
};
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({
id: `${type}:${id}`,
...versionProperties,
body: { doc: { ...mockTimestampFields, namespaces } },
}),
expect.anything()
);
};
await deleteFromNamespacesSuccessUpdate(expectFn);
});
it(`defaults to a refresh setting of wait_for`, async () => {
const expectFn = () =>
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({
refresh: 'wait_for',
}),
expect.anything()
);
await deleteFromNamespacesSuccessUpdate(expectFn);
});
it(`should use default index`, async () => {
const expectFn = () =>
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({ index: '.kibana-test' }),
expect.anything()
);
await deleteFromNamespacesSuccessUpdate(expectFn);
});
it(`should use custom index`, async () => {
const expectFn = () =>
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({ index: 'custom' }),
expect.anything()
);
await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE);
});
});
});
describe('errors', () => {
const expectNotFoundError = async (type, id, namespaces, options) => {
await expect(
savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options)
).rejects.toThrowError(createGenericNotFoundError(type, id));
};
const expectBadRequestError = async (type, id, namespaces, message) => {
await expect(
savedObjectsRepository.deleteFromNamespaces(type, id, namespaces)
).rejects.toThrowError(createBadRequestError(message));
};
it(`throws when type is invalid`, async () => {
await expectNotFoundError('unknownType', id, [namespace1, namespace2]);
expect(client.delete).not.toHaveBeenCalled();
expect(client.update).not.toHaveBeenCalled();
});
it(`throws when type is hidden`, async () => {
await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]);
expect(client.delete).not.toHaveBeenCalled();
expect(client.update).not.toHaveBeenCalled();
});
it(`throws when type is not shareable`, async () => {
const test = async (type) => {
const message = `${type} doesn't support multiple namespaces`;
await expectBadRequestError(type, id, [namespace1, namespace2], message);
expect(client.delete).not.toHaveBeenCalled();
expect(client.update).not.toHaveBeenCalled();
};
await test('index-pattern');
await test(MULTI_NAMESPACE_ISOLATED_TYPE);
await test(NAMESPACE_AGNOSTIC_TYPE);
});
it(`throws when namespaces is an empty array`, async () => {
const test = async (namespaces) => {
const message = 'namespaces must be a non-empty array of strings';
await expectBadRequestError(type, id, namespaces, message);
expect(client.delete).not.toHaveBeenCalled();
expect(client.update).not.toHaveBeenCalled();
};
await test([]);
});
it(`throws when ES is unable to find the document during get`, async () => {
client.get.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false })
);
await expectNotFoundError(type, id, [namespace1, namespace2]);
expect(client.get).toHaveBeenCalledTimes(1);
});
it(`throws when ES is unable to find the index during get`, async () => {
client.get.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
);
await expectNotFoundError(type, id, [namespace1, namespace2]);
expect(client.get).toHaveBeenCalledTimes(1);
});
it(`throws when the document exists, but not in this namespace`, async () => {
mockGetResponse(type, id, [namespace1]);
await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' });
expect(client.get).toHaveBeenCalledTimes(1);
});
it(`throws when ES is unable to find the document during delete`, async () => {
mockGetResponse(type, id, [namespace1]);
client.delete.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' })
);
await expectNotFoundError(type, id, [namespace1]);
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.delete).toHaveBeenCalledTimes(1);
});
it(`throws when ES is unable to find the index during delete`, async () => {
mockGetResponse(type, id, [namespace1]);
client.delete.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
error: { type: 'index_not_found_exception' },
})
);
await expectNotFoundError(type, id, [namespace1]);
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.delete).toHaveBeenCalledTimes(1);
});
it(`throws when ES returns an unexpected response`, async () => {
mockGetResponse(type, id, [namespace1]);
client.delete.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
result: 'something unexpected',
})
);
await expect(
savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1])
).rejects.toThrowError('Unexpected Elasticsearch DELETE response');
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.delete).toHaveBeenCalledTimes(1);
});
it(`throws when ES is unable to find the document during update`, async () => {
mockGetResponse(type, id, [namespace1, namespace2]);
client.update.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 })
);
await expectNotFoundError(type, id, [namespace1]);
expect(client.get).toHaveBeenCalledTimes(1);
expect(client.update).toHaveBeenCalledTimes(1);
});
});
describe('returns', () => {
it(`returns an empty namespaces array on success (delete)`, async () => {
const test = async (namespaces) => {
const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces);
expect(result).toEqual({ namespaces: [] });
client.delete.mockClear();
};
await test([namespace1]);
await test([namespace1, namespace2]);
});
it(`returns remaining namespaces on success (update)`, async () => {
const test = async (remaining) => {
const currentNamespaces = [namespace1].concat(remaining);
const result = await deleteFromNamespacesSuccess(
type,
id,
[namespace1],
currentNamespaces
);
expect(result).toEqual({ namespaces: remaining });
client.delete.mockClear();
};
await test([namespace2]);
await test([namespace2, namespace3]);
});
it(`succeeds when the document doesn't exist in all of the targeted namespaces`, async () => {
const namespaces = [namespace2];
const currentNamespaces = [namespace1];
const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces);
expect(result).toEqual({ namespaces: currentNamespaces });
});
});
});
describe('#update', () => {
const id = 'logstash-*';
const type = 'index-pattern';
@ -4722,4 +4203,65 @@ describe('SavedObjectsRepository', () => {
);
});
});
describe('#collectMultiNamespaceReferences', () => {
afterEach(() => {
mockCollectMultiNamespaceReferences.mockReset();
});
it('passes arguments to the collectMultiNamespaceReferences module and returns the result', async () => {
const objects = Symbol();
const expectedResult = Symbol();
mockCollectMultiNamespaceReferences.mockResolvedValue(expectedResult);
await expect(
savedObjectsRepository.collectMultiNamespaceReferences(objects)
).resolves.toEqual(expectedResult);
expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledTimes(1);
expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledWith(
expect.objectContaining({ objects })
);
});
it('returns an error from the collectMultiNamespaceReferences module', async () => {
const expectedResult = new Error('Oh no!');
mockCollectMultiNamespaceReferences.mockRejectedValue(expectedResult);
await expect(savedObjectsRepository.collectMultiNamespaceReferences([])).rejects.toEqual(
expectedResult
);
});
});
describe('#updateObjectsSpaces', () => {
afterEach(() => {
mockUpdateObjectsSpaces.mockReset();
});
it('passes arguments to the updateObjectsSpaces module and returns the result', async () => {
const objects = Symbol();
const spacesToAdd = Symbol();
const spacesToRemove = Symbol();
const options = Symbol();
const expectedResult = Symbol();
mockUpdateObjectsSpaces.mockResolvedValue(expectedResult);
await expect(
savedObjectsRepository.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)
).resolves.toEqual(expectedResult);
expect(mockUpdateObjectsSpaces).toHaveBeenCalledTimes(1);
expect(mockUpdateObjectsSpaces).toHaveBeenCalledWith(
expect.objectContaining({ objects, spacesToAdd, spacesToRemove, options })
);
});
it('returns an error from the updateObjectsSpaces module', async () => {
const expectedResult = new Error('Oh no!');
mockUpdateObjectsSpaces.mockRejectedValue(expectedResult);
await expect(savedObjectsRepository.updateObjectsSpaces([], [], [])).rejects.toEqual(
expectedResult
);
});
});
});

View file

@ -6,6 +6,36 @@
* Side Public License, v 1.
*/
import type { collectMultiNamespaceReferences } from './collect_multi_namespace_references';
import type * as InternalUtils from './internal_utils';
import type { updateObjectsSpaces } from './update_objects_spaces';
export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction<
typeof collectMultiNamespaceReferences
>;
jest.mock('./collect_multi_namespace_references', () => ({
collectMultiNamespaceReferences: mockCollectMultiNamespaceReferences,
}));
export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction<
typeof InternalUtils['getBulkOperationError']
>;
jest.mock('./internal_utils', () => {
const actual = jest.requireActual('./internal_utils');
return {
...actual,
getBulkOperationError: mockGetBulkOperationError,
};
});
export const mockUpdateObjectsSpaces = jest.fn() as jest.MockedFunction<typeof updateObjectsSpaces>;
jest.mock('./update_objects_spaces', () => ({
updateObjectsSpaces: mockUpdateObjectsSpaces,
}));
export const pointInTimeFinderMock = jest.fn();
jest.doMock('./point_in_time_finder', () => ({
PointInTimeFinder: pointInTimeFinderMock,

View file

@ -48,10 +48,6 @@ import {
SavedObjectsBulkUpdateObject,
SavedObjectsBulkUpdateOptions,
SavedObjectsDeleteOptions,
SavedObjectsAddToNamespacesOptions,
SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsDeleteFromNamespacesResponse,
SavedObjectsRemoveReferencesToOptions,
SavedObjectsRemoveReferencesToResponse,
SavedObjectsResolveResponse,
@ -64,15 +60,31 @@ import {
MutatingOperationRefreshSetting,
} from '../../types';
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { validateConvertFilterToKueryNode } from './filter_utils';
import { validateAndConvertAggregations } from './aggregations';
import {
getBulkOperationError,
getExpectedVersionProperties,
getSavedObjectFromSource,
rawDocExistsInNamespace,
} from './internal_utils';
import {
ALL_NAMESPACES_STRING,
FIND_DEFAULT_PAGE,
FIND_DEFAULT_PER_PAGE,
SavedObjectsUtils,
} from './utils';
import {
collectMultiNamespaceReferences,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
} from './collect_multi_namespace_references';
import {
updateObjectsSpaces,
SavedObjectsUpdateObjectsSpacesObject,
SavedObjectsUpdateObjectsSpacesOptions,
} from './update_objects_spaces';
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
@ -95,7 +107,7 @@ export interface SavedObjectsRepositoryOptions {
index: string;
mappings: IndexMapping;
client: ElasticsearchClient;
typeRegistry: SavedObjectTypeRegistry;
typeRegistry: ISavedObjectTypeRegistry;
serializer: SavedObjectsSerializer;
migrator: IKibanaMigrator;
allowedTypes: string[];
@ -134,7 +146,7 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp
refresh?: boolean;
}
const DEFAULT_REFRESH_SETTING = 'wait_for';
export const DEFAULT_REFRESH_SETTING = 'wait_for';
/**
* See {@link SavedObjectsRepository}
@ -160,7 +172,7 @@ export class SavedObjectsRepository {
private _migrator: IKibanaMigrator;
private _index: string;
private _mappings: IndexMapping;
private _registry: SavedObjectTypeRegistry;
private _registry: ISavedObjectTypeRegistry;
private _allowedTypes: string[];
private readonly client: RepositoryEsClient;
private _serializer: SavedObjectsSerializer;
@ -176,7 +188,7 @@ export class SavedObjectsRepository {
*/
public static createRepository(
migrator: IKibanaMigrator,
typeRegistry: SavedObjectTypeRegistry,
typeRegistry: ISavedObjectTypeRegistry,
indexName: string,
client: ElasticsearchClient,
logger: Logger,
@ -511,16 +523,11 @@ export class SavedObjectsRepository {
}
const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value;
const { error, ...rawResponse } = Object.values(
bulkResponse?.body.items[esRequestIndex] ?? {}
)[0] as any;
const rawResponse = Object.values(bulkResponse?.body.items[esRequestIndex] ?? {})[0] as any;
const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse);
if (error) {
return {
id: requestedId,
type: rawMigratedDoc._source.type,
error: getBulkOperationError(error, rawMigratedDoc._source.type, requestedId),
};
return { type: rawMigratedDoc._source.type, id: requestedId, error };
}
// When method == 'index' the bulkResponse doesn't include the indexed
@ -989,7 +996,7 @@ export class SavedObjectsRepository {
}
// @ts-expect-error MultiGetHit._source is optional
return this.getSavedObjectFromSource(type, id, doc);
return getSavedObjectFromSource(this._registry, type, id, doc);
}),
};
}
@ -1033,7 +1040,7 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return this.getSavedObjectFromSource(type, id, body);
return getSavedObjectFromSource(this._registry, type, id, body);
}
/**
@ -1138,20 +1145,25 @@ export class SavedObjectsRepository {
if (foundExactMatch && foundAliasMatch) {
return {
// @ts-expect-error MultiGetHit._source is optional
saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc),
saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc),
outcome: 'conflict',
aliasTargetId: legacyUrlAlias.targetId,
};
} else if (foundExactMatch) {
return {
// @ts-expect-error MultiGetHit._source is optional
saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc),
saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc),
outcome: 'exactMatch',
};
} else if (foundAliasMatch) {
return {
// @ts-expect-error MultiGetHit._source is optional
saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc),
saved_object: getSavedObjectFromSource(
this._registry,
type,
legacyUrlAlias.targetId,
// @ts-expect-error MultiGetHit._source is optional
aliasMatchDoc
),
outcome: 'aliasMatch',
aliasTargetId: legacyUrlAlias.targetId,
};
@ -1263,169 +1275,52 @@ export class SavedObjectsRepository {
}
/**
* Adds one or more namespaces to a given multi-namespace saved object. This method and
* [`deleteFromNamespaces`]{@link SavedObjectsRepository.deleteFromNamespaces} are the only ways to change which Spaces a multi-namespace
* saved object is shared to.
* Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace
* type.
*
* @param objects The objects to get the references for.
*/
async addToNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
): Promise<SavedObjectsAddToNamespacesResponse> {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
if (!this._registry.isShareable(type)) {
throw SavedObjectsErrorHelpers.createBadRequestError(
`${type} doesn't support multiple namespaces`
);
}
if (!namespaces.length) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'namespaces must be a non-empty array of strings'
);
}
const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options;
// we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used
const rawId = this._serializer.generateRawId(undefined, type, id);
const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace);
const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult);
// there should never be a case where a multi-namespace object does not have any existing namespaces
// however, it is a possibility if someone manually modifies the document in Elasticsearch
const time = this._getCurrentTime();
const doc = {
updated_at: time,
namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces,
};
const { statusCode } = await this.client.update(
{
id: rawId,
index: this.getIndexForType(type),
...getExpectedVersionProperties(version, preflightResult),
refresh,
body: {
doc,
},
},
{ ignore: [404] }
);
if (statusCode === 404) {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return { namespaces: doc.namespaces };
async collectMultiNamespaceReferences(
objects: SavedObjectsCollectMultiNamespaceReferencesObject[],
options?: SavedObjectsCollectMultiNamespaceReferencesOptions
) {
return collectMultiNamespaceReferences({
registry: this._registry,
allowedTypes: this._allowedTypes,
client: this.client,
serializer: this._serializer,
getIndexForType: this.getIndexForType.bind(this),
createPointInTimeFinder: this.createPointInTimeFinder.bind(this),
objects,
options,
});
}
/**
* Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted
* entirely. This method and [`addToNamespaces`]{@link SavedObjectsRepository.addToNamespaces} are the only ways to change which Spaces a
* multi-namespace saved object is shared to.
* Updates one or more objects to add and/or remove them from specified spaces.
*
* @param objects
* @param spacesToAdd
* @param spacesToRemove
* @param options
*/
async deleteFromNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
): Promise<SavedObjectsDeleteFromNamespacesResponse> {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
if (!this._registry.isShareable(type)) {
throw SavedObjectsErrorHelpers.createBadRequestError(
`${type} doesn't support multiple namespaces`
);
}
if (!namespaces.length) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'namespaces must be a non-empty array of strings'
);
}
const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options;
// we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used
const rawId = this._serializer.generateRawId(undefined, type, id);
const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace);
const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult);
// if there are somehow no existing namespaces, allow the operation to proceed and delete this saved object
const remainingNamespaces = existingNamespaces?.filter((x) => !namespaces.includes(x));
if (remainingNamespaces?.length) {
// if there is 1 or more namespace remaining, update the saved object
const time = this._getCurrentTime();
const doc = {
updated_at: time,
namespaces: remainingNamespaces,
};
const { statusCode } = await this.client.update(
{
id: rawId,
index: this.getIndexForType(type),
...getExpectedVersionProperties(undefined, preflightResult),
refresh,
body: {
doc,
},
},
{
ignore: [404],
}
);
if (statusCode === 404) {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return { namespaces: doc.namespaces };
} else {
// if there are no namespaces remaining, delete the saved object
const { body, statusCode } = await this.client.delete(
{
id: this._serializer.generateRawId(undefined, type, id),
refresh,
...getExpectedVersionProperties(undefined, preflightResult),
index: this.getIndexForType(type),
},
{
ignore: [404],
}
);
const deleted = body.result === 'deleted';
if (deleted) {
return { namespaces: [] };
}
const deleteDocNotFound = body.result === 'not_found';
// @ts-expect-error
const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception';
if (deleteDocNotFound || deleteIndexNotFound) {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
throw new Error(
`Unexpected Elasticsearch DELETE response: ${JSON.stringify({
type,
id,
response: { body, statusCode },
})}`
);
}
async updateObjectsSpaces(
objects: SavedObjectsUpdateObjectsSpacesObject[],
spacesToAdd: string[],
spacesToRemove: string[],
options?: SavedObjectsUpdateObjectsSpacesOptions
) {
return updateObjectsSpaces({
registry: this._registry,
allowedTypes: this._allowedTypes,
client: this.client,
serializer: this._serializer,
getIndexForType: this.getIndexForType.bind(this),
objects,
spacesToAdd,
spacesToRemove,
options,
});
}
/**
@ -1617,21 +1512,19 @@ export class SavedObjectsRepository {
const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value;
const response = bulkUpdateResponse?.body.items[esRequestIndex] ?? {};
const rawResponse = Object.values(response)[0] as any;
const error = getBulkOperationError(type, id, rawResponse);
if (error) {
return { type, id, error };
}
// When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the
// returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer.
const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values(
response
)[0] as any;
const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { [type]: attributes, references, updated_at } = documentToSave;
if (error) {
return {
id,
type,
error: getBulkOperationError(error, type, id),
};
}
const { originId } = get._source;
return {
@ -2055,10 +1948,10 @@ export class SavedObjectsRepository {
* }
* ```
*/
createPointInTimeFinder(
createPointInTimeFinder<T = unknown, A = unknown>(
findOptions: SavedObjectsCreatePointInTimeFinderOptions,
dependencies?: SavedObjectsCreatePointInTimeFinderDependencies
): ISavedObjectsPointInTimeFinder {
): ISavedObjectsPointInTimeFinder<T, A> {
return new PointInTimeFinder(findOptions, {
logger: this._logger,
client: this,
@ -2108,28 +2001,8 @@ export class SavedObjectsRepository {
return omit(savedObject, ['namespace']) as SavedObject<T>;
}
/**
* Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as
* we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the
* document's `namespaces` value includes the string representation of the given namespace.
*
* WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID
* format mentioned above do not apply.
*/
private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace?: string) {
const rawDocType = raw._source.type;
// if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees
// of the document ID format and don't need to check this
if (!this._registry.isMultiNamespace(rawDocType)) {
return true;
}
const namespaces = raw._source.namespaces;
const existsInNamespace =
namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) ||
namespaces?.includes('*');
return existsInNamespace ?? false;
private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) {
return rawDocExistsInNamespace(this._registry, raw, namespace);
}
/**
@ -2204,34 +2077,6 @@ export class SavedObjectsRepository {
return body;
}
private getSavedObjectFromSource<T>(
type: string,
id: string,
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource }
): SavedObject<T> {
const { originId, updated_at: updatedAt } = doc._source;
let namespaces: string[] = [];
if (!this._registry.isNamespaceAgnostic(type)) {
namespaces = doc._source.namespaces ?? [
SavedObjectsUtils.namespaceIdToString(doc._source.namespace),
];
}
return {
id,
type,
namespaces,
...(originId && { originId }),
...(updatedAt && { updated_at: updatedAt }),
version: encodeHitVersion(doc),
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
coreMigrationVersion: doc._source.coreMigrationVersion,
};
}
private async resolveExactMatch<T>(
type: string,
id: string,
@ -2242,43 +2087,6 @@ export class SavedObjectsRepository {
}
}
function getBulkOperationError(
error: { type: string; reason?: string; index?: string },
type: string,
id: string
) {
switch (error.type) {
case 'version_conflict_engine_exception':
return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id));
case 'document_missing_exception':
return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id));
case 'index_not_found_exception':
return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!));
default:
return {
message: error.reason || JSON.stringify(error),
};
}
}
/**
* Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control.
*
* @param version Optional version specified by the consumer.
* @param document Optional existing document that was obtained in a preflight operation.
*/
function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) {
if (version) {
return decodeRequestVersion(version);
} else if (document) {
return {
if_seq_no: document._seq_no,
if_primary_term: document._primary_term,
};
}
return {};
}
/**
* Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the
* current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal

View file

@ -0,0 +1,29 @@
/*
* 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 type * as InternalUtils from './internal_utils';
export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction<
typeof InternalUtils['getBulkOperationError']
>;
export const mockGetExpectedVersionProperties = jest.fn() as jest.MockedFunction<
typeof InternalUtils['getExpectedVersionProperties']
>;
export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction<
typeof InternalUtils['rawDocExistsInNamespace']
>;
jest.mock('./internal_utils', () => {
const actual = jest.requireActual('./internal_utils');
return {
...actual,
getBulkOperationError: mockGetBulkOperationError,
getExpectedVersionProperties: mockGetExpectedVersionProperties,
rawDocExistsInNamespace: mockRawDocExistsInNamespace,
};
});

View file

@ -0,0 +1,453 @@
/*
* 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 {
mockGetBulkOperationError,
mockGetExpectedVersionProperties,
mockRawDocExistsInNamespace,
} from './update_objects_spaces.test.mock';
import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest';
import type { ElasticsearchClient } from 'src/core/server/elasticsearch';
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
import { SavedObjectsSerializer } from '../../serialization';
import type {
SavedObjectsUpdateObjectsSpacesObject,
UpdateObjectsSpacesParams,
} from './update_objects_spaces';
import { updateObjectsSpaces } from './update_objects_spaces';
type SetupParams = Partial<
Pick<UpdateObjectsSpacesParams, 'objects' | 'spacesToAdd' | 'spacesToRemove' | 'options'>
>;
const EXISTING_SPACE = 'existing-space';
const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 };
const EXPECTED_VERSION_PROPS = { if_seq_no: 1, if_primary_term: 1 };
const BULK_ERROR = {
error: 'Oh no, a bulk error!',
type: 'error_type',
message: 'error_message',
statusCode: 400,
};
const SHAREABLE_OBJ_TYPE = 'type-a';
const NON_SHAREABLE_OBJ_TYPE = 'type-b';
const SHAREABLE_HIDDEN_OBJ_TYPE = 'type-c';
const mockCurrentTime = new Date('2021-05-01T10:20:30Z');
beforeAll(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(mockCurrentTime);
});
beforeEach(() => {
mockGetExpectedVersionProperties.mockReturnValue(EXPECTED_VERSION_PROPS);
mockRawDocExistsInNamespace.mockReset();
mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default
});
afterAll(() => {
jest.useRealTimers();
});
describe('#updateObjectsSpaces', () => {
let client: DeeplyMockedKeys<ElasticsearchClient>;
/** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `updateObjectsSpaces` */
function setup({ objects = [], spacesToAdd = [], spacesToRemove = [], options }: SetupParams) {
const registry = typeRegistryMock.create();
registry.isShareable.mockImplementation(
(type) => [SHAREABLE_OBJ_TYPE, SHAREABLE_HIDDEN_OBJ_TYPE].includes(type) // NON_SHAREABLE_OBJ_TYPE is excluded
);
client = elasticsearchClientMock.createElasticsearchClient();
const serializer = new SavedObjectsSerializer(registry);
return {
registry,
allowedTypes: [SHAREABLE_OBJ_TYPE, NON_SHAREABLE_OBJ_TYPE], // SHAREABLE_HIDDEN_OBJ_TYPE is excluded
client,
serializer,
getIndexForType: (type: string) => `index-for-${type}`,
objects,
spacesToAdd,
spacesToRemove,
options,
};
}
/** Mocks the saved objects client so it returns the expected results */
function mockMgetResults(...results: Array<{ found: boolean }>) {
client.mget.mockReturnValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
docs: results.map((x) =>
x.found
? {
_id: 'doesnt-matter',
_index: 'doesnt-matter',
_source: { namespaces: [EXISTING_SPACE] },
...VERSION_PROPS,
found: true,
}
: {
_id: 'doesnt-matter',
_index: 'doesnt-matter',
found: false,
}
),
})
);
}
/** Asserts that mget is called for the given objects */
function expectMgetArgs(...objects: SavedObjectsUpdateObjectsSpacesObject[]) {
const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` }));
expect(client.mget).toHaveBeenCalledWith({ body: { docs } }, expect.anything());
}
/** Mocks the saved objects client so it returns the expected results */
function mockBulkResults(...results: Array<{ error: boolean }>) {
results.forEach(({ error }) => {
if (error) {
mockGetBulkOperationError.mockReturnValueOnce(BULK_ERROR);
} else {
mockGetBulkOperationError.mockReturnValueOnce(undefined);
}
});
client.bulk.mockReturnValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise({
items: results.map(() => ({})), // as long as the result does not contain an error field, it is treated as a success
errors: false,
took: 0,
})
);
}
/** Asserts that mget is called for the given objects */
function expectBulkArgs(
...objectActions: Array<{
object: { type: string; id: string; namespaces?: string[] };
action: 'update' | 'delete';
}>
) {
const body = objectActions.flatMap(
({ object: { type, id, namespaces = expect.any(Array) }, action }) => {
const operation = {
[action]: {
_id: `${type}:${id}`,
_index: `index-for-${type}`,
...EXPECTED_VERSION_PROPS,
},
};
return action === 'update'
? [operation, { doc: { namespaces, updated_at: mockCurrentTime.toISOString() } }] // 'update' uses an operation and document metadata
: [operation]; // 'delete' only uses an operation
}
);
expect(client.bulk).toHaveBeenCalledWith(expect.objectContaining({ body }));
}
beforeEach(() => {
mockGetBulkOperationError.mockReset(); // reset calls and return undefined by default
});
describe('errors', () => {
it('throws when spacesToAdd and spacesToRemove are empty', async () => {
const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }];
const params = setup({ objects });
await expect(() => updateObjectsSpaces(params)).rejects.toThrow(
'spacesToAdd and/or spacesToRemove must be a non-empty array of strings: Bad Request'
);
});
it('throws when spacesToAdd and spacesToRemove intersect', async () => {
const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }];
const spacesToAdd = ['foo-space', 'bar-space'];
const spacesToRemove = ['bar-space', 'baz-space'];
const params = setup({ objects, spacesToAdd, spacesToRemove });
await expect(() => updateObjectsSpaces(params)).rejects.toThrow(
'spacesToAdd and spacesToRemove cannot contain any of the same strings: Bad Request'
);
});
it('throws when mget cluster call fails', async () => {
const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }];
const spacesToAdd = ['foo-space'];
const params = setup({ objects, spacesToAdd });
client.mget.mockReturnValueOnce(
elasticsearchClientMock.createErrorTransportRequestPromise(new Error('mget error'))
);
await expect(() => updateObjectsSpaces(params)).rejects.toThrow('mget error');
});
it('throws when bulk cluster call fails', async () => {
const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }];
const spacesToAdd = ['foo-space'];
const params = setup({ objects, spacesToAdd });
mockMgetResults({ found: true });
client.bulk.mockReturnValueOnce(
elasticsearchClientMock.createErrorTransportRequestPromise(new Error('bulk error'))
);
await expect(() => updateObjectsSpaces(params)).rejects.toThrow('bulk error');
});
it('returns mix of type errors, mget/bulk cluster errors, and successes', async () => {
const obj1 = { type: SHAREABLE_HIDDEN_OBJ_TYPE, id: 'id-1' }; // invalid type (Not Found)
const obj2 = { type: NON_SHAREABLE_OBJ_TYPE, id: 'id-2' }; // non-shareable type (Bad Request)
// obj3 below is mocking an example where a SOC wrapper attempted to retrieve it in a pre-flight request but it was not found.
// Since it has 'spaces: []', that indicates it should be skipped for cluster calls and just returned as a Not Found error.
// Realistically this would not be intermingled with other requested objects that do not have 'spaces' arrays, but it's fine for this
// specific test case.
const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [] }; // does not exist (Not Found)
const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4' }; // mget error (found but doesn't exist in the current space)
const obj5 = { type: SHAREABLE_OBJ_TYPE, id: 'id-5' }; // mget error (Not Found)
const obj6 = { type: SHAREABLE_OBJ_TYPE, id: 'id-6' }; // bulk error (mocked as BULK_ERROR)
const obj7 = { type: SHAREABLE_OBJ_TYPE, id: 'id-7' }; // success
const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7];
const spacesToAdd = ['foo-space'];
const params = setup({ objects, spacesToAdd });
mockMgetResults({ found: true }, { found: false }, { found: true }, { found: true }); // results for obj4, obj5, obj6, and obj7
mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4
mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj6
mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj7
mockBulkResults({ error: true }, { error: false }); // results for obj6 and obj7
const result = await updateObjectsSpaces(params);
expect(client.mget).toHaveBeenCalledTimes(1);
expectMgetArgs(obj4, obj5, obj6, obj7);
expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3);
expect(client.bulk).toHaveBeenCalledTimes(1);
expectBulkArgs({ action: 'update', object: obj6 }, { action: 'update', object: obj7 });
expect(result.objects).toEqual([
{ ...obj1, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) },
{ ...obj2, spaces: [], error: expect.objectContaining({ error: 'Bad Request' }) },
{ ...obj3, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) },
{ ...obj4, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) },
{ ...obj5, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) },
{ ...obj6, spaces: [], error: BULK_ERROR },
{ ...obj7, spaces: [EXISTING_SPACE, 'foo-space'] },
]);
});
});
// Note: these test cases do not include requested objects that will result in errors (those are covered above)
describe('cluster and module calls', () => {
it('mget call skips objects that have "spaces" defined', async () => {
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2' }; // will be passed to mget
const objects = [obj1, obj2];
const spacesToAdd = ['foo-space'];
const params = setup({ objects, spacesToAdd });
mockMgetResults({ found: true }); // result for obj2
mockBulkResults({ error: false }, { error: false }); // results for obj1 and obj2
await updateObjectsSpaces(params);
expect(client.mget).toHaveBeenCalledTimes(1);
expectMgetArgs(obj2);
});
it('does not call mget if all objects have "spaces" defined', async () => {
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved
const objects = [obj1];
const spacesToAdd = ['foo-space'];
const params = setup({ objects, spacesToAdd });
mockBulkResults({ error: false }); // result for obj1
await updateObjectsSpaces(params);
expect(client.mget).not.toHaveBeenCalled();
});
describe('bulk call skips objects that will not be changed', () => {
it('when adding spaces', async () => {
const space1 = 'space-to-add';
const space2 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated
const objects = [obj1, obj2];
const spacesToAdd = [space1];
const params = setup({ objects, spacesToAdd });
// this test case does not call mget
mockBulkResults({ error: false }); // result for obj2
await updateObjectsSpaces(params);
expect(client.bulk).toHaveBeenCalledTimes(1);
expectBulkArgs({
action: 'update',
object: { ...obj2, namespaces: [space2, space1] },
});
});
it('when removing spaces', async () => {
const space1 = 'space-to-remove';
const space2 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1
const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left)
const objects = [obj1, obj2, obj3];
const spacesToRemove = [space1];
const params = setup({ objects, spacesToRemove });
// this test case does not call mget
mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3
await updateObjectsSpaces(params);
expect(client.bulk).toHaveBeenCalledTimes(1);
expectBulkArgs(
{ action: 'update', object: { ...obj2, namespaces: [space2] } },
{ action: 'delete', object: obj3 }
);
});
it('when adding and removing spaces', async () => {
const space1 = 'space-to-add';
const space2 = 'space-to-remove';
const space3 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1
const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2
const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2
const objects = [obj1, obj2, obj3, obj4];
const spacesToAdd = [space1];
const spacesToRemove = [space2];
const params = setup({ objects, spacesToAdd, spacesToRemove });
// this test case does not call mget
mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4
await updateObjectsSpaces(params);
expect(client.bulk).toHaveBeenCalledTimes(1);
expectBulkArgs(
{ action: 'update', object: { ...obj2, namespaces: [space3, space1] } },
{ action: 'update', object: { ...obj3, namespaces: [space1] } },
{ action: 'update', object: { ...obj4, namespaces: [space3, space1] } }
);
});
});
describe('does not call bulk if all objects do not need to be changed', () => {
it('when adding spaces', async () => {
const space = 'space-to-add';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space] }; // will not be changed
const objects = [obj1];
const spacesToAdd = [space];
const params = setup({ objects, spacesToAdd });
// this test case does not call mget or bulk
await updateObjectsSpaces(params);
expect(client.bulk).not.toHaveBeenCalled();
});
it('when removing spaces', async () => {
const space1 = 'space-to-remove';
const space2 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed
const objects = [obj1];
const spacesToRemove = [space1];
const params = setup({ objects, spacesToRemove });
// this test case does not call mget or bulk
await updateObjectsSpaces(params);
expect(client.bulk).not.toHaveBeenCalled();
});
it('when adding and removing spaces', async () => {
const space1 = 'space-to-add';
const space2 = 'space-to-remove';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed
const objects = [obj1];
const spacesToAdd = [space1];
const spacesToRemove = [space2];
const params = setup({ objects, spacesToAdd, spacesToRemove });
// this test case does not call mget or bulk
await updateObjectsSpaces(params);
expect(client.bulk).not.toHaveBeenCalled();
});
});
});
describe('returns expected results', () => {
it('when adding spaces', async () => {
const space1 = 'space-to-add';
const space2 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated
const objects = [obj1, obj2];
const spacesToAdd = [space1];
const params = setup({ objects, spacesToAdd });
// this test case does not call mget
mockBulkResults({ error: false }); // result for obj2
const result = await updateObjectsSpaces(params);
expect(result.objects).toEqual([
{ ...obj1, spaces: [space1] },
{ ...obj2, spaces: [space2, space1] },
]);
});
it('when removing spaces', async () => {
const space1 = 'space-to-remove';
const space2 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1
const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left)
const objects = [obj1, obj2, obj3];
const spacesToRemove = [space1];
const params = setup({ objects, spacesToRemove });
// this test case does not call mget
mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3
const result = await updateObjectsSpaces(params);
expect(result.objects).toEqual([
{ ...obj1, spaces: [space2] },
{ ...obj2, spaces: [space2] },
{ ...obj3, spaces: [] },
]);
});
it('when adding and removing spaces', async () => {
const space1 = 'space-to-add';
const space2 = 'space-to-remove';
const space3 = 'other-space';
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1
const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2
const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2
const objects = [obj1, obj2, obj3, obj4];
const spacesToAdd = [space1];
const spacesToRemove = [space2];
const params = setup({ objects, spacesToAdd, spacesToRemove });
// this test case does not call mget
mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4
const result = await updateObjectsSpaces(params);
expect(result.objects).toEqual([
{ ...obj1, spaces: [space1] },
{ ...obj2, spaces: [space3, space1] },
{ ...obj3, spaces: [space1] },
{ ...obj4, spaces: [space3, space1] },
]);
});
});
});

View file

@ -0,0 +1,315 @@
/*
* 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 type { BulkOperationContainer, MultiGetOperation } from '@elastic/elasticsearch/api/types';
import intersection from 'lodash/intersection';
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import type { SavedObjectsRawDocSource, SavedObjectsSerializer } from '../../serialization';
import type {
MutatingOperationRefreshSetting,
SavedObjectError,
SavedObjectsBaseOptions,
} from '../../types';
import type { DecoratedError } from './errors';
import { SavedObjectsErrorHelpers } from './errors';
import {
getBulkOperationError,
getExpectedVersionProperties,
rawDocExistsInNamespace,
} from './internal_utils';
import { DEFAULT_REFRESH_SETTING } from './repository';
import type { RepositoryEsClient } from './repository_es_client';
/**
* An object that should have its spaces updated.
*
* @public
*/
export interface SavedObjectsUpdateObjectsSpacesObject {
/** The type of the object to update */
id: string;
/** The ID of the object to update */
type: string;
/**
* The space(s) that the object to update currently exists in. This is only intended to be used by SOC wrappers.
*
* @internal
*/
spaces?: string[];
/**
* The version of the object to update; this is used for optimistic concurrency control. This is only intended to be used by SOC wrappers.
*
* @internal
*/
version?: string;
}
/**
* Options for the update operation.
*
* @public
*/
export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions {
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
}
/**
* The response when objects' spaces are updated.
*
* @public
*/
export interface SavedObjectsUpdateObjectsSpacesResponse {
objects: SavedObjectsUpdateObjectsSpacesResponseObject[];
}
/**
* Details about a specific object's update result.
*
* @public
*/
export interface SavedObjectsUpdateObjectsSpacesResponseObject {
/** The type of the referenced object */
type: string;
/** The ID of the referenced object */
id: string;
/** The space(s) that the referenced object exists in */
spaces: string[];
/** Included if there was an error updating this object's spaces */
error?: SavedObjectError;
}
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type Left = { tag: 'Left'; error: SavedObjectsUpdateObjectsSpacesResponseObject };
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type Right = { tag: 'Right'; value: Record<string, any> };
type Either = Left | Right;
const isLeft = (either: Either): either is Left => either.tag === 'Left';
const isRight = (either: Either): either is Right => either.tag === 'Right';
/**
* Parameters for the updateObjectsSpaces function.
*
* @internal
*/
export interface UpdateObjectsSpacesParams {
registry: ISavedObjectTypeRegistry;
allowedTypes: string[];
client: RepositoryEsClient;
serializer: SavedObjectsSerializer;
getIndexForType: (type: string) => string;
objects: SavedObjectsUpdateObjectsSpacesObject[];
spacesToAdd: string[];
spacesToRemove: string[];
options?: SavedObjectsUpdateObjectsSpacesOptions;
}
/**
* Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace
* type.
*/
export async function updateObjectsSpaces({
registry,
allowedTypes,
client,
serializer,
getIndexForType,
objects,
spacesToAdd,
spacesToRemove,
options = {},
}: UpdateObjectsSpacesParams): Promise<SavedObjectsUpdateObjectsSpacesResponse> {
if (!spacesToAdd.length && !spacesToRemove.length) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'spacesToAdd and/or spacesToRemove must be a non-empty array of strings'
);
}
if (intersection(spacesToAdd, spacesToRemove).length > 0) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'spacesToAdd and spacesToRemove cannot contain any of the same strings'
);
}
const { namespace } = options;
let bulkGetRequestIndexCounter = 0;
const expectedBulkGetResults: Either[] = objects.map((object) => {
const { type, id, spaces, version } = object;
if (!allowedTypes.includes(type)) {
const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id));
return {
tag: 'Left' as 'Left',
error: { id, type, spaces: [], error },
};
}
if (!registry.isShareable(type)) {
const error = errorContent(
SavedObjectsErrorHelpers.createBadRequestError(
`${type} doesn't support multiple namespaces`
)
);
return {
tag: 'Left' as 'Left',
error: { id, type, spaces: [], error },
};
}
return {
tag: 'Right' as 'Right',
value: {
type,
id,
spaces,
version,
...(!spaces && { esRequestIndex: bulkGetRequestIndexCounter++ }),
},
};
});
const bulkGetDocs = expectedBulkGetResults.reduce<MultiGetOperation[]>((acc, x) => {
if (isRight(x) && x.value.esRequestIndex !== undefined) {
acc.push({
_id: serializer.generateRawId(undefined, x.value.type, x.value.id),
_index: getIndexForType(x.value.type),
_source: ['type', 'namespaces'],
});
}
return acc;
}, []);
const bulkGetResponse = bulkGetDocs.length
? await client.mget<SavedObjectsRawDocSource>(
{ body: { docs: bulkGetDocs } },
{ ignore: [404] }
)
: undefined;
const time = new Date().toISOString();
let bulkOperationRequestIndexCounter = 0;
const bulkOperationParams: BulkOperationContainer[] = [];
const expectedBulkOperationResults: Either[] = expectedBulkGetResults.map(
(expectedBulkGetResult) => {
if (isLeft(expectedBulkGetResult)) {
return expectedBulkGetResult;
}
const { id, type, spaces, version, esRequestIndex } = expectedBulkGetResult.value;
let currentSpaces: string[] = spaces;
let versionProperties;
if (esRequestIndex !== undefined) {
const doc = bulkGetResponse?.body.docs[esRequestIndex];
// @ts-expect-error MultiGetHit._source is optional
if (!doc?.found || !rawDocExistsInNamespace(registry, doc, namespace)) {
const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id));
return {
tag: 'Left' as 'Left',
error: { id, type, spaces: [], error },
};
}
currentSpaces = doc._source?.namespaces ?? [];
// @ts-expect-error MultiGetHit._source is optional
versionProperties = getExpectedVersionProperties(version, doc);
} else if (spaces?.length === 0) {
// A SOC wrapper attempted to retrieve this object in a pre-flight request and it was not found.
const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id));
return {
tag: 'Left' as 'Left',
error: { id, type, spaces: [], error },
};
} else {
versionProperties = getExpectedVersionProperties(version);
}
const { newSpaces, isUpdateRequired } = getNewSpacesArray(
currentSpaces,
spacesToAdd,
spacesToRemove
);
const expectedResult = {
type,
id,
newSpaces,
...(isUpdateRequired && { esRequestIndex: bulkOperationRequestIndexCounter++ }),
};
if (isUpdateRequired) {
const documentMetadata = {
_id: serializer.generateRawId(undefined, type, id),
_index: getIndexForType(type),
...versionProperties,
};
if (newSpaces.length) {
const documentToSave = { updated_at: time, namespaces: newSpaces };
// @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional
bulkOperationParams.push({ update: documentMetadata }, { doc: documentToSave });
} else {
// @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional
bulkOperationParams.push({ delete: documentMetadata });
}
}
return { tag: 'Right' as 'Right', value: expectedResult };
}
);
const { refresh = DEFAULT_REFRESH_SETTING } = options;
const bulkOperationResponse = bulkOperationParams.length
? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true })
: undefined;
return {
objects: expectedBulkOperationResults.map<SavedObjectsUpdateObjectsSpacesResponseObject>(
(expectedResult) => {
if (isLeft(expectedResult)) {
return expectedResult.error;
}
const { type, id, newSpaces, esRequestIndex } = expectedResult.value;
if (esRequestIndex !== undefined) {
const response = bulkOperationResponse?.body.items[esRequestIndex] ?? {};
const rawResponse = Object.values(response)[0] as any;
const error = getBulkOperationError(type, id, rawResponse);
if (error) {
return { id, type, spaces: [], error };
}
}
return { id, type, spaces: newSpaces };
}
),
};
}
/** Extracts the contents of a decorated error to return the attributes for bulk operations. */
function errorContent(error: DecoratedError) {
return error.output.payload;
}
/** Gets the remaining spaces for an object after adding new ones and removing old ones. */
function getNewSpacesArray(
existingSpaces: string[],
spacesToAdd: string[],
spacesToRemove: string[]
) {
const addSet = new Set(spacesToAdd);
const removeSet = new Set(spacesToRemove);
const newSpaces = existingSpaces
.filter((x) => {
addSet.delete(x);
return !removeSet.delete(x);
})
.concat(Array.from(addSet));
const isAnySpaceAdded = addSet.size > 0;
const isAnySpaceRemoved = removeSet.size < spacesToRemove.length;
const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved;
return { newSpaces, isUpdateRequired };
}

View file

@ -26,9 +26,9 @@ const create = () => {
openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }),
resolve: jest.fn(),
update: jest.fn(),
addToNamespaces: jest.fn(),
deleteFromNamespaces: jest.fn(),
removeReferencesTo: jest.fn(),
collectMultiNamespaceReferences: jest.fn(),
updateObjectsSpaces: jest.fn(),
} as unknown) as jest.Mocked<SavedObjectsClientContract>;
mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({

View file

@ -237,52 +237,39 @@ test(`#bulkUpdate`, async () => {
expect(result).toBe(returnValue);
});
test(`#addToNamespaces`, async () => {
test(`#collectMultiNamespaceReferences`, async () => {
const returnValue = Symbol();
const mockRepository = {
addToNamespaces: jest.fn().mockResolvedValue(returnValue),
collectMultiNamespaceReferences: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const namespaces = Symbol();
const objects = Symbol();
const options = Symbol();
const result = await client.addToNamespaces(type, id, namespaces, options);
const result = await client.collectMultiNamespaceReferences(objects, options);
expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options);
expect(mockRepository.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options);
expect(result).toBe(returnValue);
});
test(`#deleteFromNamespaces`, async () => {
test(`#updateObjectsSpaces`, async () => {
const returnValue = Symbol();
const mockRepository = {
deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue),
updateObjectsSpaces: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const namespaces = Symbol();
const objects = Symbol();
const spacesToAdd = Symbol();
const spacesToRemove = Symbol();
const options = Symbol();
const result = await client.deleteFromNamespaces(type, id, namespaces, options);
const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options);
expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options);
expect(result).toBe(returnValue);
});
test(`#removeReferencesTo`, async () => {
const returnValue = Symbol();
const mockRepository = {
removeReferencesTo: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const options = Symbol();
const result = await client.removeReferencesTo(type, id, options);
expect(mockRepository.removeReferencesTo).toHaveBeenCalledWith(type, id, options);
expect(mockRepository.updateObjectsSpaces).toHaveBeenCalledWith(
objects,
spacesToAdd,
spacesToRemove,
options
);
expect(result).toBe(returnValue);
});

View file

@ -11,6 +11,11 @@ import type {
ISavedObjectsPointInTimeFinder,
SavedObjectsCreatePointInTimeFinderOptions,
SavedObjectsCreatePointInTimeFinderDependencies,
SavedObjectsCollectMultiNamespaceReferencesObject,
SavedObjectsCollectMultiNamespaceReferencesOptions,
SavedObjectsCollectMultiNamespaceReferencesResponse,
SavedObjectsUpdateObjectsSpacesObject,
SavedObjectsUpdateObjectsSpacesOptions,
} from './lib';
import {
SavedObject,
@ -218,44 +223,6 @@ export interface SavedObjectsUpdateOptions<Attributes = unknown> extends SavedOb
upsert?: Attributes;
}
/**
*
* @public
*/
export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions {
/** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */
version?: string;
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
*/
export interface SavedObjectsAddToNamespacesResponse {
/** The namespaces the object exists in after this operation is complete. */
namespaces: string[];
}
/**
*
* @public
*/
export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions {
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
*/
export interface SavedObjectsDeleteFromNamespacesResponse {
/** The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. */
namespaces: string[];
}
/**
*
* @public
@ -536,40 +503,6 @@ export class SavedObjectsClient {
return await this._repository.update(type, id, attributes, options);
}
/**
* Adds namespaces to a SavedObject
*
* @param type
* @param id
* @param namespaces
* @param options
*/
async addToNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
): Promise<SavedObjectsAddToNamespacesResponse> {
return await this._repository.addToNamespaces(type, id, namespaces, options);
}
/**
* Removes namespaces from a SavedObject
*
* @param type
* @param id
* @param namespaces
* @param options
*/
async deleteFromNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
): Promise<SavedObjectsDeleteFromNamespacesResponse> {
return await this._repository.deleteFromNamespaces(type, id, namespaces, options);
}
/**
* Bulk Updates multiple SavedObject at once
*
@ -665,14 +598,49 @@ export class SavedObjectsClient {
* }
* ```
*/
createPointInTimeFinder(
createPointInTimeFinder<T = unknown, A = unknown>(
findOptions: SavedObjectsCreatePointInTimeFinderOptions,
dependencies?: SavedObjectsCreatePointInTimeFinderDependencies
): ISavedObjectsPointInTimeFinder {
): ISavedObjectsPointInTimeFinder<T, A> {
return this._repository.createPointInTimeFinder(findOptions, {
client: this,
// Include dependencies last so that SO client wrappers have their settings applied.
...dependencies,
});
}
/**
* Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type.
*
* @param objects
* @param options
*/
async collectMultiNamespaceReferences(
objects: SavedObjectsCollectMultiNamespaceReferencesObject[],
options?: SavedObjectsCollectMultiNamespaceReferencesOptions
): Promise<SavedObjectsCollectMultiNamespaceReferencesResponse> {
return await this._repository.collectMultiNamespaceReferences(objects, options);
}
/**
* Updates one or more objects to add and/or remove them from specified spaces.
*
* @param objects
* @param spacesToAdd
* @param spacesToRemove
* @param options
*/
async updateObjectsSpaces(
objects: SavedObjectsUpdateObjectsSpacesObject[],
spacesToAdd: string[],
spacesToRemove: string[],
options?: SavedObjectsUpdateObjectsSpacesOptions
) {
return await this._repository.updateObjectsSpaces(
objects,
spacesToAdd,
spacesToRemove,
options
);
}
}

View file

@ -1255,9 +1255,9 @@ export type ISavedObjectsExporter = PublicMethodsOf<SavedObjectsExporter>;
export type ISavedObjectsImporter = PublicMethodsOf<SavedObjectsImporter>;
// @public (undocumented)
export interface ISavedObjectsPointInTimeFinder {
export interface ISavedObjectsPointInTimeFinder<T, A> {
close: () => Promise<void>;
find: () => AsyncGenerator<SavedObjectsFindResponse>;
find: () => AsyncGenerator<SavedObjectsFindResponse<T, A>>;
}
// @public
@ -2144,6 +2144,7 @@ export type SavedObjectAttributeSingle = string | number | boolean | null | unde
// @public (undocumented)
export interface SavedObjectExportBaseOptions {
excludeExportDetails?: boolean;
includeNamespaces?: boolean;
includeReferencesDeep?: boolean;
namespace?: string;
request: KibanaRequest;
@ -2175,15 +2176,18 @@ export interface SavedObjectReference {
type: string;
}
// @public (undocumented)
export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
version?: string;
}
// @public (undocumented)
export interface SavedObjectsAddToNamespacesResponse {
namespaces: string[];
// @public
export interface SavedObjectReferenceWithContext {
id: string;
inboundReferences: Array<{
type: string;
id: string;
name: string;
}>;
isMissing?: boolean;
spaces: string[];
spacesWithMatchingAliases?: string[];
type: string;
}
// Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts
@ -2277,16 +2281,15 @@ export interface SavedObjectsCheckConflictsResponse {
export class SavedObjectsClient {
// @internal
constructor(repository: ISavedObjectsRepository);
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
bulkCreate<T = unknown>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsCheckConflictsResponse>;
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise<SavedObjectsClosePointInTimeResponse>;
collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise<SavedObjectsCollectMultiNamespaceReferencesResponse>;
create<T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder;
createPointInTimeFinder<T = unknown, A = unknown>(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder<T, A>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
// (undocumented)
static errors: typeof SavedObjectsErrorHelpers;
// (undocumented)
@ -2297,6 +2300,7 @@ export class SavedObjectsClient {
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions<T>): Promise<SavedObjectsUpdateResponse<T>>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise<import("./lib").SavedObjectsUpdateObjectsSpacesResponse>;
}
// @public
@ -2341,6 +2345,25 @@ export interface SavedObjectsClosePointInTimeResponse {
succeeded: boolean;
}
// @public
export interface SavedObjectsCollectMultiNamespaceReferencesObject {
// (undocumented)
id: string;
// (undocumented)
type: string;
}
// @public
export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions {
purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces';
}
// @public
export interface SavedObjectsCollectMultiNamespaceReferencesResponse {
// (undocumented)
objects: SavedObjectReferenceWithContext[];
}
// @public
export interface SavedObjectsComplexFieldMapping {
// (undocumented)
@ -2401,16 +2424,6 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp
refresh?: boolean;
}
// @public (undocumented)
export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
}
// @public (undocumented)
export interface SavedObjectsDeleteFromNamespacesResponse {
namespaces: string[];
}
// @public (undocumented)
export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions {
force?: boolean;
@ -2884,21 +2897,20 @@ export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBase
// @public (undocumented)
export class SavedObjectsRepository {
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
bulkCreate<T = unknown>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsCheckConflictsResponse>;
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise<SavedObjectsClosePointInTimeResponse>;
collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise<import("./collect_multi_namespace_references").SavedObjectsCollectMultiNamespaceReferencesResponse>;
create<T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder;
createPointInTimeFinder<T = unknown, A = unknown>(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder<T, A>;
// Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts
//
// @internal
static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
static createRepository(migrator: IKibanaMigrator, typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise<any>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<SavedObjectsDeleteFromNamespacesResponse>;
// (undocumented)
find<T = unknown, A = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T, A>>;
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
@ -2907,6 +2919,7 @@ export class SavedObjectsRepository {
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions<T>): Promise<SavedObjectsUpdateResponse<T>>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise<import("./update_objects_spaces").SavedObjectsUpdateObjectsSpacesResponse>;
}
// @public
@ -2938,7 +2951,7 @@ export class SavedObjectsSerializer {
generateRawId(namespace: string | undefined, type: string, id: string): string;
generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string;
isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean;
rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc;
rawToSavedObject<T = unknown>(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc<T>;
savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc;
}
@ -3004,6 +3017,35 @@ export interface SavedObjectsTypeMappingDefinition {
properties: SavedObjectsMappingProperties;
}
// @public
export interface SavedObjectsUpdateObjectsSpacesObject {
id: string;
// @internal
spaces?: string[];
type: string;
// @internal
version?: string;
}
// @public
export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
}
// @public
export interface SavedObjectsUpdateObjectsSpacesResponse {
// (undocumented)
objects: SavedObjectsUpdateObjectsSpacesResponseObject[];
}
// @public
export interface SavedObjectsUpdateObjectsSpacesResponseObject {
error?: SavedObjectError;
id: string;
spaces: string[];
type: string;
}
// @public (undocumented)
export interface SavedObjectsUpdateOptions<Attributes = unknown> extends SavedObjectsBaseOptions {
references?: SavedObjectReference[];

Some files were not shown because too many files have changed in this diff Show more