mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
parent
104fdb8f63
commit
73d1f644cd
118 changed files with 4924 additions and 875 deletions
|
@ -10,6 +10,8 @@ The following saved objects APIs are available:
|
|||
|
||||
* <<saved-objects-api-get, Get object API>> to retrieve a single {kib} saved object by ID
|
||||
|
||||
* <<saved-objects-api-resolve, Resolve object API>> to retrieve a single {kib} saved object by ID, using any legacy URL alias if it exists
|
||||
|
||||
* <<saved-objects-api-bulk-get, Bulk get objects API>> to retrieve multiple {kib} saved objects by ID
|
||||
|
||||
* <<saved-objects-api-find, Find objects API>> to retrieve a paginated set of {kib} saved objects by various conditions
|
||||
|
@ -40,4 +42,5 @@ include::saved-objects/delete.asciidoc[]
|
|||
include::saved-objects/export.asciidoc[]
|
||||
include::saved-objects/import.asciidoc[]
|
||||
include::saved-objects/resolve_import_errors.asciidoc[]
|
||||
include::saved-objects/resolve.asciidoc[]
|
||||
include::saved-objects/rotate_encryption_key.asciidoc[]
|
||||
|
|
|
@ -80,7 +80,7 @@ The API returns the following:
|
|||
"title": "[Flights] Global Flight Dashboard",
|
||||
"hits": 0,
|
||||
"description": "Analyze mock flight data for ES-Air, Logstash Airways, Kibana Airlines and JetBeats",
|
||||
"panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":32,\"h\":7,\"i\":\"1\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_0\"},{\"panelIndex\":\"3\",\"gridData\":{\"x\":17,\"y\":7,\"w\":23,\"h\":12,\"i\":\"3\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Average Ticket Price\":\"#0A50A1\",\"Flight Count\":\"#82B5D8\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_1\"},{\"panelIndex\":\"4\",\"gridData\":{\"x\":0,\"y\":85,\"w\":48,\"h\":15,\"i\":\"4\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_2\"},{\"panelIndex\":\"5\",\"gridData\":{\"x\":0,\"y\":7,\"w\":17,\"h\":12,\"i\":\"5\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"ES-Air\":\"#447EBC\",\"JetBeats\":\"#65C5DB\",\"Kibana Airlines\":\"#BA43A9\",\"Logstash Airways\":\"#E5AC0E\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_3\"},{\"panelIndex\":\"6\",\"gridData\":{\"x\":24,\"y\":33,\"w\":24,\"h\":14,\"i\":\"6\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Carrier Delay\":\"#5195CE\",\"Late Aircraft Delay\":\"#1F78C1\",\"NAS Delay\":\"#70DBED\",\"No Delay\":\"#BADFF4\",\"Security Delay\":\"#052B51\",\"Weather Delay\":\"#6ED0E0\"}}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_4\"},{\"panelIndex\":\"7\",\"gridData\":{\"x\":24,\"y\":19,\"w\":24,\"h\":14,\"i\":\"7\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_5\"},{\"panelIndex\":\"10\",\"gridData\":{\"x\":0,\"y\":35,\"w\":24,\"h\":12,\"i\":\"10\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_6\"},{\"panelIndex\":\"13\",\"gridData\":{\"x\":10,\"y\":19,\"w\":14,\"h\":8,\"i\":\"13\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_7\"},{\"panelIndex\":\"14\",\"gridData\":{\"x\":10,\"y\":27,\"w\":14,\"h\":8,\"i\":\"14\"},\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_8\"},{\"panelIndex\":\"18\",\"gridData\":{\"x\":24,\"y\":70,\"w\":24,\"h\":15,\"i\":\"18\"},\"embeddableConfig\":{\"mapCenter\":[27.421687059550266,15.371002131141724],\"mapZoom\":1},\"version\":\"6.3.0\",\"panelRefName\":\"panel_9\"},{\"panelIndex\":\"21\",\"gridData\":{\"x\":0,\"y\":62,\"w\":48,\"h\":8,\"i\":\"21\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_10\"},{\"panelIndex\":\"22\",\"gridData\":{\"x\":32,\"y\":0,\"w\":16,\"h\":7,\"i\":\"22\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_11\"},{\"panelIndex\":\"23\",\"gridData\":{\"x\":0,\"y\":70,\"w\":24,\"h\":15,\"i\":\"23\"},\"embeddableConfig\":{\"mapCenter\":[42.19556096274418,9.536742995308601e-7],\"mapZoom\":1},\"version\":\"6.3.0\",\"panelRefName\":\"panel_12\"},{\"panelIndex\":\"25\",\"gridData\":{\"x\":0,\"y\":19,\"w\":10,\"h\":8,\"i\":\"25\"},\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(247,251,255)\",\"100 - 150\":\"rgb(107,174,214)\",\"150 - 200\":\"rgb(33,113,181)\",\"200 - 250\":\"rgb(8,48,107)\",\"50 - 100\":\"rgb(198,219,239)\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_13\"},{\"panelIndex\":\"27\",\"gridData\":{\"x\":0,\"y\":27,\"w\":10,\"h\":8,\"i\":\"27\"},\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(247,251,255)\",\"100 - 150\":\"rgb(107,174,214)\",\"150 - 200\":\"rgb(33,113,181)\",\"200 - 250\":\"rgb(8,48,107)\",\"50 - 100\":\"rgb(198,219,239)\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_14\"},{\"panelIndex\":\"28\",\"gridData\":{\"x\":0,\"y\":47,\"w\":24,\"h\":15,\"i\":\"28\"},\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 -* Connection #0 to host 69c72adb58fa46c69a01afdf4a6cbfd3.us-west1.gcp.cloud.es.io left intact\n 11\":\"rgb(247,251,255)\",\"11 - 22\":\"rgb(208,225,242)\",\"22 - 33\":\"rgb(148,196,223)\",\"33 - 44\":\"rgb(74,152,201)\",\"44 - 55\":\"rgb(23,100,171)\"},\"legendOpen\":false}},\"version\":\"6.3.0\",\"panelRefName\":\"panel_15\"},{\"panelIndex\":\"29\",\"gridData\":{\"x\":40,\"y\":7,\"w\":8,\"h\":6,\"i\":\"29\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_16\"},{\"panelIndex\":\"30\",\"gridData\":{\"x\":40,\"y\":13,\"w\":8,\"h\":6,\"i\":\"30\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_17\"},{\"panelIndex\":\"31\",\"gridData\":{\"x\":24,\"y\":47,\"w\":24,\"h\":15,\"i\":\"31\"},\"embeddableConfig\":{},\"version\":\"6.3.0\",\"panelRefName\":\"panel_18\"}]",
|
||||
"panelsJSON": "[ . . . ]",
|
||||
"optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}",
|
||||
"version": 1,
|
||||
"timeRestore": true,
|
||||
|
|
130
docs/api/saved-objects/resolve.asciidoc
Normal file
130
docs/api/saved-objects/resolve.asciidoc
Normal file
|
@ -0,0 +1,130 @@
|
|||
[[saved-objects-api-resolve]]
|
||||
=== Resolve object API
|
||||
++++
|
||||
<titleabbrev>Resolve object</titleabbrev>
|
||||
++++
|
||||
|
||||
experimental[] Retrieve a single {kib} saved object by ID, using any legacy URL alias if it exists.
|
||||
|
||||
Under certain circumstances, when Kibana is upgraded, saved object migrations may necessitate regenerating some object IDs to enable new
|
||||
features. When an object's ID is regenerated, a legacy URL alias is created for that object, preserving its old ID. In such a scenario, that
|
||||
object can be retrieved via the Resolve API using either its new ID or its old ID.
|
||||
|
||||
[[saved-objects-api-resolve-request]]
|
||||
==== Request
|
||||
|
||||
`GET <kibana host>:<port>/api/saved_objects/resolve/<type>/<id>`
|
||||
|
||||
`GET <kibana host>:<port>/s/<space_id>/api/saved_objects/resolve/<type>/<id>`
|
||||
|
||||
[[saved-objects-api-resolve-params]]
|
||||
==== Path parameters
|
||||
|
||||
`space_id`::
|
||||
(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used.
|
||||
|
||||
|
||||
`type`::
|
||||
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
|
||||
|
||||
`id`::
|
||||
(Required, string) The ID of the object to retrieve.
|
||||
|
||||
[[saved-objects-api-resolve-codes]]
|
||||
==== Response code
|
||||
|
||||
`200`::
|
||||
Indicates a successful call.
|
||||
|
||||
[[saved-objects-api-resolve-example]]
|
||||
==== Example
|
||||
|
||||
Retrieve the index pattern object with the `my-pattern` ID:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X GET api/saved_objects/resolve/index-pattern/my-pattern
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
The API returns the following:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"saved_object": {
|
||||
"id": "my-pattern",
|
||||
"type": "index-pattern",
|
||||
"version": 1,
|
||||
"attributes": {
|
||||
"title": "my-pattern-*"
|
||||
}
|
||||
},
|
||||
"outcome": "exactMatch"
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
The `outcome` field may be any of the following:
|
||||
|
||||
* `"exactMatch"` -- One document exactly matched the given ID.
|
||||
* `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID.
|
||||
* `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.
|
||||
|
||||
Retrieve a dashboard object in the `testspace` by ID:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
$ curl -X GET s/testspace/api/saved_objects/resolve/dashboard/7adfa750-4c81-11e8-b3d7-01146121b73d
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
The API returns the following:
|
||||
|
||||
[source,sh]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"saved_object": {
|
||||
"id": "7adfa750-4c81-11e8-b3d7-01146121b73d",
|
||||
"type": "dashboard",
|
||||
"updated_at": "2019-07-23T00:11:07.059Z",
|
||||
"version": "WzQ0LDFd",
|
||||
"attributes": {
|
||||
"title": "[Flights] Global Flight Dashboard",
|
||||
"hits": 0,
|
||||
"description": "Analyze mock flight data for ES-Air, Logstash Airways, Kibana Airlines and JetBeats",
|
||||
"panelsJSON": "[ . . . ]",
|
||||
"optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}",
|
||||
"version": 1,
|
||||
"timeRestore": true,
|
||||
"timeTo": "now",
|
||||
"timeFrom": "now-24h",
|
||||
"refreshInterval": {
|
||||
"display": "15 minutes",
|
||||
"pause": false,
|
||||
"section": 2,
|
||||
"value": 900000
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
"id": "aeb212e0-4c84-11e8-b3d7-01146121b73d"
|
||||
},
|
||||
. . .
|
||||
{
|
||||
"name": "panel_18",
|
||||
"type": "visualization",
|
||||
"id": "ed78a660-53a0-11e8-acbd-0be0ad9d822b"
|
||||
}
|
||||
],
|
||||
"migrationVersion": {
|
||||
"dashboard": "7.0.0"
|
||||
}
|
||||
},
|
||||
"outcome": "conflict"
|
||||
}
|
||||
--------------------------------------------------
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [coreMigrationVersion](./kibana-plugin-core-public.savedobject.coremigrationversion.md)
|
||||
|
||||
## SavedObject.coreMigrationVersion property
|
||||
|
||||
A semver value that is used when upgrading objects between Kibana versions.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
coreMigrationVersion?: string;
|
||||
```
|
|
@ -15,6 +15,7 @@ export interface SavedObject<T = unknown>
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | <code>T</code> | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
|
||||
| [coreMigrationVersion](./kibana-plugin-core-public.savedobject.coremigrationversion.md) | <code>string</code> | A semver value that is used when upgrading objects between Kibana versions. |
|
||||
| [error](./kibana-plugin-core-public.savedobject.error.md) | <code>SavedObjectError</code> | |
|
||||
| [id](./kibana-plugin-core-public.savedobject.id.md) | <code>string</code> | The ID of this Saved Object, guaranteed to be unique for all objects of the same <code>type</code> |
|
||||
| [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) > [coreMigrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md)
|
||||
|
||||
## SavedObjectsCreateOptions.coreMigrationVersion property
|
||||
|
||||
A semver value that is used when upgrading objects between Kibana versions.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
coreMigrationVersion?: string;
|
||||
```
|
|
@ -15,6 +15,7 @@ export interface SavedObjectsCreateOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [coreMigrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md) | <code>string</code> | A semver value that is used when upgrading objects between Kibana versions. |
|
||||
| [id](./kibana-plugin-core-public.savedobjectscreateoptions.id.md) | <code>string</code> | (Not recommended) Specify an id instead of having the saved objects service generate one for you. |
|
||||
| [migrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
| [overwrite](./kibana-plugin-core-public.savedobjectscreateoptions.overwrite.md) | <code>boolean</code> | If a document with the given <code>id</code> already exists, overwrite it's contents (default=false). |
|
||||
|
|
|
@ -9,7 +9,7 @@ Constructs a new instance of the `SimpleSavedObject` class
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType<T>);
|
||||
constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, }: SavedObjectType<T>);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -17,5 +17,5 @@ constructor(client: SavedObjectsClientContract, { id, type, version, attributes,
|
|||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| client | <code>SavedObjectsClientContract</code> | |
|
||||
| { id, type, version, attributes, error, references, migrationVersion } | <code>SavedObjectType<T></code> | |
|
||||
| { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, } | <code>SavedObjectType<T></code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SimpleSavedObject](./kibana-plugin-core-public.simplesavedobject.md) > [coreMigrationVersion](./kibana-plugin-core-public.simplesavedobject.coremigrationversion.md)
|
||||
|
||||
## SimpleSavedObject.coreMigrationVersion property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion'];
|
||||
```
|
|
@ -18,7 +18,7 @@ export declare class SimpleSavedObject<T = unknown>
|
|||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(client, { id, type, version, attributes, error, references, migrationVersion })](./kibana-plugin-core-public.simplesavedobject._constructor_.md) | | Constructs a new instance of the <code>SimpleSavedObject</code> class |
|
||||
| [(constructor)(client, { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, })](./kibana-plugin-core-public.simplesavedobject._constructor_.md) | | Constructs a new instance of the <code>SimpleSavedObject</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
|
@ -26,6 +26,7 @@ export declare class SimpleSavedObject<T = unknown>
|
|||
| --- | --- | --- | --- |
|
||||
| [\_version](./kibana-plugin-core-public.simplesavedobject._version.md) | | <code>SavedObjectType<T>['version']</code> | |
|
||||
| [attributes](./kibana-plugin-core-public.simplesavedobject.attributes.md) | | <code>T</code> | |
|
||||
| [coreMigrationVersion](./kibana-plugin-core-public.simplesavedobject.coremigrationversion.md) | | <code>SavedObjectType<T>['coreMigrationVersion']</code> | |
|
||||
| [error](./kibana-plugin-core-public.simplesavedobject.error.md) | | <code>SavedObjectType<T>['error']</code> | |
|
||||
| [id](./kibana-plugin-core-public.simplesavedobject.id.md) | | <code>SavedObjectType<T>['id']</code> | |
|
||||
| [migrationVersion](./kibana-plugin-core-public.simplesavedobject.migrationversion.md) | | <code>SavedObjectType<T>['migrationVersion']</code> | |
|
||||
|
|
|
@ -139,7 +139,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectAttributes](./kibana-plugin-core-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
|
||||
| [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) | |
|
||||
| [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.<!-- -->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. |
|
||||
| [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) | |
|
||||
|
@ -188,10 +188,12 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) | |
|
||||
| [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
| [SavedObjectsRawDoc](./kibana-plugin-core-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. |
|
||||
| [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) | Options that can be specified when using the saved objects serializer to parse a raw document. |
|
||||
| [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md) | |
|
||||
| [SavedObjectsRemoveReferencesToResponse](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md) | |
|
||||
| [SavedObjectsRepositoryFactory](./kibana-plugin-core-server.savedobjectsrepositoryfactory.md) | Factory provided when invoking a [client factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) See [SavedObjectsServiceSetup.setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) |
|
||||
| [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. |
|
||||
| [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) | |
|
||||
| [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for registering Saved Object types, creating and registering Saved Object client wrappers and factories. |
|
||||
| [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. |
|
||||
| [SavedObjectStatusMeta](./kibana-plugin-core-server.savedobjectstatusmeta.md) | Meta information about the SavedObjectService's status. Available to plugins via [CoreSetup.status](./kibana-plugin-core-server.coresetup.status.md)<!-- -->. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [coreMigrationVersion](./kibana-plugin-core-server.savedobject.coremigrationversion.md)
|
||||
|
||||
## SavedObject.coreMigrationVersion property
|
||||
|
||||
A semver value that is used when upgrading objects between Kibana versions.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
coreMigrationVersion?: string;
|
||||
```
|
|
@ -15,6 +15,7 @@ export interface SavedObject<T = unknown>
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | <code>T</code> | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
|
||||
| [coreMigrationVersion](./kibana-plugin-core-server.savedobject.coremigrationversion.md) | <code>string</code> | A semver value that is used when upgrading objects between Kibana versions. |
|
||||
| [error](./kibana-plugin-core-server.savedobject.error.md) | <code>SavedObjectError</code> | |
|
||||
| [id](./kibana-plugin-core-server.savedobject.id.md) | <code>string</code> | The ID of this Saved Object, guaranteed to be unique for all objects of the same <code>type</code> |
|
||||
| [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## SavedObjectMigrationMap interface
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) > [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md)
|
||||
|
||||
## SavedObjectsBulkCreateObject.coreMigrationVersion property
|
||||
|
||||
A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
coreMigrationVersion?: string;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion` field set and you want to create it again.
|
||||
|
|
@ -16,6 +16,7 @@ export interface SavedObjectsBulkCreateObject<T = unknown>
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | <code>T</code> | |
|
||||
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | <code>string</code> | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
|
||||
| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | <code>string</code> | |
|
||||
| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | <code>string[]</code> | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md)<!-- -->.<!-- -->Note: this can only be used for multi-namespace object types. |
|
||||
| [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
|
|
|
@ -36,5 +36,6 @@ The constructor for this class is marked as internal. Third-party code should no
|
|||
| [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 |
|
||||
| [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 |
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [resolve](./kibana-plugin-core-server.savedobjectsclient.resolve.md)
|
||||
|
||||
## SavedObjectsClient.resolve() method
|
||||
|
||||
Resolves a single object, using any legacy URL alias if it exists
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| type | <code>string</code> | The type of SavedObject to retrieve |
|
||||
| id | <code>string</code> | The ID of the SavedObject to retrieve |
|
||||
| options | <code>SavedObjectsBaseOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObjectsResolveResponse<T>>`
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) > [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md)
|
||||
|
||||
## SavedObjectsCreateOptions.coreMigrationVersion property
|
||||
|
||||
A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
coreMigrationVersion?: string;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion` field set and you want to create it again.
|
||||
|
|
@ -15,6 +15,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | <code>string</code> | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
|
||||
| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | <code>string</code> | (not recommended) Specify an id for the document |
|
||||
| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | <code>string[]</code> | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md)<!-- -->.<!-- -->Note: this can only be used for multi-namespace object types. |
|
||||
| [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md)
|
||||
|
||||
## SavedObjectsRawDocParseOptions interface
|
||||
|
||||
Options that can be specified when using the saved objects serializer to parse a raw document.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsRawDocParseOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [namespaceTreatment](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md) | <code>'strict' | 'lax'</code> | Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade migrations.<!-- -->If not specified, the default treatment is <code>strict</code>. |
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) > [namespaceTreatment](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md)
|
||||
|
||||
## SavedObjectsRawDocParseOptions.namespaceTreatment property
|
||||
|
||||
Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade migrations.
|
||||
|
||||
If not specified, the default treatment is `strict`<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
namespaceTreatment?: 'strict' | 'lax';
|
||||
```
|
|
@ -28,5 +28,6 @@ export declare class SavedObjectsRepository
|
|||
| [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. |
|
||||
| [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 |
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [resolve](./kibana-plugin-core-server.savedobjectsrepository.resolve.md)
|
||||
|
||||
## SavedObjectsRepository.resolve() method
|
||||
|
||||
Resolves a single object, using any legacy URL alias if it exists
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| type | <code>string</code> | |
|
||||
| id | <code>string</code> | |
|
||||
| options | <code>SavedObjectsBaseOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObjectsResolveResponse<T>>`
|
||||
|
||||
{<!-- -->promise<!-- -->} - { saved\_object, outcome }
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md)
|
||||
|
||||
## SavedObjectsResolveResponse interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsResolveResponse<T = unknown>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | <code>'exactMatch' | 'aliasMatch' | 'conflict'</code> | The outcome for a successful <code>resolve</code> call is one of the following values:<!-- -->\* <code>'exactMatch'</code> -- One document exactly matched the given ID. \* <code>'aliasMatch'</code> -- One document with a legacy URL alias matched the given ID; in this case the <code>saved_object.id</code> field is different than the given ID. \* <code>'conflict'</code> -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the <code>saved_object</code> object is the exact match, and the <code>saved_object.id</code> field is the same as the given ID. |
|
||||
| [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | <code>SavedObject<T></code> | |
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md)
|
||||
|
||||
## SavedObjectsResolveResponse.outcome property
|
||||
|
||||
The outcome for a successful `resolve` call is one of the following values:
|
||||
|
||||
\* `'exactMatch'` -- One document exactly matched the given ID. \* `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID. \* `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md)
|
||||
|
||||
## SavedObjectsResolveResponse.saved\_object property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
saved_object: SavedObject<T>;
|
||||
```
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsSerializer](./kibana-plugin-core-server.savedobjectsserializer.md) > [generateRawLegacyUrlAliasId](./kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md)
|
||||
|
||||
## SavedObjectsSerializer.generateRawLegacyUrlAliasId() method
|
||||
|
||||
Given a saved object type and id, generates the compound id that is stored in the raw document for its legacy URL alias.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| namespace | <code>string</code> | |
|
||||
| type | <code>string</code> | |
|
||||
| id | <code>string</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`string`
|
||||
|
|
@ -9,14 +9,15 @@ Determines whether or not the raw document can be converted to a saved object.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isRawSavedObject(rawDoc: SavedObjectsRawDoc): boolean;
|
||||
isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| rawDoc | <code>SavedObjectsRawDoc</code> | |
|
||||
| doc | <code>SavedObjectsRawDoc</code> | |
|
||||
| options | <code>SavedObjectsRawDocParseOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -23,7 +23,8 @@ The constructor for this class is marked as internal. Third-party code should no
|
|||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [generateRawId(namespace, type, id)](./kibana-plugin-core-server.savedobjectsserializer.generaterawid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document. |
|
||||
| [isRawSavedObject(rawDoc)](./kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. |
|
||||
| [rawToSavedObject(doc)](./kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. |
|
||||
| [generateRawLegacyUrlAliasId(namespace, type, id)](./kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document for its legacy URL alias. |
|
||||
| [isRawSavedObject(doc, options)](./kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. |
|
||||
| [rawToSavedObject(doc, options)](./kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. |
|
||||
| [savedObjectToRaw(savedObj)](./kibana-plugin-core-server.savedobjectsserializer.savedobjecttoraw.md) | | Converts a document from the saved object client format to the format that is stored in elasticsearch. |
|
||||
|
||||
|
|
|
@ -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): SavedObjectSanitizedDoc;
|
||||
rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -17,6 +17,7 @@ rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc;
|
|||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| doc | <code>SavedObjectsRawDoc</code> | |
|
||||
| options | <code>SavedObjectsRawDocParseOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md)
|
||||
|
||||
## SavedObjectsType.convertToMultiNamespaceTypeVersion property
|
||||
|
||||
If defined, objects of this type will be converted to multi-namespace objects when migrating to this version.
|
||||
|
||||
Requirements:
|
||||
|
||||
1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md)
|
||||
|
||||
Example of a single-namespace type in 7.10:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'foo',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: {...}
|
||||
}
|
||||
|
||||
```
|
||||
Example after converting to a multi-namespace type in 7.11:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'foo',
|
||||
hidden: false,
|
||||
namespaceType: 'multiple',
|
||||
mappings: {...},
|
||||
convertToMultiNamespaceTypeVersion: '7.11.0'
|
||||
}
|
||||
|
||||
```
|
||||
Note: a migration function can be optionally specified for the same version.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
convertToMultiNamespaceTypeVersion?: string;
|
||||
```
|
|
@ -19,6 +19,28 @@ This is only internal for now, and will only be public when we expose the regist
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [convertToAliasScript](./kibana-plugin-core-server.savedobjectstype.converttoaliasscript.md) | <code>string</code> | If defined, will be used to convert the type to an alias. |
|
||||
| [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md) | <code>string</code> | If defined, objects of this type will be converted to multi-namespace objects when migrating to this version.<!-- -->Requirements:<!-- -->1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md)<!-- -->Example of a single-namespace type in 7.10:
|
||||
```ts
|
||||
{
|
||||
name: 'foo',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: {...}
|
||||
}
|
||||
|
||||
```
|
||||
Example after converting to a multi-namespace type in 7.11:
|
||||
```ts
|
||||
{
|
||||
name: 'foo',
|
||||
hidden: false,
|
||||
namespaceType: 'multiple',
|
||||
mappings: {...},
|
||||
convertToMultiNamespaceTypeVersion: '7.11.0'
|
||||
}
|
||||
|
||||
```
|
||||
Note: a migration function can be optionally specified for the same version. |
|
||||
| [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | <code>boolean</code> | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an <code>extraType</code> when creating the repository.<!-- -->See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md)<!-- -->. |
|
||||
| [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | <code>string</code> | If defined, the type instances will be stored in the given index instead of the default one. |
|
||||
| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | <code>SavedObjectsTypeManagementDefinition</code> | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. |
|
||||
|
|
|
@ -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" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "resolve" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").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" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "resolve" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
|
||||
};
|
||||
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
|
||||
}`
|
||||
|
|
|
@ -194,6 +194,10 @@ Refer to the corresponding {es} logs for potential write errors.
|
|||
| `success` | User has accessed a saved object.
|
||||
| `failure` | User is not authorized to access a saved object.
|
||||
|
||||
.2+| `saved_object_resolve`
|
||||
| `success` | User has accessed a saved object.
|
||||
| `failure` | User is not authorized to access a saved object.
|
||||
|
||||
.2+| `saved_object_find`
|
||||
| `success` | User has accessed a saved object as part of a search operation.
|
||||
| `failure` | User is not authorized to search for saved objects.
|
||||
|
|
|
@ -1034,6 +1034,7 @@ export type PublicUiSettingsParams = Omit<UiSettingsParams, 'schema'>;
|
|||
// @public (undocumented)
|
||||
export interface SavedObject<T = unknown> {
|
||||
attributes: T;
|
||||
coreMigrationVersion?: string;
|
||||
// (undocumented)
|
||||
error?: SavedObjectError;
|
||||
id: string;
|
||||
|
@ -1151,6 +1152,7 @@ export type SavedObjectsClientContract = PublicMethodsOf<SavedObjectsClient>;
|
|||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsCreateOptions {
|
||||
coreMigrationVersion?: string;
|
||||
id?: string;
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
overwrite?: boolean;
|
||||
|
@ -1384,10 +1386,12 @@ export class ScopedHistory<HistoryLocationState = unknown> implements History<Hi
|
|||
|
||||
// @public
|
||||
export class SimpleSavedObject<T = unknown> {
|
||||
constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObject<T>);
|
||||
constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, }: SavedObject<T>);
|
||||
// (undocumented)
|
||||
attributes: T;
|
||||
// (undocumented)
|
||||
coreMigrationVersion: SavedObject<T>['coreMigrationVersion'];
|
||||
// (undocumented)
|
||||
delete(): Promise<{}>;
|
||||
// (undocumented)
|
||||
error: SavedObject<T>['error'];
|
||||
|
|
|
@ -38,6 +38,8 @@ export interface SavedObjectsCreateOptions {
|
|||
overwrite?: boolean;
|
||||
/** {@inheritDoc SavedObjectsMigrationVersion} */
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
/** A semver value that is used when upgrading objects between Kibana versions. */
|
||||
coreMigrationVersion?: string;
|
||||
references?: SavedObjectReference[];
|
||||
}
|
||||
|
||||
|
|
|
@ -27,12 +27,22 @@ export class SimpleSavedObject<T = unknown> {
|
|||
public id: SavedObjectType<T>['id'];
|
||||
public type: SavedObjectType<T>['type'];
|
||||
public migrationVersion: SavedObjectType<T>['migrationVersion'];
|
||||
public coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion'];
|
||||
public error: SavedObjectType<T>['error'];
|
||||
public references: SavedObjectType<T>['references'];
|
||||
|
||||
constructor(
|
||||
private client: SavedObjectsClientContract,
|
||||
{ id, type, version, attributes, error, references, migrationVersion }: SavedObjectType<T>
|
||||
{
|
||||
id,
|
||||
type,
|
||||
version,
|
||||
attributes,
|
||||
error,
|
||||
references,
|
||||
migrationVersion,
|
||||
coreMigrationVersion,
|
||||
}: SavedObjectType<T>
|
||||
) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
|
@ -40,6 +50,7 @@ export class SimpleSavedObject<T = unknown> {
|
|||
this.references = references || [];
|
||||
this._version = version;
|
||||
this.migrationVersion = migrationVersion;
|
||||
this.coreMigrationVersion = coreMigrationVersion;
|
||||
if (error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
@ -66,6 +77,7 @@ export class SimpleSavedObject<T = unknown> {
|
|||
} else {
|
||||
return this.client.create(this.type, this.attributes, {
|
||||
migrationVersion: this.migrationVersion,
|
||||
coreMigrationVersion: this.coreMigrationVersion,
|
||||
references: this.references,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ const createUsageStatsClientMock = () =>
|
|||
incrementSavedObjectsDelete: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsFind: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsGet: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsResolve: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsUpdate: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsImport: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null),
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
DELETE_STATS_PREFIX,
|
||||
FIND_STATS_PREFIX,
|
||||
GET_STATS_PREFIX,
|
||||
RESOLVE_STATS_PREFIX,
|
||||
UPDATE_STATS_PREFIX,
|
||||
IMPORT_STATS_PREFIX,
|
||||
RESOLVE_IMPORT_STATS_PREFIX,
|
||||
|
@ -594,6 +595,81 @@ describe('CoreUsageStatsClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsResolve', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await expect(
|
||||
usageStatsClient.incrementSavedObjectsResolve({
|
||||
request,
|
||||
} as BaseIncrementOptions)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await usageStatsClient.incrementSavedObjectsResolve({
|
||||
request,
|
||||
} as BaseIncrementOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_STATS_PREFIX}.namespace.default.total`,
|
||||
`${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options and the default namespace string appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders });
|
||||
await usageStatsClient.incrementSavedObjectsResolve({
|
||||
request,
|
||||
} as BaseIncrementOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_STATS_PREFIX}.namespace.default.total`,
|
||||
`${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles a non-default space appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup('foo');
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await usageStatsClient.incrementSavedObjectsResolve({
|
||||
request,
|
||||
} as BaseIncrementOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_STATS_PREFIX}.namespace.custom.total`,
|
||||
`${RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsUpdate', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
|
|
@ -40,6 +40,7 @@ export const CREATE_STATS_PREFIX = 'apiCalls.savedObjectsCreate';
|
|||
export const DELETE_STATS_PREFIX = 'apiCalls.savedObjectsDelete';
|
||||
export const FIND_STATS_PREFIX = 'apiCalls.savedObjectsFind';
|
||||
export const GET_STATS_PREFIX = 'apiCalls.savedObjectsGet';
|
||||
export const RESOLVE_STATS_PREFIX = 'apiCalls.savedObjectsResolve';
|
||||
export const UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsUpdate';
|
||||
export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport';
|
||||
export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors';
|
||||
|
@ -53,6 +54,7 @@ const ALL_COUNTER_FIELDS = [
|
|||
...getFieldsForCounter(DELETE_STATS_PREFIX),
|
||||
...getFieldsForCounter(FIND_STATS_PREFIX),
|
||||
...getFieldsForCounter(GET_STATS_PREFIX),
|
||||
...getFieldsForCounter(RESOLVE_STATS_PREFIX),
|
||||
...getFieldsForCounter(UPDATE_STATS_PREFIX),
|
||||
// Saved Objects Management APIs
|
||||
...getFieldsForCounter(IMPORT_STATS_PREFIX),
|
||||
|
@ -123,6 +125,10 @@ export class CoreUsageStatsClient {
|
|||
await this.updateUsageStats([], GET_STATS_PREFIX, options);
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsResolve(options: BaseIncrementOptions) {
|
||||
await this.updateUsageStats([], RESOLVE_STATS_PREFIX, options);
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsUpdate(options: BaseIncrementOptions) {
|
||||
await this.updateUsageStats([], UPDATE_STATS_PREFIX, options);
|
||||
}
|
||||
|
|
|
@ -66,6 +66,13 @@ export interface CoreUsageStats {
|
|||
'apiCalls.savedObjectsGet.namespace.custom.total'?: number;
|
||||
'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsResolve.total'?: number;
|
||||
'apiCalls.savedObjectsResolve.namespace.default.total'?: number;
|
||||
'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.total'?: number;
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsUpdate.total'?: number;
|
||||
'apiCalls.savedObjectsUpdate.namespace.default.total'?: number;
|
||||
'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes'?: number;
|
||||
|
|
|
@ -277,10 +277,12 @@ export {
|
|||
SavedObjectMigrationContext,
|
||||
SavedObjectsMigrationLogger,
|
||||
SavedObjectsRawDoc,
|
||||
SavedObjectsRawDocParseOptions,
|
||||
SavedObjectSanitizedDoc,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectsRepositoryFactory,
|
||||
SavedObjectsResolveImportErrorsOptions,
|
||||
SavedObjectsResolveResponse,
|
||||
SavedObjectsSerializer,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsUpdateResponse,
|
||||
|
|
|
@ -45,6 +45,7 @@ export {
|
|||
export {
|
||||
SavedObjectsSerializer,
|
||||
SavedObjectsRawDoc,
|
||||
SavedObjectsRawDocParseOptions,
|
||||
SavedObjectSanitizedDoc,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
} from './serialization';
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
const mockUuidv5 = jest.fn().mockReturnValue('uuidv5');
|
||||
Object.defineProperty(mockUuidv5, 'DNS', { value: 'DNSUUID', writable: false });
|
||||
jest.mock('uuid/v5', () => mockUuidv5);
|
||||
|
||||
export { mockUuidv5 };
|
|
@ -6,6 +6,7 @@ Object {
|
|||
"migrationMappingPropertyHashes": Object {
|
||||
"aaa": "625b32086eb1d1203564cf85062dd22e",
|
||||
"bbb": "18c78c995965207ed3f6e7fc5c6e55fe",
|
||||
"coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
|
||||
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
|
||||
"namespace": "2f4316de49999235636386fe51dc06c1",
|
||||
"namespaces": "2f4316de49999235636386fe51dc06c1",
|
||||
|
@ -23,6 +24,9 @@ Object {
|
|||
"bbb": Object {
|
||||
"type": "long",
|
||||
},
|
||||
"coreMigrationVersion": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"migrationVersion": Object {
|
||||
"dynamic": "true",
|
||||
"type": "object",
|
||||
|
@ -64,6 +68,7 @@ exports[`buildActiveMappings handles the \`dynamic\` property of types 1`] = `
|
|||
Object {
|
||||
"_meta": Object {
|
||||
"migrationMappingPropertyHashes": Object {
|
||||
"coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
|
||||
"firstType": "635418ab953d81d93f1190b70a8d3f57",
|
||||
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
|
||||
"namespace": "2f4316de49999235636386fe51dc06c1",
|
||||
|
@ -78,6 +83,9 @@ Object {
|
|||
},
|
||||
"dynamic": "strict",
|
||||
"properties": Object {
|
||||
"coreMigrationVersion": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"firstType": Object {
|
||||
"dynamic": "strict",
|
||||
"properties": Object {
|
||||
|
|
|
@ -153,6 +153,9 @@ function defaultMapping(): IndexMapping {
|
|||
},
|
||||
},
|
||||
},
|
||||
coreMigrationVersion: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -50,50 +50,102 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import uuidv5 from 'uuid/v5';
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import _ from 'lodash';
|
||||
import Semver from 'semver';
|
||||
import { Logger } from '../../../logging';
|
||||
import { SavedObjectUnsanitizedDoc } from '../../serialization';
|
||||
import { SavedObjectsMigrationVersion } from '../../types';
|
||||
import {
|
||||
SavedObjectsMigrationVersion,
|
||||
SavedObjectsNamespaceType,
|
||||
SavedObjectsType,
|
||||
} from '../../types';
|
||||
import { MigrationLogger } from './migration_logger';
|
||||
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types';
|
||||
import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils';
|
||||
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
|
||||
|
||||
export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
|
||||
const DEFAULT_MINIMUM_CONVERT_VERSION = '8.0.0';
|
||||
|
||||
export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;
|
||||
export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[];
|
||||
|
||||
interface TransformResult {
|
||||
/**
|
||||
* This is the original document that has been transformed.
|
||||
*/
|
||||
transformedDoc: SavedObjectUnsanitizedDoc;
|
||||
/**
|
||||
* These are any new document(s) that have been created during the transformation process; these are not transformed, but they are marked
|
||||
* as up-to-date. Only conversion transforms generate additional documents.
|
||||
*/
|
||||
additionalDocs: SavedObjectUnsanitizedDoc[];
|
||||
}
|
||||
|
||||
type ApplyTransformsFn = (
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
options?: TransformOptions
|
||||
) => TransformResult;
|
||||
|
||||
interface TransformOptions {
|
||||
convertNamespaceTypes?: boolean;
|
||||
}
|
||||
|
||||
interface DocumentMigratorOptions {
|
||||
kibanaVersion: string;
|
||||
typeRegistry: ISavedObjectTypeRegistry;
|
||||
minimumConvertVersion?: string;
|
||||
log: Logger;
|
||||
}
|
||||
|
||||
interface ActiveMigrations {
|
||||
[type: string]: {
|
||||
latestVersion: string;
|
||||
transforms: Array<{
|
||||
version: string;
|
||||
transform: TransformFn;
|
||||
}>;
|
||||
/** Derived from `migrate` transforms and `convert` transforms */
|
||||
latestMigrationVersion?: string;
|
||||
/** Derived from `reference` transforms */
|
||||
latestCoreMigrationVersion?: string;
|
||||
transforms: Transform[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Transform {
|
||||
version: string;
|
||||
transform: (doc: SavedObjectUnsanitizedDoc) => TransformResult;
|
||||
/**
|
||||
* There are two "migrationVersion" transform types:
|
||||
* * `migrate` - These transforms are defined and added by consumers using the type registry; each is applied to a single object type
|
||||
* based on an object's `migrationVersion[type]` field. These are applied during index migrations and document migrations.
|
||||
* * `convert` - These transforms are defined by core and added by consumers using the type registry; each is applied to a single object
|
||||
* type based on an object's `migrationVersion[type]` field. These are applied during index migrations, NOT document migrations.
|
||||
*
|
||||
* There is one "coreMigrationVersion" transform type:
|
||||
* * `reference` - These transforms are defined by core and added by consumers using the type registry; they are applied to all object
|
||||
* types based on their `coreMigrationVersion` field. These are applied during index migrations, NOT document migrations.
|
||||
*
|
||||
* If any additional transform types are added, the functions below should be updated to account for them.
|
||||
*/
|
||||
transformType: 'migrate' | 'convert' | 'reference';
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages migration of individual documents.
|
||||
*/
|
||||
export interface VersionedTransformer {
|
||||
migrationVersion: SavedObjectsMigrationVersion;
|
||||
migrate: MigrateFn;
|
||||
migrateAndConvert: MigrateAndConvertFn;
|
||||
prepareMigrations: () => void;
|
||||
migrate: TransformFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* A concrete implementation of the VersionedTransformer interface.
|
||||
*/
|
||||
export class DocumentMigrator implements VersionedTransformer {
|
||||
private documentMigratorOptions: DocumentMigratorOptions;
|
||||
private documentMigratorOptions: Omit<DocumentMigratorOptions, 'minimumConvertVersion'>;
|
||||
private migrations?: ActiveMigrations;
|
||||
private transformDoc?: TransformFn;
|
||||
private transformDoc?: ApplyTransformsFn;
|
||||
|
||||
/**
|
||||
* Creates an instance of DocumentMigrator.
|
||||
|
@ -101,11 +153,19 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
* @param {DocumentMigratorOptions} opts
|
||||
* @prop {string} kibanaVersion - The current version of Kibana
|
||||
* @prop {SavedObjectTypeRegistry} typeRegistry - The type registry to get type migrations from
|
||||
* @prop {string} minimumConvertVersion - The minimum version of Kibana in which documents can be converted to multi-namespace types
|
||||
* @prop {Logger} log - The migration logger
|
||||
* @memberof DocumentMigrator
|
||||
*/
|
||||
constructor({ typeRegistry, kibanaVersion, log }: DocumentMigratorOptions) {
|
||||
validateMigrationDefinition(typeRegistry);
|
||||
constructor({
|
||||
typeRegistry,
|
||||
kibanaVersion: rawKibanaVersion,
|
||||
minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION,
|
||||
log,
|
||||
}: DocumentMigratorOptions) {
|
||||
const kibanaVersion = rawKibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z)
|
||||
validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion);
|
||||
|
||||
this.documentMigratorOptions = { typeRegistry, kibanaVersion, log };
|
||||
}
|
||||
|
||||
|
@ -120,7 +180,14 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
if (!this.migrations) {
|
||||
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
|
||||
}
|
||||
return _.mapValues(this.migrations, ({ latestVersion }) => latestVersion);
|
||||
|
||||
return Object.entries(this.migrations).reduce((acc, [prop, { latestMigrationVersion }]) => {
|
||||
// some migration objects won't have a latestMigrationVersion (they only contain reference transforms that are applied from other types)
|
||||
if (latestMigrationVersion) {
|
||||
return { ...acc, [prop]: latestMigrationVersion };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,7 +199,7 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
|
||||
public prepareMigrations = () => {
|
||||
const { typeRegistry, kibanaVersion, log } = this.documentMigratorOptions;
|
||||
this.migrations = buildActiveMigrations(typeRegistry, log);
|
||||
this.migrations = buildActiveMigrations(typeRegistry, kibanaVersion, log);
|
||||
this.transformDoc = buildDocumentTransform({
|
||||
kibanaVersion,
|
||||
migrations: this.migrations,
|
||||
|
@ -155,25 +222,56 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
// Ex: Importing sample data that is cached at import level, migrations would
|
||||
// execute on mutated data the second time.
|
||||
const clonedDoc = _.cloneDeep(doc);
|
||||
return this.transformDoc(clonedDoc);
|
||||
const { transformedDoc } = this.transformDoc(clonedDoc);
|
||||
return transformedDoc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates a document to the latest version and applies type conversions if applicable. Also returns any additional document(s) that may
|
||||
* have been created during the transformation process.
|
||||
*
|
||||
* @param {SavedObjectUnsanitizedDoc} doc
|
||||
* @returns {SavedObjectUnsanitizedDoc}
|
||||
* @memberof DocumentMigrator
|
||||
*/
|
||||
public migrateAndConvert = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc[] => {
|
||||
if (!this.migrations || !this.transformDoc) {
|
||||
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
|
||||
}
|
||||
|
||||
// Clone the document to prevent accidental mutations on the original data
|
||||
// Ex: Importing sample data that is cached at import level, migrations would
|
||||
// execute on mutated data the second time.
|
||||
const clonedDoc = _.cloneDeep(doc);
|
||||
const { transformedDoc, additionalDocs } = this.transformDoc(clonedDoc, {
|
||||
convertNamespaceTypes: true,
|
||||
});
|
||||
return [transformedDoc, ...additionalDocs];
|
||||
};
|
||||
}
|
||||
|
||||
function validateMigrationsMapObject(name: string, migrationsMap?: SavedObjectMigrationMap) {
|
||||
function validateMigrationsMapObject(
|
||||
name: string,
|
||||
kibanaVersion: string,
|
||||
migrationsMap?: SavedObjectMigrationMap
|
||||
) {
|
||||
function assertObject(obj: any, prefix: string) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
throw new Error(`${prefix} Got ${obj}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertValidSemver(version: string, type: string) {
|
||||
if (!Semver.valid(version)) {
|
||||
throw new Error(
|
||||
`Invalid migration for type ${type}. Expected all properties to be semvers, but got ${version}.`
|
||||
);
|
||||
}
|
||||
if (Semver.gt(version, kibanaVersion)) {
|
||||
throw new Error(
|
||||
`Invalid migration for type ${type}. Property '${version}' cannot be greater than the current Kibana version '${kibanaVersion}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertValidTransform(fn: any, version: string, type: string) {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`Invalid migration ${type}.${version}: expected a function, but got ${fn}.`);
|
||||
|
@ -194,23 +292,63 @@ function validateMigrationsMapObject(name: string, migrationsMap?: SavedObjectMi
|
|||
}
|
||||
|
||||
/**
|
||||
* Basic validation that the migraiton definition matches our expectations. We can't
|
||||
* Basic validation that the migration definition matches our expectations. We can't
|
||||
* rely on TypeScript here, as the caller may be JavaScript / ClojureScript / any compile-to-js
|
||||
* language. So, this is just to provide a little developer-friendly error messaging. Joi was
|
||||
* giving weird errors, so we're just doing manual validation.
|
||||
*/
|
||||
function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) {
|
||||
function validateMigrationDefinition(
|
||||
registry: ISavedObjectTypeRegistry,
|
||||
kibanaVersion: string,
|
||||
minimumConvertVersion: string
|
||||
) {
|
||||
function assertObjectOrFunction(entity: any, prefix: string) {
|
||||
if (!entity || (typeof entity !== 'function' && typeof entity !== 'object')) {
|
||||
throw new Error(`${prefix} Got! ${typeof entity}, ${JSON.stringify(entity)}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertValidConvertToMultiNamespaceType(
|
||||
namespaceType: SavedObjectsNamespaceType,
|
||||
convertToMultiNamespaceTypeVersion: string,
|
||||
type: string
|
||||
) {
|
||||
if (namespaceType !== 'multiple') {
|
||||
throw new Error(
|
||||
`Invalid convertToMultiNamespaceTypeVersion for type ${type}. Expected namespaceType to be 'multiple', but got '${namespaceType}'.`
|
||||
);
|
||||
} else if (!Semver.valid(convertToMultiNamespaceTypeVersion)) {
|
||||
throw new Error(
|
||||
`Invalid convertToMultiNamespaceTypeVersion for type ${type}. Expected value to be a semver, but got '${convertToMultiNamespaceTypeVersion}'.`
|
||||
);
|
||||
} else if (Semver.lt(convertToMultiNamespaceTypeVersion, minimumConvertVersion)) {
|
||||
throw new Error(
|
||||
`Invalid convertToMultiNamespaceTypeVersion for type ${type}. Value '${convertToMultiNamespaceTypeVersion}' cannot be less than '${minimumConvertVersion}'.`
|
||||
);
|
||||
} else if (Semver.gt(convertToMultiNamespaceTypeVersion, kibanaVersion)) {
|
||||
throw new Error(
|
||||
`Invalid convertToMultiNamespaceTypeVersion for type ${type}. Value '${convertToMultiNamespaceTypeVersion}' cannot be greater than the current Kibana version '${kibanaVersion}'.`
|
||||
);
|
||||
} else if (Semver.patch(convertToMultiNamespaceTypeVersion)) {
|
||||
throw new Error(
|
||||
`Invalid convertToMultiNamespaceTypeVersion for type ${type}. Value '${convertToMultiNamespaceTypeVersion}' cannot be used on a patch version (must be like 'x.y.0').`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registry.getAllTypes().forEach((type) => {
|
||||
if (type.migrations) {
|
||||
const { name, migrations, convertToMultiNamespaceTypeVersion, namespaceType } = type;
|
||||
if (migrations) {
|
||||
assertObjectOrFunction(
|
||||
type.migrations,
|
||||
`Migration for type ${type.name} should be an object or a function returning an object like { '2.0.0': (doc) => doc }.`
|
||||
`Migration for type ${name} should be an object or a function returning an object like { '2.0.0': (doc) => doc }.`
|
||||
);
|
||||
}
|
||||
if (convertToMultiNamespaceTypeVersion) {
|
||||
assertValidConvertToMultiNamespaceType(
|
||||
namespaceType,
|
||||
convertToMultiNamespaceTypeVersion,
|
||||
name
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -220,74 +358,144 @@ function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) {
|
|||
* Converts migrations from a format that is convenient for callers to a format that
|
||||
* is convenient for our internal usage:
|
||||
* From: { type: { version: fn } }
|
||||
* To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } }
|
||||
* To: { type: { latestMigrationVersion?: string; latestCoreMigrationVersion?: string; transforms: [{ version: string, transform: fn }] } }
|
||||
*/
|
||||
function buildActiveMigrations(
|
||||
typeRegistry: ISavedObjectTypeRegistry,
|
||||
kibanaVersion: string,
|
||||
log: Logger
|
||||
): ActiveMigrations {
|
||||
const typesWithMigrationMaps = typeRegistry
|
||||
.getAllTypes()
|
||||
.map((type) => ({
|
||||
...type,
|
||||
migrationsMap: typeof type.migrations === 'function' ? type.migrations() : type.migrations,
|
||||
}))
|
||||
.filter((type) => typeof type.migrationsMap !== 'undefined');
|
||||
const referenceTransforms = getReferenceTransforms(typeRegistry);
|
||||
|
||||
typesWithMigrationMaps.forEach((type) =>
|
||||
validateMigrationsMapObject(type.name, type.migrationsMap)
|
||||
);
|
||||
return typeRegistry.getAllTypes().reduce((migrations, type) => {
|
||||
const migrationsMap =
|
||||
typeof type.migrations === 'function' ? type.migrations() : type.migrations;
|
||||
validateMigrationsMapObject(type.name, kibanaVersion, migrationsMap);
|
||||
|
||||
return typesWithMigrationMaps
|
||||
.filter((type) => type.migrationsMap && Object.keys(type.migrationsMap).length > 0)
|
||||
.reduce((migrations, type) => {
|
||||
const transforms = Object.entries(type.migrationsMap!)
|
||||
.map(([version, transform]) => ({
|
||||
version,
|
||||
transform: wrapWithTry(version, type.name, transform, log),
|
||||
}))
|
||||
.sort((a, b) => Semver.compare(a.version, b.version));
|
||||
return {
|
||||
...migrations,
|
||||
[type.name]: {
|
||||
latestVersion: _.last(transforms)!.version,
|
||||
transforms,
|
||||
},
|
||||
};
|
||||
}, {} as ActiveMigrations);
|
||||
const migrationTransforms = Object.entries(migrationsMap ?? {}).map<Transform>(
|
||||
([version, transform]) => ({
|
||||
version,
|
||||
transform: wrapWithTry(version, type.name, transform, log),
|
||||
transformType: 'migrate',
|
||||
})
|
||||
);
|
||||
const conversionTransforms = getConversionTransforms(type);
|
||||
const transforms = [
|
||||
...referenceTransforms,
|
||||
...conversionTransforms,
|
||||
...migrationTransforms,
|
||||
].sort(transformComparator);
|
||||
|
||||
if (!transforms.length) {
|
||||
return migrations;
|
||||
}
|
||||
|
||||
const migrationVersionTransforms: Transform[] = [];
|
||||
const coreMigrationVersionTransforms: Transform[] = [];
|
||||
transforms.forEach((x) => {
|
||||
if (x.transformType === 'migrate' || x.transformType === 'convert') {
|
||||
migrationVersionTransforms.push(x);
|
||||
} else {
|
||||
coreMigrationVersionTransforms.push(x);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...migrations,
|
||||
[type.name]: {
|
||||
latestMigrationVersion: _.last(migrationVersionTransforms)?.version,
|
||||
latestCoreMigrationVersion: _.last(coreMigrationVersionTransforms)?.version,
|
||||
transforms,
|
||||
},
|
||||
};
|
||||
}, {} as ActiveMigrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function which migrates and validates any document that is passed to it.
|
||||
*/
|
||||
function buildDocumentTransform({
|
||||
kibanaVersion,
|
||||
migrations,
|
||||
}: {
|
||||
kibanaVersion: string;
|
||||
migrations: ActiveMigrations;
|
||||
}): TransformFn {
|
||||
return function transformAndValidate(doc: SavedObjectUnsanitizedDoc) {
|
||||
const result = doc.migrationVersion
|
||||
? applyMigrations(doc, migrations)
|
||||
: markAsUpToDate(doc, migrations);
|
||||
}): ApplyTransformsFn {
|
||||
return function transformAndValidate(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
options: TransformOptions = {}
|
||||
) {
|
||||
validateCoreMigrationVersion(doc, kibanaVersion);
|
||||
|
||||
const { convertNamespaceTypes = false } = options;
|
||||
let transformedDoc: SavedObjectUnsanitizedDoc;
|
||||
let additionalDocs: SavedObjectUnsanitizedDoc[] = [];
|
||||
if (doc.migrationVersion) {
|
||||
const result = applyMigrations(doc, migrations, kibanaVersion, convertNamespaceTypes);
|
||||
transformedDoc = result.transformedDoc;
|
||||
additionalDocs = additionalDocs.concat(
|
||||
result.additionalDocs.map((x) => markAsUpToDate(x, migrations, kibanaVersion))
|
||||
);
|
||||
} else {
|
||||
transformedDoc = markAsUpToDate(doc, migrations, kibanaVersion);
|
||||
}
|
||||
|
||||
// In order to keep tests a bit more stable, we won't
|
||||
// tack on an empy migrationVersion to docs that have
|
||||
// no migrations defined.
|
||||
if (_.isEmpty(result.migrationVersion)) {
|
||||
delete result.migrationVersion;
|
||||
if (_.isEmpty(transformedDoc.migrationVersion)) {
|
||||
delete transformedDoc.migrationVersion;
|
||||
}
|
||||
|
||||
return result;
|
||||
return { transformedDoc, additionalDocs };
|
||||
};
|
||||
}
|
||||
|
||||
function applyMigrations(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) {
|
||||
function validateCoreMigrationVersion(doc: SavedObjectUnsanitizedDoc, kibanaVersion: string) {
|
||||
const { id, coreMigrationVersion: docVersion } = doc;
|
||||
if (!docVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We verify that the object's coreMigrationVersion is valid, and that it is not greater than the version supported by Kibana.
|
||||
// If we have a coreMigrationVersion and the kibanaVersion is smaller than it or does not exist, we are dealing with a document that
|
||||
// belongs to a future Kibana / plugin version.
|
||||
if (!Semver.valid(docVersion)) {
|
||||
throw Boom.badData(
|
||||
`Document "${id}" has an invalid "coreMigrationVersion" [${docVersion}]. This must be a semver value.`,
|
||||
doc
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.coreMigrationVersion && Semver.gt(docVersion, kibanaVersion)) {
|
||||
throw Boom.badData(
|
||||
`Document "${id}" has a "coreMigrationVersion" which belongs to a more recent version` +
|
||||
` of Kibana [${docVersion}]. The current version is [${kibanaVersion}].`,
|
||||
doc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function applyMigrations(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
migrations: ActiveMigrations,
|
||||
kibanaVersion: string,
|
||||
convertNamespaceTypes: boolean
|
||||
) {
|
||||
let additionalDocs: SavedObjectUnsanitizedDoc[] = [];
|
||||
while (true) {
|
||||
const prop = nextUnmigratedProp(doc, migrations);
|
||||
if (!prop) {
|
||||
return doc;
|
||||
// regardless of whether or not any reference transform was applied, update the coreMigrationVersion
|
||||
// this is needed to ensure that newly created documents have an up-to-date coreMigrationVersion field
|
||||
return {
|
||||
transformedDoc: { ...doc, coreMigrationVersion: kibanaVersion },
|
||||
additionalDocs,
|
||||
};
|
||||
}
|
||||
doc = migrateProp(doc, prop, migrations);
|
||||
const result = migrateProp(doc, prop, migrations, convertNamespaceTypes);
|
||||
doc = result.transformedDoc;
|
||||
additionalDocs = [...additionalDocs, ...result.additionalDocs];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,7 +511,7 @@ function props(doc: SavedObjectUnsanitizedDoc) {
|
|||
*/
|
||||
function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: string) {
|
||||
return (
|
||||
((doc as any)[prop] && (doc as any)[prop].latestVersion) ||
|
||||
((doc as any)[prop] && (doc as any)[prop].latestMigrationVersion) ||
|
||||
(doc.migrationVersion && (doc as any).migrationVersion[prop])
|
||||
);
|
||||
}
|
||||
|
@ -311,16 +519,137 @@ function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: st
|
|||
/**
|
||||
* Sets the doc's migrationVersion to be the most recent version
|
||||
*/
|
||||
function markAsUpToDate(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) {
|
||||
function markAsUpToDate(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
migrations: ActiveMigrations,
|
||||
kibanaVersion: string
|
||||
) {
|
||||
return {
|
||||
...doc,
|
||||
migrationVersion: props(doc).reduce((acc, prop) => {
|
||||
const version = propVersion(migrations, prop);
|
||||
return version ? set(acc, prop, version) : acc;
|
||||
}, {}),
|
||||
coreMigrationVersion: kibanaVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a single-namespace object to a multi-namespace object. This primarily entails removing the `namespace` field and adding the
|
||||
* `namespaces` field.
|
||||
*
|
||||
* If the object does not exist in the default namespace (undefined), its ID is also regenerated, and an "originId" is added to preserve
|
||||
* legacy import/copy behavior.
|
||||
*/
|
||||
function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) {
|
||||
const { namespace, ...otherAttrs } = doc;
|
||||
const additionalDocs: SavedObjectUnsanitizedDoc[] = [];
|
||||
|
||||
// If this object exists in the default namespace, return it with the appropriate `namespaces` field without changing its ID.
|
||||
if (namespace === undefined) {
|
||||
return {
|
||||
transformedDoc: { ...otherAttrs, namespaces: [DEFAULT_NAMESPACE_STRING] },
|
||||
additionalDocs,
|
||||
};
|
||||
}
|
||||
|
||||
const { id: originId, type } = otherAttrs;
|
||||
const id = deterministicallyRegenerateObjectId(namespace, type, originId!);
|
||||
if (namespace !== undefined) {
|
||||
const legacyUrlAlias: SavedObjectUnsanitizedDoc<LegacyUrlAlias> = {
|
||||
id: `${namespace}:${type}:${originId}`,
|
||||
type: LEGACY_URL_ALIAS_TYPE,
|
||||
attributes: {
|
||||
targetNamespace: namespace,
|
||||
targetType: type,
|
||||
targetId: id,
|
||||
},
|
||||
};
|
||||
additionalDocs.push(legacyUrlAlias);
|
||||
}
|
||||
return {
|
||||
transformedDoc: { ...otherAttrs, id, originId, namespaces: [namespace] },
|
||||
additionalDocs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all applicable conversion transforms for a given object type.
|
||||
*/
|
||||
function getConversionTransforms(type: SavedObjectsType): Transform[] {
|
||||
const { convertToMultiNamespaceTypeVersion } = type;
|
||||
if (!convertToMultiNamespaceTypeVersion) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
version: convertToMultiNamespaceTypeVersion,
|
||||
transform: convertNamespaceType,
|
||||
transformType: 'convert',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all applicable reference transforms for all object types.
|
||||
*/
|
||||
function getReferenceTransforms(typeRegistry: ISavedObjectTypeRegistry): Transform[] {
|
||||
const transformMap = typeRegistry
|
||||
.getAllTypes()
|
||||
.filter((type) => type.convertToMultiNamespaceTypeVersion)
|
||||
.reduce((acc, { convertToMultiNamespaceTypeVersion: version, name }) => {
|
||||
const types = acc.get(version!) ?? new Set();
|
||||
return acc.set(version!, types.add(name));
|
||||
}, new Map<string, Set<string>>());
|
||||
|
||||
return Array.from(transformMap, ([version, types]) => ({
|
||||
version,
|
||||
transform: (doc) => {
|
||||
const { namespace, references } = doc;
|
||||
if (namespace && references?.length) {
|
||||
return {
|
||||
transformedDoc: {
|
||||
...doc,
|
||||
references: references.map(({ type, id, ...attrs }) => ({
|
||||
...attrs,
|
||||
type,
|
||||
id: types.has(type) ? deterministicallyRegenerateObjectId(namespace, type, id) : id,
|
||||
})),
|
||||
},
|
||||
additionalDocs: [],
|
||||
};
|
||||
}
|
||||
return { transformedDoc: doc, additionalDocs: [] };
|
||||
},
|
||||
transformType: 'reference',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms are sorted in ascending order by version. One version may contain multiple transforms; 'reference' transforms always run
|
||||
* first, 'convert' transforms always run second, and 'migrate' transforms always run last. This is because:
|
||||
* 1. 'convert' transforms get rid of the `namespace` field, which must be present for 'reference' transforms to function correctly.
|
||||
* 2. 'migrate' transforms are defined by the consumer, and may change the object type or migrationVersion which resets the migration loop
|
||||
* and could cause any remaining transforms for this version to be skipped.
|
||||
*/
|
||||
function transformComparator(a: Transform, b: Transform) {
|
||||
const semver = Semver.compare(a.version, b.version);
|
||||
if (semver !== 0) {
|
||||
return semver;
|
||||
} else if (a.transformType !== b.transformType) {
|
||||
if (a.transformType === 'migrate') {
|
||||
return 1;
|
||||
} else if (b.transformType === 'migrate') {
|
||||
return -1;
|
||||
} else if (a.transformType === 'convert') {
|
||||
return 1;
|
||||
} else if (b.transformType === 'convert') {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a specific transform function fails, this tacks on a bit of information
|
||||
* about the document and transform that caused the failure.
|
||||
|
@ -342,7 +671,7 @@ function wrapWithTry(
|
|||
throw new Error(`Invalid saved object returned from migration ${type}:${version}.`);
|
||||
}
|
||||
|
||||
return result;
|
||||
return { transformedDoc: result, additionalDocs: [] };
|
||||
} catch (error) {
|
||||
const failedTransform = `${type}:${version}`;
|
||||
const failedDoc = JSON.stringify(doc);
|
||||
|
@ -354,32 +683,52 @@ function wrapWithTry(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not a document has any pending transforms that should be applied based on its coreMigrationVersion field.
|
||||
* Currently, only reference transforms qualify.
|
||||
*/
|
||||
function getHasPendingCoreMigrationVersionTransform(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
migrations: ActiveMigrations,
|
||||
prop: string
|
||||
) {
|
||||
if (!migrations.hasOwnProperty(prop)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { latestCoreMigrationVersion } = migrations[prop];
|
||||
const { coreMigrationVersion } = doc;
|
||||
return (
|
||||
latestCoreMigrationVersion &&
|
||||
(!coreMigrationVersion || Semver.gt(latestCoreMigrationVersion, coreMigrationVersion))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first unmigrated property in the specified document.
|
||||
*/
|
||||
function nextUnmigratedProp(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) {
|
||||
return props(doc).find((p) => {
|
||||
const latestVersion = propVersion(migrations, p);
|
||||
const latestMigrationVersion = propVersion(migrations, p);
|
||||
const docVersion = propVersion(doc, p);
|
||||
|
||||
if (latestVersion === docVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We verify that the version is not greater than the version supported by Kibana.
|
||||
// If we didn't, this would cause an infinite loop, as we'd be unable to migrate the property
|
||||
// but it would continue to show up as unmigrated.
|
||||
// If we have a docVersion and the latestVersion is smaller than it or does not exist,
|
||||
// If we have a docVersion and the latestMigrationVersion is smaller than it or does not exist,
|
||||
// we are dealing with a document that belongs to a future Kibana / plugin version.
|
||||
if (docVersion && (!latestVersion || Semver.gt(docVersion, latestVersion))) {
|
||||
if (docVersion && (!latestMigrationVersion || Semver.gt(docVersion, latestMigrationVersion))) {
|
||||
throw Boom.badData(
|
||||
`Document "${doc.id}" has property "${p}" which belongs to a more recent` +
|
||||
` version of Kibana [${docVersion}]. The last known version is [${latestVersion}]`,
|
||||
` version of Kibana [${docVersion}]. The last known version is [${latestMigrationVersion}]`,
|
||||
doc
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
return (
|
||||
(latestMigrationVersion && latestMigrationVersion !== docVersion) ||
|
||||
getHasPendingCoreMigrationVersionTransform(doc, migrations, p) // If the object itself is up-to-date, check if its references are up-to-date too
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -389,23 +738,42 @@ function nextUnmigratedProp(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMi
|
|||
function migrateProp(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
prop: string,
|
||||
migrations: ActiveMigrations
|
||||
): SavedObjectUnsanitizedDoc {
|
||||
migrations: ActiveMigrations,
|
||||
convertNamespaceTypes: boolean
|
||||
): TransformResult {
|
||||
const originalType = doc.type;
|
||||
let migrationVersion = _.clone(doc.migrationVersion) || {};
|
||||
const typeChanged = () => !doc.hasOwnProperty(prop) || doc.type !== originalType;
|
||||
let additionalDocs: SavedObjectUnsanitizedDoc[] = [];
|
||||
|
||||
for (const { version, transform } of applicableTransforms(migrations, doc, prop)) {
|
||||
doc = transform(doc);
|
||||
migrationVersion = updateMigrationVersion(doc, migrationVersion, prop, version);
|
||||
doc.migrationVersion = _.clone(migrationVersion);
|
||||
for (const { version, transform, transformType } of applicableTransforms(migrations, doc, prop)) {
|
||||
const currentVersion = propVersion(doc, prop);
|
||||
if (currentVersion && Semver.gt(currentVersion, version)) {
|
||||
// the previous transform function increased the object's migrationVersion; break out of the loop
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeChanged()) {
|
||||
if (convertNamespaceTypes || (transformType !== 'convert' && transformType !== 'reference')) {
|
||||
// migrate transforms are always applied, but conversion transforms and reference transforms are only applied during index migrations
|
||||
const result = transform(doc);
|
||||
doc = result.transformedDoc;
|
||||
additionalDocs = [...additionalDocs, ...result.additionalDocs];
|
||||
}
|
||||
if (transformType === 'reference') {
|
||||
// regardless of whether or not the reference transform was applied, update the object's coreMigrationVersion
|
||||
// this is needed to ensure that we don't have an endless migration loop
|
||||
doc.coreMigrationVersion = version;
|
||||
} else {
|
||||
migrationVersion = updateMigrationVersion(doc, migrationVersion, prop, version);
|
||||
doc.migrationVersion = _.clone(migrationVersion);
|
||||
}
|
||||
|
||||
if (doc.type !== originalType) {
|
||||
// the transform function changed the object's type; break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
return { transformedDoc: doc, additionalDocs };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -417,9 +785,14 @@ function applicableTransforms(
|
|||
prop: string
|
||||
) {
|
||||
const minVersion = propVersion(doc, prop);
|
||||
const minReferenceVersion = doc.coreMigrationVersion || '0.0.0';
|
||||
const { transforms } = migrations[prop];
|
||||
return minVersion
|
||||
? transforms.filter(({ version }) => Semver.gt(version, minVersion))
|
||||
? transforms.filter(({ version, transformType }) =>
|
||||
transformType === 'reference'
|
||||
? Semver.gt(version, minReferenceVersion)
|
||||
: Semver.gt(version, minVersion)
|
||||
)
|
||||
: transforms;
|
||||
}
|
||||
|
||||
|
@ -466,3 +839,14 @@ function assertNoDowngrades(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministically regenerates a saved object's ID based upon it's current namespace, type, and ID. This ensures that we can regenerate
|
||||
* any existing object IDs without worrying about collisions if two objects that exist in different namespaces share an ID. It also ensures
|
||||
* that we can later regenerate any inbound object references to match.
|
||||
*
|
||||
* @note This is only intended to be used when single-namespace object types are converted into multi-namespace object types.
|
||||
*/
|
||||
function deterministicallyRegenerateObjectId(namespace: string, type: string, id: string) {
|
||||
return uuidv5(`${namespace}:${type}:${id}`, uuidv5.DNS); // the uuidv5 namespace constant (uuidv5.DNS) is arbitrary
|
||||
}
|
||||
|
|
|
@ -557,6 +557,7 @@ describe('ElasticIndex', () => {
|
|||
mappings,
|
||||
count,
|
||||
migrations,
|
||||
kibanaVersion,
|
||||
}: any) {
|
||||
client.indices.get = jest.fn().mockReturnValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({
|
||||
|
@ -570,7 +571,12 @@ describe('ElasticIndex', () => {
|
|||
})
|
||||
);
|
||||
|
||||
const hasMigrations = await Index.migrationsUpToDate(client, index, migrations);
|
||||
const hasMigrations = await Index.migrationsUpToDate(
|
||||
client,
|
||||
index,
|
||||
migrations,
|
||||
kibanaVersion
|
||||
);
|
||||
return { hasMigrations };
|
||||
}
|
||||
|
||||
|
@ -584,6 +590,7 @@ describe('ElasticIndex', () => {
|
|||
},
|
||||
count: 0,
|
||||
migrations: { dashy: '2.3.4' },
|
||||
kibanaVersion: '7.10.0',
|
||||
});
|
||||
|
||||
expect(hasMigrations).toBeFalsy();
|
||||
|
@ -611,6 +618,7 @@ describe('ElasticIndex', () => {
|
|||
},
|
||||
count: 2,
|
||||
migrations: {},
|
||||
kibanaVersion: '7.10.0',
|
||||
});
|
||||
|
||||
expect(hasMigrations).toBeTruthy();
|
||||
|
@ -652,6 +660,7 @@ describe('ElasticIndex', () => {
|
|||
},
|
||||
count: 3,
|
||||
migrations: { dashy: '23.2.5' },
|
||||
kibanaVersion: '7.10.0',
|
||||
});
|
||||
|
||||
expect(hasMigrations).toBeFalsy();
|
||||
|
@ -677,6 +686,7 @@ describe('ElasticIndex', () => {
|
|||
bashy: '99.9.3',
|
||||
flashy: '3.4.5',
|
||||
},
|
||||
kibanaVersion: '7.10.0',
|
||||
});
|
||||
|
||||
function shouldClause(type: string, version: string) {
|
||||
|
@ -702,6 +712,15 @@ describe('ElasticIndex', () => {
|
|||
shouldClause('dashy', '23.2.5'),
|
||||
shouldClause('bashy', '99.9.3'),
|
||||
shouldClause('flashy', '3.4.5'),
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
term: {
|
||||
coreMigrationVersion: '7.10.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -147,6 +147,7 @@ export async function migrationsUpToDate(
|
|||
client: MigrationEsClient,
|
||||
index: string,
|
||||
migrationVersion: SavedObjectsMigrationVersion,
|
||||
kibanaVersion: string,
|
||||
retryCount: number = 10
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
|
@ -165,18 +166,29 @@ export async function migrationsUpToDate(
|
|||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
should: Object.entries(migrationVersion).map(([type, latestVersion]) => ({
|
||||
bool: {
|
||||
must: [
|
||||
{ exists: { field: type } },
|
||||
{
|
||||
bool: {
|
||||
must_not: { term: { [`migrationVersion.${type}`]: latestVersion } },
|
||||
should: [
|
||||
...Object.entries(migrationVersion).map(([type, latestVersion]) => ({
|
||||
bool: {
|
||||
must: [
|
||||
{ exists: { field: type } },
|
||||
{
|
||||
bool: {
|
||||
must_not: { term: { [`migrationVersion.${type}`]: latestVersion } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
term: {
|
||||
coreMigrationVersion: kibanaVersion,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -194,7 +206,7 @@ export async function migrationsUpToDate(
|
|||
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
|
||||
return await migrationsUpToDate(client, index, migrationVersion, retryCount - 1);
|
||||
return await migrationsUpToDate(client, index, migrationVersion, kibanaVersion, retryCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ describe('IndexMigrator', () => {
|
|||
batchSize: 10,
|
||||
client: elasticsearchClientMock.createElasticsearchClient(),
|
||||
index: '.kibana',
|
||||
kibanaVersion: '7.10.0',
|
||||
log: loggingSystemMock.create().get(),
|
||||
mappingProperties: {},
|
||||
pollInterval: 1,
|
||||
|
@ -31,6 +32,7 @@ describe('IndexMigrator', () => {
|
|||
documentMigrator: {
|
||||
migrationVersion: {},
|
||||
migrate: _.identity,
|
||||
migrateAndConvert: _.identity,
|
||||
prepareMigrations: jest.fn(),
|
||||
},
|
||||
serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
|
||||
|
@ -58,6 +60,7 @@ describe('IndexMigrator', () => {
|
|||
namespaces: '2f4316de49999235636386fe51dc06c1',
|
||||
originId: '2f4316de49999235636386fe51dc06c1',
|
||||
references: '7997cf5a56cc02bdc9c93361bde732b0',
|
||||
coreMigrationVersion: '2f4316de49999235636386fe51dc06c1',
|
||||
type: '2f4316de49999235636386fe51dc06c1',
|
||||
updated_at: '00da57df13e94e9d98437d13ace4bfe0',
|
||||
},
|
||||
|
@ -78,6 +81,7 @@ describe('IndexMigrator', () => {
|
|||
id: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
coreMigrationVersion: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
settings: { number_of_shards: 1, auto_expand_replicas: '0-1' },
|
||||
|
@ -179,6 +183,7 @@ describe('IndexMigrator', () => {
|
|||
namespaces: '2f4316de49999235636386fe51dc06c1',
|
||||
originId: '2f4316de49999235636386fe51dc06c1',
|
||||
references: '7997cf5a56cc02bdc9c93361bde732b0',
|
||||
coreMigrationVersion: '2f4316de49999235636386fe51dc06c1',
|
||||
type: '2f4316de49999235636386fe51dc06c1',
|
||||
updated_at: '00da57df13e94e9d98437d13ace4bfe0',
|
||||
},
|
||||
|
@ -200,6 +205,7 @@ describe('IndexMigrator', () => {
|
|||
id: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
coreMigrationVersion: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
settings: { number_of_shards: 1, auto_expand_replicas: '0-1' },
|
||||
|
@ -240,6 +246,7 @@ describe('IndexMigrator', () => {
|
|||
namespaces: '2f4316de49999235636386fe51dc06c1',
|
||||
originId: '2f4316de49999235636386fe51dc06c1',
|
||||
references: '7997cf5a56cc02bdc9c93361bde732b0',
|
||||
coreMigrationVersion: '2f4316de49999235636386fe51dc06c1',
|
||||
type: '2f4316de49999235636386fe51dc06c1',
|
||||
updated_at: '00da57df13e94e9d98437d13ace4bfe0',
|
||||
},
|
||||
|
@ -261,6 +268,7 @@ describe('IndexMigrator', () => {
|
|||
id: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
coreMigrationVersion: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
settings: { number_of_shards: 1, auto_expand_replicas: '0-1' },
|
||||
|
@ -307,17 +315,15 @@ describe('IndexMigrator', () => {
|
|||
test('transforms all docs from the original index', async () => {
|
||||
let count = 0;
|
||||
const { client } = testOpts;
|
||||
const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => {
|
||||
return {
|
||||
...doc,
|
||||
attributes: { name: ++count },
|
||||
};
|
||||
const migrateAndConvertDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => {
|
||||
return [{ ...doc, attributes: { name: ++count } }];
|
||||
});
|
||||
|
||||
testOpts.documentMigrator = {
|
||||
migrationVersion: { foo: '1.2.3' },
|
||||
migrate: jest.fn(),
|
||||
migrateAndConvert: migrateAndConvertDoc,
|
||||
prepareMigrations: jest.fn(),
|
||||
migrate: migrateDoc,
|
||||
};
|
||||
|
||||
withIndex(client, {
|
||||
|
@ -331,14 +337,14 @@ describe('IndexMigrator', () => {
|
|||
await new IndexMigrator(testOpts).migrate();
|
||||
|
||||
expect(count).toEqual(2);
|
||||
expect(migrateDoc).toHaveBeenCalledWith({
|
||||
expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(1, {
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: { name: 'Bar' },
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
});
|
||||
expect(migrateDoc).toHaveBeenCalledWith({
|
||||
expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(2, {
|
||||
id: '2',
|
||||
type: 'foo',
|
||||
attributes: { name: 'Baz' },
|
||||
|
@ -363,14 +369,15 @@ describe('IndexMigrator', () => {
|
|||
|
||||
test('rejects when the migration function throws an error', async () => {
|
||||
const { client } = testOpts;
|
||||
const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => {
|
||||
const migrateAndConvertDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => {
|
||||
throw new Error('error migrating document');
|
||||
});
|
||||
|
||||
testOpts.documentMigrator = {
|
||||
migrationVersion: { foo: '1.2.3' },
|
||||
migrate: jest.fn(),
|
||||
migrateAndConvert: migrateAndConvertDoc,
|
||||
prepareMigrations: jest.fn(),
|
||||
migrate: migrateDoc,
|
||||
};
|
||||
|
||||
withIndex(client, {
|
||||
|
|
|
@ -60,13 +60,14 @@ export class IndexMigrator {
|
|||
* Determines what action the migration system needs to take (none, patch, migrate).
|
||||
*/
|
||||
async function requiresMigration(context: Context): Promise<boolean> {
|
||||
const { client, alias, documentMigrator, dest, log } = context;
|
||||
const { client, alias, documentMigrator, dest, kibanaVersion, log } = context;
|
||||
|
||||
// Have all of our known migrations been run against the index?
|
||||
const hasMigrations = await Index.migrationsUpToDate(
|
||||
client,
|
||||
alias,
|
||||
documentMigrator.migrationVersion
|
||||
documentMigrator.migrationVersion,
|
||||
kibanaVersion
|
||||
);
|
||||
|
||||
if (!hasMigrations) {
|
||||
|
@ -184,7 +185,7 @@ async function migrateSourceToDest(context: Context) {
|
|||
await Index.write(
|
||||
client,
|
||||
dest.indexName,
|
||||
await migrateRawDocs(serializer, documentMigrator.migrate, docs, log)
|
||||
await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs, log)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks';
|
|||
|
||||
describe('migrateRawDocs', () => {
|
||||
test('converts raw docs to saved objects', async () => {
|
||||
const transform = jest.fn<any, any>((doc: any) => set(doc, 'attributes.name', 'HOI!'));
|
||||
const transform = jest.fn<any, any>((doc: any) => [
|
||||
set(_.cloneDeep(doc), 'attributes.name', 'HOI!'),
|
||||
]);
|
||||
const result = await migrateRawDocs(
|
||||
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
|
||||
transform,
|
||||
|
@ -37,14 +39,30 @@ describe('migrateRawDocs', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(transform).toHaveBeenCalled();
|
||||
const obj1 = {
|
||||
id: 'b',
|
||||
type: 'a',
|
||||
attributes: { name: 'AAA' },
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
};
|
||||
const obj2 = {
|
||||
id: 'd',
|
||||
type: 'c',
|
||||
attributes: { name: 'DDD' },
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
};
|
||||
expect(transform).toHaveBeenCalledTimes(2);
|
||||
expect(transform).toHaveBeenNthCalledWith(1, obj1);
|
||||
expect(transform).toHaveBeenNthCalledWith(2, obj2);
|
||||
});
|
||||
|
||||
test('passes invalid docs through untouched and logs error', async () => {
|
||||
const logger = createSavedObjectsMigrationLoggerMock();
|
||||
const transform = jest.fn<any, any>((doc: any) =>
|
||||
set(_.cloneDeep(doc), 'attributes.name', 'TADA')
|
||||
);
|
||||
const transform = jest.fn<any, any>((doc: any) => [
|
||||
set(_.cloneDeep(doc), 'attributes.name', 'TADA'),
|
||||
]);
|
||||
const result = await migrateRawDocs(
|
||||
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
|
||||
transform,
|
||||
|
@ -63,23 +81,53 @@ describe('migrateRawDocs', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(transform.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'd',
|
||||
type: 'c',
|
||||
attributes: {
|
||||
name: 'DDD',
|
||||
},
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
]);
|
||||
const obj2 = {
|
||||
id: 'd',
|
||||
type: 'c',
|
||||
attributes: { name: 'DDD' },
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
};
|
||||
expect(transform).toHaveBeenCalledTimes(1);
|
||||
expect(transform).toHaveBeenCalledWith(obj2);
|
||||
|
||||
expect(logger.error).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('handles when one document is transformed into multiple documents', async () => {
|
||||
const transform = jest.fn<any, any>((doc: any) => [
|
||||
set(_.cloneDeep(doc), 'attributes.name', 'HOI!'),
|
||||
{ id: 'bar', type: 'foo', attributes: { name: 'baz' } },
|
||||
]);
|
||||
const result = await migrateRawDocs(
|
||||
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
|
||||
transform,
|
||||
[{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }],
|
||||
createSavedObjectsMigrationLoggerMock()
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
_id: 'a:b',
|
||||
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] },
|
||||
},
|
||||
{
|
||||
_id: 'foo:bar',
|
||||
_source: { type: 'foo', foo: { name: 'baz' }, references: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
const obj = {
|
||||
id: 'b',
|
||||
type: 'a',
|
||||
attributes: { name: 'AAA' },
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
};
|
||||
expect(transform).toHaveBeenCalledTimes(1);
|
||||
expect(transform).toHaveBeenCalledWith(obj);
|
||||
});
|
||||
|
||||
test('rejects when the transform function throws an error', async () => {
|
||||
const transform = jest.fn<any, any>((doc: any) => {
|
||||
throw new Error('error during transform');
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
SavedObjectsSerializer,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
} from '../../serialization';
|
||||
import { TransformFn } from './document_migrator';
|
||||
import { MigrateAndConvertFn } from './document_migrator';
|
||||
import { SavedObjectsMigrationLogger } from '.';
|
||||
|
||||
/**
|
||||
|
@ -28,21 +28,24 @@ import { SavedObjectsMigrationLogger } from '.';
|
|||
*/
|
||||
export async function migrateRawDocs(
|
||||
serializer: SavedObjectsSerializer,
|
||||
migrateDoc: TransformFn,
|
||||
migrateDoc: MigrateAndConvertFn,
|
||||
rawDocs: SavedObjectsRawDoc[],
|
||||
log: SavedObjectsMigrationLogger
|
||||
): Promise<SavedObjectsRawDoc[]> {
|
||||
const migrateDocWithoutBlocking = transformNonBlocking(migrateDoc);
|
||||
const processedDocs = [];
|
||||
for (const raw of rawDocs) {
|
||||
if (serializer.isRawSavedObject(raw)) {
|
||||
const savedObject = serializer.rawToSavedObject(raw);
|
||||
const options = { namespaceTreatment: 'lax' as 'lax' };
|
||||
if (serializer.isRawSavedObject(raw, options)) {
|
||||
const savedObject = serializer.rawToSavedObject(raw, options);
|
||||
savedObject.migrationVersion = savedObject.migrationVersion || {};
|
||||
processedDocs.push(
|
||||
serializer.savedObjectToRaw({
|
||||
references: [],
|
||||
...(await migrateDocWithoutBlocking(savedObject)),
|
||||
})
|
||||
...(await migrateDocWithoutBlocking(savedObject)).map((attrs) =>
|
||||
serializer.savedObjectToRaw({
|
||||
references: [],
|
||||
...attrs,
|
||||
})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
log.error(
|
||||
|
@ -63,8 +66,8 @@ export async function migrateRawDocs(
|
|||
* work in between each transform.
|
||||
*/
|
||||
function transformNonBlocking(
|
||||
transform: TransformFn
|
||||
): (doc: SavedObjectUnsanitizedDoc) => Promise<SavedObjectUnsanitizedDoc> {
|
||||
transform: MigrateAndConvertFn
|
||||
): (doc: SavedObjectUnsanitizedDoc) => Promise<SavedObjectUnsanitizedDoc[]> {
|
||||
// promises aren't enough to unblock the event loop
|
||||
return (doc: SavedObjectUnsanitizedDoc) =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface MigrationOpts {
|
|||
scrollDuration: string;
|
||||
client: MigrationEsClient;
|
||||
index: string;
|
||||
kibanaVersion: string;
|
||||
log: Logger;
|
||||
mappingProperties: SavedObjectsTypeMappingDefinitions;
|
||||
documentMigrator: VersionedTransformer;
|
||||
|
@ -54,6 +55,7 @@ export interface Context {
|
|||
source: Index.FullIndexInfo;
|
||||
dest: Index.FullIndexInfo;
|
||||
documentMigrator: VersionedTransformer;
|
||||
kibanaVersion: string;
|
||||
log: SavedObjectsMigrationLogger;
|
||||
batchSize: number;
|
||||
pollInterval: number;
|
||||
|
@ -78,6 +80,7 @@ export async function migrationContext(opts: MigrationOpts): Promise<Context> {
|
|||
alias,
|
||||
source,
|
||||
dest,
|
||||
kibanaVersion: opts.kibanaVersion,
|
||||
log: new MigrationLogger(log),
|
||||
batchSize: opts.batchSize,
|
||||
documentMigrator: opts.documentMigrator,
|
||||
|
|
|
@ -6,6 +6,7 @@ Object {
|
|||
"migrationMappingPropertyHashes": Object {
|
||||
"amap": "510f1f0adb69830cf8a1c5ce2923ed82",
|
||||
"bmap": "510f1f0adb69830cf8a1c5ce2923ed82",
|
||||
"coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
|
||||
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
|
||||
"namespace": "2f4316de49999235636386fe51dc06c1",
|
||||
"namespaces": "2f4316de49999235636386fe51dc06c1",
|
||||
|
@ -31,6 +32,9 @@ Object {
|
|||
},
|
||||
},
|
||||
},
|
||||
"coreMigrationVersion": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"migrationVersion": Object {
|
||||
"dynamic": "true",
|
||||
"type": "object",
|
||||
|
|
|
@ -90,6 +90,7 @@ export class KibanaMigrator {
|
|||
}: KibanaMigratorOptions) {
|
||||
this.client = client;
|
||||
this.kibanaConfig = kibanaConfig;
|
||||
this.kibanaVersion = kibanaVersion;
|
||||
this.savedObjectsConfig = savedObjectsConfig;
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.serializer = new SavedObjectsSerializer(this.typeRegistry);
|
||||
|
@ -177,7 +178,7 @@ export class KibanaMigrator {
|
|||
transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) =>
|
||||
migrateRawDocs(
|
||||
this.serializer,
|
||||
this.documentMigrator.migrate,
|
||||
this.documentMigrator.migrateAndConvert,
|
||||
rawDocs,
|
||||
new MigrationLogger(this.log)
|
||||
),
|
||||
|
@ -192,6 +193,7 @@ export class KibanaMigrator {
|
|||
client: createMigrationEsClient(this.client, this.log, this.migrationsRetryDelay),
|
||||
documentMigrator: this.documentMigrator,
|
||||
index,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
log: this.log,
|
||||
mappingProperties: indexMap[index].typeMappings,
|
||||
pollInterval: this.savedObjectsConfig.pollInterval,
|
||||
|
|
|
@ -61,7 +61,7 @@ export interface SavedObjectMigrationContext {
|
|||
|
||||
/**
|
||||
* A map of {@link SavedObjectMigrationFn | migration functions} to be used for a given type.
|
||||
* The map's keys must be valid semver versions.
|
||||
* 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.
|
||||
|
|
12
src/core/server/saved_objects/object_types/constants.ts
Normal file
12
src/core/server/saved_objects/object_types/constants.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const LEGACY_URL_ALIAS_TYPE = 'legacy-url-alias';
|
11
src/core/server/saved_objects/object_types/index.ts
Normal file
11
src/core/server/saved_objects/object_types/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
export { LEGACY_URL_ALIAS_TYPE } from './constants';
|
||||
export { LegacyUrlAlias } from './types';
|
||||
export { registerCoreObjectTypes } from './registration';
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { typeRegistryMock } from '../saved_objects_type_registry.mock';
|
||||
import { LEGACY_URL_ALIAS_TYPE } from './constants';
|
||||
import { registerCoreObjectTypes } from './registration';
|
||||
|
||||
describe('Core saved object types registration', () => {
|
||||
describe('#registerCoreObjectTypes', () => {
|
||||
it('registers all expected types', () => {
|
||||
const typeRegistry = typeRegistryMock.create();
|
||||
registerCoreObjectTypes(typeRegistry);
|
||||
|
||||
expect(typeRegistry.registerType).toHaveBeenCalledTimes(1);
|
||||
expect(typeRegistry.registerType).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: LEGACY_URL_ALIAS_TYPE })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
29
src/core/server/saved_objects/object_types/registration.ts
Normal file
29
src/core/server/saved_objects/object_types/registration.ts
Normal 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
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { LEGACY_URL_ALIAS_TYPE } from './constants';
|
||||
import { ISavedObjectTypeRegistry, SavedObjectsType, SavedObjectTypeRegistry } from '..';
|
||||
|
||||
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: {},
|
||||
},
|
||||
hidden: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function registerCoreObjectTypes(
|
||||
typeRegistry: ISavedObjectTypeRegistry & Pick<SavedObjectTypeRegistry, 'registerType'>
|
||||
) {
|
||||
typeRegistry.registerType(legacyUrlAliasType);
|
||||
}
|
19
src/core/server/saved_objects/object_types/types.ts
Normal file
19
src/core/server/saved_objects/object_types/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface LegacyUrlAlias {
|
||||
targetNamespace: string;
|
||||
targetType: string;
|
||||
targetId: string;
|
||||
lastResolved?: string;
|
||||
resolveCounter?: number;
|
||||
disabled?: boolean;
|
||||
}
|
|
@ -29,6 +29,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout
|
|||
attributes: schema.recordOf(schema.string(), schema.any()),
|
||||
version: schema.maybe(schema.string()),
|
||||
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
coreMigrationVersion: schema.maybe(schema.string()),
|
||||
references: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
|
|
|
@ -29,6 +29,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep
|
|||
body: schema.object({
|
||||
attributes: schema.recordOf(schema.string(), schema.any()),
|
||||
migrationVersion: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
coreMigrationVersion: schema.maybe(schema.string()),
|
||||
references: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
|
@ -45,12 +46,25 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep
|
|||
router.handleLegacyErrors(async (context, req, res) => {
|
||||
const { type, id } = req.params;
|
||||
const { overwrite } = req.query;
|
||||
const { attributes, migrationVersion, references, initialNamespaces } = req.body;
|
||||
const {
|
||||
attributes,
|
||||
migrationVersion,
|
||||
coreMigrationVersion,
|
||||
references,
|
||||
initialNamespaces,
|
||||
} = req.body;
|
||||
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {});
|
||||
|
||||
const options = { id, overwrite, migrationVersion, references, initialNamespaces };
|
||||
const options = {
|
||||
id,
|
||||
overwrite,
|
||||
migrationVersion,
|
||||
coreMigrationVersion,
|
||||
references,
|
||||
initialNamespaces,
|
||||
};
|
||||
const result = await context.core.savedObjects.client.create(type, attributes, options);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Logger } from '../../logging';
|
|||
import { SavedObjectConfig } from '../saved_objects_config';
|
||||
import { IKibanaMigrator } from '../migrations';
|
||||
import { registerGetRoute } from './get';
|
||||
import { registerResolveRoute } from './resolve';
|
||||
import { registerCreateRoute } from './create';
|
||||
import { registerDeleteRoute } from './delete';
|
||||
import { registerFindRoute } from './find';
|
||||
|
@ -41,6 +42,7 @@ export function registerRoutes({
|
|||
const router = http.createRouter('/api/saved_objects/');
|
||||
|
||||
registerGetRoute(router, { coreUsageData });
|
||||
registerResolveRoute(router, { coreUsageData });
|
||||
registerCreateRoute(router, { coreUsageData });
|
||||
registerDeleteRoute(router, { coreUsageData });
|
||||
registerFindRoute(router, { coreUsageData });
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { registerResolveRoute } from '../resolve';
|
||||
import { ContextService } from '../../../context';
|
||||
import { savedObjectsClientMock } from '../../service/saved_objects_client.mock';
|
||||
import { CoreUsageStatsClient } from '../../../core_usage_data';
|
||||
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
|
||||
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
|
||||
import { HttpService, InternalHttpServiceSetup } from '../../../http';
|
||||
import { createHttpServer, createCoreContext } from '../../../http/test_utils';
|
||||
import { coreMock } from '../../../mocks';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
||||
describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
|
||||
let server: HttpService;
|
||||
let httpSetup: InternalHttpServiceSetup;
|
||||
let handlerContext: ReturnType<typeof coreMock.createRequestHandlerContext>;
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const coreContext = createCoreContext({ coreId });
|
||||
server = createHttpServer(coreContext);
|
||||
|
||||
const contextService = new ContextService(coreContext);
|
||||
httpSetup = await server.setup({
|
||||
context: contextService.setup({ pluginDependencies: new Map() }),
|
||||
});
|
||||
|
||||
handlerContext = coreMock.createRequestHandlerContext();
|
||||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
|
||||
httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => {
|
||||
return handlerContext;
|
||||
});
|
||||
|
||||
const router = httpSetup.createRouter('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
coreUsageStatsClient.incrementSavedObjectsResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
|
||||
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
|
||||
registerResolveRoute(router, { coreUsageData });
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('formats successful response', async () => {
|
||||
const clientResponse = {
|
||||
saved_object: {
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
type: 'logstash-type',
|
||||
attributes: {},
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
references: [],
|
||||
},
|
||||
outcome: 'exactMatch' as 'exactMatch',
|
||||
};
|
||||
|
||||
savedObjectsClient.resolve.mockResolvedValue(clientResponse);
|
||||
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/resolve/index-pattern/logstash-*')
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toEqual(clientResponse);
|
||||
});
|
||||
|
||||
it('calls upon savedObjectClient.resolve', async () => {
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/resolve/index-pattern/logstash-*')
|
||||
.expect(200);
|
||||
|
||||
expect(savedObjectsClient.resolve).toHaveBeenCalled();
|
||||
|
||||
const args = savedObjectsClient.resolve.mock.calls[0];
|
||||
expect(args).toEqual(['index-pattern', 'logstash-*']);
|
||||
});
|
||||
});
|
38
src/core/server/saved_objects/routes/resolve.ts
Normal file
38
src/core/server/saved_objects/routes/resolve.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '../../http';
|
||||
import { CoreUsageDataSetup } from '../../core_usage_data';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
}
|
||||
|
||||
export const registerResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
|
||||
router.get(
|
||||
{
|
||||
path: '/resolve/{type}/{id}',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async (context, req, res) => {
|
||||
const { type, id } = req.params;
|
||||
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsResolve({ request: req }).catch(() => {});
|
||||
|
||||
const result = await context.core.savedObjects.client.resolve(type, id);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
);
|
||||
};
|
|
@ -25,8 +25,10 @@ import { httpServerMock } from '../http/http_server.mocks';
|
|||
import { SavedObjectsClientFactoryProvider } from './service/lib';
|
||||
import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version';
|
||||
import { SavedObjectsRepository } from './service/lib/repository';
|
||||
import { registerCoreObjectTypes } from './object_types';
|
||||
|
||||
jest.mock('./service/lib/repository');
|
||||
jest.mock('./object_types');
|
||||
|
||||
describe('SavedObjectsService', () => {
|
||||
const createCoreContext = ({
|
||||
|
@ -67,6 +69,16 @@ describe('SavedObjectsService', () => {
|
|||
});
|
||||
|
||||
describe('#setup()', () => {
|
||||
it('calls registerCoreObjectTypes', async () => {
|
||||
const coreContext = createCoreContext();
|
||||
const soService = new SavedObjectsService(coreContext);
|
||||
|
||||
const mockedRegisterCoreObjectTypes = registerCoreObjectTypes as jest.Mock<any, any>;
|
||||
expect(mockedRegisterCoreObjectTypes).not.toHaveBeenCalled();
|
||||
await soService.setup(createSetupDeps());
|
||||
expect(mockedRegisterCoreObjectTypes).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('#setClientFactoryProvider', () => {
|
||||
it('registers the factory to the clientProvider', async () => {
|
||||
const coreContext = createCoreContext();
|
||||
|
@ -130,6 +142,7 @@ describe('SavedObjectsService', () => {
|
|||
|
||||
describe('#registerType', () => {
|
||||
it('registers the type to the internal typeRegistry', async () => {
|
||||
// we mocked registerCoreObjectTypes above, so this test case only reflects direct calls to the registerType method
|
||||
const coreContext = createCoreContext();
|
||||
const soService = new SavedObjectsService(coreContext);
|
||||
const setup = await soService.setup(createSetupDeps());
|
||||
|
|
|
@ -43,6 +43,7 @@ import { SavedObjectsImporter, ISavedObjectsImporter } from './import';
|
|||
import { registerRoutes } from './routes';
|
||||
import { ServiceStatus } from '../status';
|
||||
import { calculateStatus$ } from './status';
|
||||
import { registerCoreObjectTypes } from './object_types';
|
||||
|
||||
/**
|
||||
* Saved Objects is Kibana's data persistence mechanism allowing plugins to
|
||||
|
@ -305,6 +306,8 @@ export class SavedObjectsService
|
|||
migratorPromise: this.migrator$.pipe(first()).toPromise(),
|
||||
});
|
||||
|
||||
registerCoreObjectTypes(this.typeRegistry);
|
||||
|
||||
return {
|
||||
status$: calculateStatus$(
|
||||
this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())),
|
||||
|
|
|
@ -15,6 +15,7 @@ export {
|
|||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectSanitizedDoc,
|
||||
SavedObjectsRawDoc,
|
||||
SavedObjectsRawDocParseOptions,
|
||||
SavedObjectsRawDocSource,
|
||||
} from './types';
|
||||
export { SavedObjectsSerializer } from './serializer';
|
||||
|
|
|
@ -11,6 +11,7 @@ import { SavedObjectsSerializer } from './serializer';
|
|||
import { SavedObjectsRawDoc } from './types';
|
||||
import { typeRegistryMock } from '../saved_objects_type_registry.mock';
|
||||
import { encodeVersion } from '../version';
|
||||
import { LEGACY_URL_ALIAS_TYPE } from '../object_types';
|
||||
|
||||
let typeRegistry = typeRegistryMock.create();
|
||||
typeRegistry.isNamespaceAgnostic.mockReturnValue(true);
|
||||
|
@ -131,6 +132,27 @@ describe('#rawToSavedObject', () => {
|
|||
expect(expected).toEqual(actual);
|
||||
});
|
||||
|
||||
test('if specified it copies the _source.coreMigrationVersion property to coreMigrationVersion', () => {
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
_id: 'foo:bar',
|
||||
_source: {
|
||||
type: 'foo',
|
||||
coreMigrationVersion: '1.2.3',
|
||||
},
|
||||
});
|
||||
expect(actual).toHaveProperty('coreMigrationVersion', '1.2.3');
|
||||
});
|
||||
|
||||
test(`if _source.coreMigrationVersion is unspecified it doesn't set coreMigrationVersion`, () => {
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
_id: 'foo:bar',
|
||||
_source: {
|
||||
type: 'foo',
|
||||
},
|
||||
});
|
||||
expect(actual).not.toHaveProperty('coreMigrationVersion');
|
||||
});
|
||||
|
||||
test(`if version is unspecified it doesn't set version`, () => {
|
||||
const actual = singleNamespaceSerializer.rawToSavedObject({
|
||||
_id: 'foo:bar',
|
||||
|
@ -288,6 +310,7 @@ describe('#rawToSavedObject', () => {
|
|||
foo: '1.2.3',
|
||||
bar: '9.8.7',
|
||||
},
|
||||
coreMigrationVersion: '4.5.6',
|
||||
namespace: 'foo-namespace',
|
||||
updated_at: String(new Date()),
|
||||
references: [],
|
||||
|
@ -412,6 +435,41 @@ describe('#rawToSavedObject', () => {
|
|||
test(`doesn't copy _source.namespace to namespace`, () => {
|
||||
expect(actual).not.toHaveProperty('namespace');
|
||||
});
|
||||
|
||||
describe('with lax namespaceTreatment', () => {
|
||||
const options = { namespaceTreatment: 'lax' as 'lax' };
|
||||
|
||||
test(`removes type prefix from _id and, and does not copy _source.namespace to namespace`, () => {
|
||||
const _actual = multiNamespaceSerializer.rawToSavedObject(raw, options);
|
||||
expect(_actual).toHaveProperty('id', 'bar');
|
||||
expect(_actual).not.toHaveProperty('namespace');
|
||||
});
|
||||
|
||||
test(`removes type and namespace prefix from _id, and copies _source.namespace to namespace`, () => {
|
||||
const _id = `${raw._source.namespace}:${raw._id}`;
|
||||
const _actual = multiNamespaceSerializer.rawToSavedObject({ ...raw, _id }, options);
|
||||
expect(_actual).toHaveProperty('id', 'bar');
|
||||
expect(_actual).toHaveProperty('namespace', raw._source.namespace); // "baz"
|
||||
});
|
||||
|
||||
test(`removes type and namespace prefix from _id when the namespace matches the type`, () => {
|
||||
const _raw = createSampleDoc({ _id: 'foo:foo:bar', _source: { namespace: 'foo' } });
|
||||
const _actual = multiNamespaceSerializer.rawToSavedObject(_raw, options);
|
||||
expect(_actual).toHaveProperty('id', 'bar');
|
||||
expect(_actual).toHaveProperty('namespace', 'foo');
|
||||
});
|
||||
|
||||
test(`does not remove the entire _id when the namespace matches the type`, () => {
|
||||
// This is not a realistic/valid document, but we defensively check to ensure we aren't trimming the entire ID.
|
||||
// In this test case, a multi-namespace document has a raw ID with the type prefix "foo:" and an object ID of "foo:" (no namespace
|
||||
// prefix). This document *also* has a `namespace` field the same as the type, while it should not have a `namespace` field at all
|
||||
// since it has no namespace prefix in its raw ID.
|
||||
const _raw = createSampleDoc({ _id: 'foo:foo:', _source: { namespace: 'foo' } });
|
||||
const _actual = multiNamespaceSerializer.rawToSavedObject(_raw, options);
|
||||
expect(_actual).toHaveProperty('id', 'foo:');
|
||||
expect(_actual).not.toHaveProperty('namespace');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-namespace type with namespaces', () => {
|
||||
|
@ -515,6 +573,25 @@ describe('#savedObjectToRaw', () => {
|
|||
expect(actual._source).not.toHaveProperty('migrationVersion');
|
||||
});
|
||||
|
||||
test('it copies the coreMigrationVersion property to _source.coreMigrationVersion', () => {
|
||||
const actual = singleNamespaceSerializer.savedObjectToRaw({
|
||||
type: '',
|
||||
attributes: {},
|
||||
coreMigrationVersion: '1.2.3',
|
||||
} as any);
|
||||
|
||||
expect(actual._source).toHaveProperty('coreMigrationVersion', '1.2.3');
|
||||
});
|
||||
|
||||
test(`if unspecified it doesn't add coreMigrationVersion property to _source`, () => {
|
||||
const actual = singleNamespaceSerializer.savedObjectToRaw({
|
||||
type: '',
|
||||
attributes: {},
|
||||
} as any);
|
||||
|
||||
expect(actual._source).not.toHaveProperty('coreMigrationVersion');
|
||||
});
|
||||
|
||||
test('it decodes the version property to _seq_no and _primary_term', () => {
|
||||
const actual = singleNamespaceSerializer.savedObjectToRaw({
|
||||
type: '',
|
||||
|
@ -841,6 +918,116 @@ describe('#isRawSavedObject', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('multi-namespace type with a namespace', () => {
|
||||
test('is true if the id is prefixed with type and the type matches', () => {
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject({
|
||||
_id: 'hello:world',
|
||||
_source: {
|
||||
type: 'hello',
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
})
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('is false if the id is not prefixed', () => {
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject({
|
||||
_id: 'world',
|
||||
_source: {
|
||||
type: 'hello',
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is false if the id is prefixed with type and namespace', () => {
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject({
|
||||
_id: 'foo:hello:world',
|
||||
_source: {
|
||||
type: 'hello',
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is true if the id is prefixed with type and namespace, and namespaceTreatment is lax', () => {
|
||||
const options = { namespaceTreatment: 'lax' as 'lax' };
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject(
|
||||
{
|
||||
_id: 'foo:hello:world',
|
||||
_source: {
|
||||
type: 'hello',
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
},
|
||||
options
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test(`is false if the type prefix omits the :`, () => {
|
||||
expect(
|
||||
namespaceAgnosticSerializer.isRawSavedObject({
|
||||
_id: 'helloworld',
|
||||
_source: {
|
||||
type: 'hello',
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is false if the type attribute is missing', () => {
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject({
|
||||
_id: 'hello:world',
|
||||
_source: {
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
} as any,
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is false if the type attribute does not match the id', () => {
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject({
|
||||
_id: 'hello:world',
|
||||
_source: {
|
||||
type: 'jam',
|
||||
jam: {},
|
||||
hello: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('is false if there is no [type] attribute', () => {
|
||||
expect(
|
||||
multiNamespaceSerializer.isRawSavedObject({
|
||||
_id: 'hello:world',
|
||||
_source: {
|
||||
type: 'hello',
|
||||
jam: {},
|
||||
namespace: 'foo',
|
||||
},
|
||||
})
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('namespace-agnostic type with a namespace', () => {
|
||||
test('is true if the id is prefixed with type and the type matches', () => {
|
||||
expect(
|
||||
|
@ -950,6 +1137,13 @@ describe('#generateRawId', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('multi-namespace type with a namespace', () => {
|
||||
test(`uses the id that is specified and doesn't prefix the namespace`, () => {
|
||||
const id = multiNamespaceSerializer.generateRawId('foo', 'hello', 'world');
|
||||
expect(id).toEqual('hello:world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('namespace-agnostic type with a namespace', () => {
|
||||
test(`uses the id that is specified and doesn't prefix the namespace`, () => {
|
||||
const id = namespaceAgnosticSerializer.generateRawId('foo', 'hello', 'world');
|
||||
|
@ -957,3 +1151,24 @@ describe('#generateRawId', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateRawLegacyUrlAliasId', () => {
|
||||
describe(`returns expected value`, () => {
|
||||
const expected = `${LEGACY_URL_ALIAS_TYPE}:foo:bar:baz`;
|
||||
|
||||
test(`for single-namespace types`, () => {
|
||||
const id = singleNamespaceSerializer.generateRawLegacyUrlAliasId('foo', 'bar', 'baz');
|
||||
expect(id).toEqual(expected);
|
||||
});
|
||||
|
||||
test(`for multi-namespace types`, () => {
|
||||
const id = multiNamespaceSerializer.generateRawLegacyUrlAliasId('foo', 'bar', 'baz');
|
||||
expect(id).toEqual(expected);
|
||||
});
|
||||
|
||||
test(`for namespace-agnostic types`, () => {
|
||||
const id = namespaceAgnosticSerializer.generateRawLegacyUrlAliasId('foo', 'bar', 'baz');
|
||||
expect(id).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,14 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { LEGACY_URL_ALIAS_TYPE } from '../object_types';
|
||||
import { decodeVersion, encodeVersion } from '../version';
|
||||
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
|
||||
import { SavedObjectsRawDoc, SavedObjectSanitizedDoc } from './types';
|
||||
import {
|
||||
SavedObjectsRawDoc,
|
||||
SavedObjectSanitizedDoc,
|
||||
SavedObjectsRawDocParseOptions,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* A serializer that can be used to manually convert {@link SavedObjectsRawDoc | raw} or
|
||||
|
@ -30,42 +35,60 @@ export class SavedObjectsSerializer {
|
|||
/**
|
||||
* Determines whether or not the raw document can be converted to a saved object.
|
||||
*
|
||||
* @param {SavedObjectsRawDoc} rawDoc - The raw ES document to be tested
|
||||
* @param {SavedObjectsRawDoc} doc - The raw ES document to be tested
|
||||
* @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document.
|
||||
*/
|
||||
public isRawSavedObject(rawDoc: SavedObjectsRawDoc) {
|
||||
const { type, namespace } = rawDoc._source;
|
||||
const namespacePrefix =
|
||||
namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : '';
|
||||
return Boolean(
|
||||
type &&
|
||||
rawDoc._id.startsWith(`${namespacePrefix}${type}:`) &&
|
||||
rawDoc._source.hasOwnProperty(type)
|
||||
);
|
||||
public isRawSavedObject(doc: SavedObjectsRawDoc, options: SavedObjectsRawDocParseOptions = {}) {
|
||||
const { namespaceTreatment = 'strict' } = options;
|
||||
const { _id, _source } = doc;
|
||||
const { type, namespace } = _source;
|
||||
if (!type) {
|
||||
return false;
|
||||
}
|
||||
const { idMatchesPrefix } = this.parseIdPrefix(namespace, type, _id, namespaceTreatment);
|
||||
return idMatchesPrefix && _source.hasOwnProperty(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a document from the format that is stored in elasticsearch to the saved object client format.
|
||||
*
|
||||
* @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format.
|
||||
* @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(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc {
|
||||
public rawToSavedObject(
|
||||
doc: SavedObjectsRawDoc,
|
||||
options: SavedObjectsRawDocParseOptions = {}
|
||||
): SavedObjectSanitizedDoc {
|
||||
const { namespaceTreatment = 'strict' } = options;
|
||||
const { _id, _source, _seq_no, _primary_term } = doc;
|
||||
const { type, namespace, namespaces, originId } = _source;
|
||||
const {
|
||||
type,
|
||||
namespaces,
|
||||
originId,
|
||||
migrationVersion,
|
||||
references,
|
||||
coreMigrationVersion,
|
||||
} = _source;
|
||||
|
||||
const version =
|
||||
_seq_no != null || _primary_term != null
|
||||
? encodeVersion(_seq_no!, _primary_term!)
|
||||
: undefined;
|
||||
const { id, namespace } = this.trimIdPrefix(_source.namespace, type, _id, namespaceTreatment);
|
||||
const includeNamespace =
|
||||
namespace && (namespaceTreatment === 'lax' || this.registry.isSingleNamespace(type));
|
||||
const includeNamespaces = this.registry.isMultiNamespace(type);
|
||||
|
||||
return {
|
||||
type,
|
||||
id: this.trimIdPrefix(namespace, type, _id),
|
||||
...(namespace && this.registry.isSingleNamespace(type) && { namespace }),
|
||||
...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }),
|
||||
id,
|
||||
...(includeNamespace && { namespace }),
|
||||
...(includeNamespaces && { namespaces }),
|
||||
...(originId && { originId }),
|
||||
attributes: _source[type],
|
||||
references: _source.references || [],
|
||||
...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }),
|
||||
references: references || [],
|
||||
...(migrationVersion && { migrationVersion }),
|
||||
...(coreMigrationVersion && { coreMigrationVersion }),
|
||||
...(_source.updated_at && { updated_at: _source.updated_at }),
|
||||
...(version && { version }),
|
||||
};
|
||||
|
@ -89,6 +112,7 @@ export class SavedObjectsSerializer {
|
|||
updated_at,
|
||||
version,
|
||||
references,
|
||||
coreMigrationVersion,
|
||||
} = savedObj;
|
||||
const source = {
|
||||
[type]: attributes,
|
||||
|
@ -98,6 +122,7 @@ export class SavedObjectsSerializer {
|
|||
...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }),
|
||||
...(originId && { originId }),
|
||||
...(migrationVersion && { migrationVersion }),
|
||||
...(coreMigrationVersion && { coreMigrationVersion }),
|
||||
...(updated_at && { updated_at }),
|
||||
};
|
||||
|
||||
|
@ -121,22 +146,77 @@ export class SavedObjectsSerializer {
|
|||
return `${namespacePrefix}${type}:${id}`;
|
||||
}
|
||||
|
||||
private trimIdPrefix(namespace: string | undefined, type: string, id: string) {
|
||||
/**
|
||||
* Given a saved object type and id, generates the compound id that is stored in the raw document for its legacy URL alias.
|
||||
*
|
||||
* @param {string} namespace - The namespace of the saved object
|
||||
* @param {string} type - The saved object type
|
||||
* @param {string} id - The id of the saved object
|
||||
*/
|
||||
public generateRawLegacyUrlAliasId(namespace: string, type: string, id: string) {
|
||||
return `${LEGACY_URL_ALIAS_TYPE}:${namespace}:${type}:${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a document's source namespace, type, and raw ID, trim the ID prefix (based on the namespaceType), returning the object ID and the
|
||||
* detected namespace. A single-namespace object is only considered to exist in a namespace if its raw ID is prefixed by that *and* it has
|
||||
* the namespace field in its source.
|
||||
*/
|
||||
private trimIdPrefix(
|
||||
sourceNamespace: string | undefined,
|
||||
type: string,
|
||||
id: string,
|
||||
namespaceTreatment: 'strict' | 'lax'
|
||||
) {
|
||||
assertNonEmptyString(id, 'document id');
|
||||
assertNonEmptyString(type, 'saved object type');
|
||||
|
||||
const namespacePrefix =
|
||||
namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : '';
|
||||
const prefix = `${namespacePrefix}${type}:`;
|
||||
const { prefix, idMatchesPrefix, namespace } = this.parseIdPrefix(
|
||||
sourceNamespace,
|
||||
type,
|
||||
id,
|
||||
namespaceTreatment
|
||||
);
|
||||
return {
|
||||
id: idMatchesPrefix ? id.slice(prefix.length) : id,
|
||||
namespace,
|
||||
};
|
||||
}
|
||||
|
||||
if (!id.startsWith(prefix)) {
|
||||
return id;
|
||||
private parseIdPrefix(
|
||||
sourceNamespace: string | undefined,
|
||||
type: string,
|
||||
id: string,
|
||||
namespaceTreatment: 'strict' | 'lax'
|
||||
) {
|
||||
let prefix: string; // the prefix that is used to validate this raw object ID
|
||||
let namespace: string | undefined; // the namespace that is in the raw object ID (only for single-namespace objects)
|
||||
const parseFlexibly = namespaceTreatment === 'lax' && this.registry.isMultiNamespace(type);
|
||||
if (sourceNamespace && (this.registry.isSingleNamespace(type) || parseFlexibly)) {
|
||||
prefix = `${sourceNamespace}:${type}:`;
|
||||
if (parseFlexibly && !checkIdMatchesPrefix(id, prefix)) {
|
||||
prefix = `${type}:`;
|
||||
} else {
|
||||
// this is either a single-namespace object, or is being converted into a multi-namespace object
|
||||
namespace = sourceNamespace;
|
||||
}
|
||||
} else {
|
||||
// there is no source namespace, OR there is a source namespace but this is not a single-namespace object
|
||||
prefix = `${type}:`;
|
||||
}
|
||||
|
||||
return id.slice(prefix.length);
|
||||
return {
|
||||
prefix,
|
||||
idMatchesPrefix: checkIdMatchesPrefix(id, prefix),
|
||||
namespace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function checkIdMatchesPrefix(id: string, prefix: string) {
|
||||
return id.startsWith(prefix) && id.length > prefix.length;
|
||||
}
|
||||
|
||||
function assertNonEmptyString(value: string, name: string) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
throw new TypeError(`Expected "${value}" to be a ${name}`);
|
||||
|
|
|
@ -43,6 +43,7 @@ interface SavedObjectDoc<T = unknown> {
|
|||
namespace?: string;
|
||||
namespaces?: string[];
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
coreMigrationVersion?: string;
|
||||
version?: string;
|
||||
updated_at?: string;
|
||||
originId?: string;
|
||||
|
@ -68,3 +69,19 @@ export type SavedObjectUnsanitizedDoc<T = unknown> = SavedObjectDoc<T> & Partial
|
|||
* @public
|
||||
*/
|
||||
export type SavedObjectSanitizedDoc<T = unknown> = SavedObjectDoc<T> & Referencable;
|
||||
|
||||
/**
|
||||
* Options that can be specified when using the saved objects serializer to parse a raw document.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsRawDocParseOptions {
|
||||
/**
|
||||
* Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously
|
||||
* single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade
|
||||
* migrations.
|
||||
*
|
||||
* If not specified, the default treatment is `strict`.
|
||||
*/
|
||||
namespaceTreatment?: 'strict' | 'lax';
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { includedFields } from './included_fields';
|
||||
|
||||
const BASE_FIELD_COUNT = 9;
|
||||
const BASE_FIELD_COUNT = 10;
|
||||
|
||||
describe('includedFields', () => {
|
||||
it('returns undefined if fields are not provided', () => {
|
||||
|
@ -32,6 +32,7 @@ Array [
|
|||
"type",
|
||||
"references",
|
||||
"migrationVersion",
|
||||
"coreMigrationVersion",
|
||||
"updated_at",
|
||||
"originId",
|
||||
"foo",
|
||||
|
@ -66,6 +67,7 @@ Array [
|
|||
"type",
|
||||
"references",
|
||||
"migrationVersion",
|
||||
"coreMigrationVersion",
|
||||
"updated_at",
|
||||
"originId",
|
||||
"foo",
|
||||
|
|
|
@ -30,6 +30,7 @@ export function includedFields(type: string | string[] = '*', fields?: string[]
|
|||
.concat('type')
|
||||
.concat('references')
|
||||
.concat('migrationVersion')
|
||||
.concat('coreMigrationVersion')
|
||||
.concat('updated_at')
|
||||
.concat('originId')
|
||||
.concat(fields); // v5 compatibility
|
||||
|
|
|
@ -17,6 +17,7 @@ const create = (): jest.Mocked<ISavedObjectsRepository> => ({
|
|||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
resolve: jest.fn(),
|
||||
update: jest.fn(),
|
||||
addToNamespaces: jest.fn(),
|
||||
deleteFromNamespaces: jest.fn(),
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ALL_NAMESPACES_STRING } from './utils';
|
|||
import { SavedObjectsSerializer } from '../../serialization';
|
||||
import { encodeHitVersion } from '../../version';
|
||||
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import { LEGACY_URL_ALIAS_TYPE } from '../../object_types';
|
||||
import { DocumentMigrator } from '../../migrations/core/document_migrator';
|
||||
import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock';
|
||||
import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks';
|
||||
|
@ -44,6 +45,7 @@ describe('SavedObjectsRepository', () => {
|
|||
const mockVersionProps = { _seq_no: 1, _primary_term: 1 };
|
||||
const mockVersion = encodeHitVersion(mockVersionProps);
|
||||
|
||||
const KIBANA_VERSION = '2.0.0';
|
||||
const CUSTOM_INDEX_TYPE = 'customIndex';
|
||||
const NAMESPACE_AGNOSTIC_TYPE = 'globalType';
|
||||
const MULTI_NAMESPACE_TYPE = 'shareableType';
|
||||
|
@ -142,7 +144,7 @@ describe('SavedObjectsRepository', () => {
|
|||
|
||||
const documentMigrator = new DocumentMigrator({
|
||||
typeRegistry: registry,
|
||||
kibanaVersion: '2.0.0',
|
||||
kibanaVersion: KIBANA_VERSION,
|
||||
log: {},
|
||||
});
|
||||
|
||||
|
@ -216,6 +218,7 @@ describe('SavedObjectsRepository', () => {
|
|||
rawToSavedObject: jest.fn(),
|
||||
savedObjectToRaw: jest.fn(),
|
||||
generateRawId: jest.fn(),
|
||||
generateRawLegacyUrlAliasId: jest.fn(),
|
||||
trimIdPrefix: jest.fn(),
|
||||
};
|
||||
const _serializer = new SavedObjectsSerializer(registry);
|
||||
|
@ -501,6 +504,7 @@ describe('SavedObjectsRepository', () => {
|
|||
const expectSuccessResult = (obj) => ({
|
||||
...obj,
|
||||
migrationVersion: { [obj.type]: '1.1.1' },
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
version: mockVersion,
|
||||
namespaces: obj.namespaces ?? [obj.namespace ?? 'default'],
|
||||
...mockTimestampFields,
|
||||
|
@ -954,6 +958,7 @@ describe('SavedObjectsRepository', () => {
|
|||
...response.items[0].create,
|
||||
_source: {
|
||||
...response.items[0].create._source,
|
||||
coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation
|
||||
namespaces: response.items[0].create._source.namespaces,
|
||||
},
|
||||
_id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/),
|
||||
|
@ -962,6 +967,7 @@ describe('SavedObjectsRepository', () => {
|
|||
...response.items[1].create,
|
||||
_source: {
|
||||
...response.items[1].create._source,
|
||||
coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation
|
||||
namespaces: response.items[1].create._source.namespaces,
|
||||
},
|
||||
});
|
||||
|
@ -2140,6 +2146,7 @@ describe('SavedObjectsRepository', () => {
|
|||
references,
|
||||
namespaces: [namespace ?? 'default'],
|
||||
migrationVersion: { [type]: '1.1.1' },
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2724,6 +2731,7 @@ describe('SavedObjectsRepository', () => {
|
|||
'type',
|
||||
'references',
|
||||
'migrationVersion',
|
||||
'coreMigrationVersion',
|
||||
'updated_at',
|
||||
'originId',
|
||||
'title',
|
||||
|
@ -3254,6 +3262,231 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#resolve', () => {
|
||||
const type = 'index-pattern';
|
||||
const id = 'logstash-*';
|
||||
const aliasTargetId = 'some-other-id'; // only used for 'aliasMatch' and 'conflict' outcomes
|
||||
const namespace = 'foo-namespace';
|
||||
|
||||
const getMockAliasDocument = (resolveCounter) => ({
|
||||
body: {
|
||||
get: {
|
||||
_source: {
|
||||
[LEGACY_URL_ALIAS_TYPE]: {
|
||||
targetId: aliasTargetId,
|
||||
...(resolveCounter && { resolveCounter }),
|
||||
// other fields are not used by the repository
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('outcomes', () => {
|
||||
describe('error', () => {
|
||||
const expectNotFoundError = async (type, id, options) => {
|
||||
await expect(savedObjectsRepository.resolve(type, id, options)).rejects.toThrowError(
|
||||
createGenericNotFoundError(type, id)
|
||||
);
|
||||
};
|
||||
|
||||
it('because type is invalid', async () => {
|
||||
await expectNotFoundError('unknownType', id);
|
||||
expect(client.update).not.toHaveBeenCalled();
|
||||
expect(client.get).not.toHaveBeenCalled();
|
||||
expect(client.mget).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('because type is hidden', async () => {
|
||||
await expectNotFoundError(HIDDEN_TYPE, id);
|
||||
expect(client.update).not.toHaveBeenCalled();
|
||||
expect(client.get).not.toHaveBeenCalled();
|
||||
expect(client.mget).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('because alias is not used and actual object is not found', async () => {
|
||||
const options = { namespace: undefined };
|
||||
const response = { found: false };
|
||||
client.get.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target
|
||||
);
|
||||
|
||||
await expectNotFoundError(type, id, options);
|
||||
expect(client.update).not.toHaveBeenCalled();
|
||||
expect(client.get).toHaveBeenCalledTimes(1); // retrieved actual target
|
||||
expect(client.mget).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('because actual object and alias object are both not found', async () => {
|
||||
const options = { namespace };
|
||||
const objectResults = [
|
||||
{ type, id, found: false },
|
||||
{ type, id: aliasTargetId, found: false },
|
||||
];
|
||||
client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object
|
||||
const response = getMockMgetResponse(objectResults, options.namespace);
|
||||
client.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target
|
||||
);
|
||||
|
||||
await expectNotFoundError(type, id, options);
|
||||
expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object
|
||||
expect(client.get).not.toHaveBeenCalled();
|
||||
expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target
|
||||
});
|
||||
});
|
||||
|
||||
describe('exactMatch', () => {
|
||||
it('because namespace is undefined', async () => {
|
||||
const options = { namespace: undefined };
|
||||
const response = getMockGetResponse({ type, id });
|
||||
client.get.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target
|
||||
);
|
||||
|
||||
const result = await savedObjectsRepository.resolve(type, id, options);
|
||||
expect(client.update).not.toHaveBeenCalled();
|
||||
expect(client.get).toHaveBeenCalledTimes(1); // retrieved actual target
|
||||
expect(client.mget).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
saved_object: expect.objectContaining({ type, id }),
|
||||
outcome: 'exactMatch',
|
||||
});
|
||||
});
|
||||
|
||||
describe('because alias is not used', () => {
|
||||
const expectExactMatchResult = async (aliasResult) => {
|
||||
const options = { namespace };
|
||||
client.update.mockResolvedValueOnce(aliasResult); // for alias object
|
||||
const response = getMockGetResponse({ type, id }, options.namespace);
|
||||
client.get.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target
|
||||
);
|
||||
|
||||
const result = await savedObjectsRepository.resolve(type, id, options);
|
||||
expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object
|
||||
expect(client.get).toHaveBeenCalledTimes(1); // retrieved actual target
|
||||
expect(client.mget).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
saved_object: expect.objectContaining({ type, id }),
|
||||
outcome: 'exactMatch',
|
||||
});
|
||||
};
|
||||
|
||||
it('since alias call resulted in 404', async () => {
|
||||
await expectExactMatchResult({ statusCode: 404 });
|
||||
});
|
||||
|
||||
it('since alias is not found', async () => {
|
||||
await expectExactMatchResult({ body: { get: { found: false } } });
|
||||
});
|
||||
|
||||
it('since alias is disabled', async () => {
|
||||
await expectExactMatchResult({
|
||||
body: { get: { _source: { [LEGACY_URL_ALIAS_TYPE]: { disabled: true } } } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('because alias is used', () => {
|
||||
const expectExactMatchResult = async (objectResults) => {
|
||||
const options = { namespace };
|
||||
client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object
|
||||
const response = getMockMgetResponse(objectResults, options.namespace);
|
||||
client.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target and alias target
|
||||
);
|
||||
|
||||
const result = await savedObjectsRepository.resolve(type, id, options);
|
||||
expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object
|
||||
expect(client.get).not.toHaveBeenCalled();
|
||||
expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target
|
||||
expect(result).toEqual({
|
||||
saved_object: expect.objectContaining({ type, id }),
|
||||
outcome: 'exactMatch',
|
||||
});
|
||||
};
|
||||
|
||||
it('but alias target is not found', async () => {
|
||||
const objects = [
|
||||
{ type, id },
|
||||
{ type, id: aliasTargetId, found: false },
|
||||
];
|
||||
await expectExactMatchResult(objects);
|
||||
});
|
||||
|
||||
it('but alias target does not exist in this namespace', async () => {
|
||||
const objects = [
|
||||
{ type: MULTI_NAMESPACE_TYPE, id }, // correct namespace field is added by getMockMgetResponse
|
||||
{ type: MULTI_NAMESPACE_TYPE, id: aliasTargetId, namespace: `not-${namespace}` }, // overrides namespace field that would otherwise be added by getMockMgetResponse
|
||||
];
|
||||
await expectExactMatchResult(objects);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aliasMatch', () => {
|
||||
const expectAliasMatchResult = async (objectResults) => {
|
||||
const options = { namespace };
|
||||
client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object
|
||||
const response = getMockMgetResponse(objectResults, options.namespace);
|
||||
client.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target and alias target
|
||||
);
|
||||
|
||||
const result = await savedObjectsRepository.resolve(type, id, options);
|
||||
expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object
|
||||
expect(client.get).not.toHaveBeenCalled();
|
||||
expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target
|
||||
expect(result).toEqual({
|
||||
saved_object: expect.objectContaining({ type, id: aliasTargetId }),
|
||||
outcome: 'aliasMatch',
|
||||
});
|
||||
};
|
||||
|
||||
it('because actual target is not found', async () => {
|
||||
const objects = [
|
||||
{ type, id, found: false },
|
||||
{ type, id: aliasTargetId },
|
||||
];
|
||||
await expectAliasMatchResult(objects);
|
||||
});
|
||||
|
||||
it('because actual target does not exist in this namespace', async () => {
|
||||
const objects = [
|
||||
{ type: MULTI_NAMESPACE_TYPE, id, namespace: `not-${namespace}` }, // overrides namespace field that would otherwise be added by getMockMgetResponse
|
||||
{ type: MULTI_NAMESPACE_TYPE, id: aliasTargetId }, // correct namespace field is added by getMockMgetResponse
|
||||
];
|
||||
await expectAliasMatchResult(objects);
|
||||
});
|
||||
});
|
||||
|
||||
describe('conflict', () => {
|
||||
it('because actual target and alias target are both found', async () => {
|
||||
const options = { namespace };
|
||||
const objectResults = [
|
||||
{ type, id }, // correct namespace field is added by getMockMgetResponse
|
||||
{ type, id: aliasTargetId }, // correct namespace field is added by getMockMgetResponse
|
||||
];
|
||||
client.update.mockResolvedValueOnce(getMockAliasDocument()); // for alias object
|
||||
const response = getMockMgetResponse(objectResults, options.namespace);
|
||||
client.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target and alias target
|
||||
);
|
||||
|
||||
const result = await savedObjectsRepository.resolve(type, id, options);
|
||||
expect(client.update).toHaveBeenCalledTimes(1); // retrieved alias object
|
||||
expect(client.get).not.toHaveBeenCalled();
|
||||
expect(client.mget).toHaveBeenCalledTimes(1); // retrieved actual target and alias target
|
||||
expect(result).toEqual({
|
||||
saved_object: expect.objectContaining({ type, id }),
|
||||
outcome: 'conflict',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementCounter', () => {
|
||||
const type = 'config';
|
||||
const id = 'one';
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
SavedObjectsDeleteFromNamespacesResponse,
|
||||
SavedObjectsRemoveReferencesToOptions,
|
||||
SavedObjectsRemoveReferencesToResponse,
|
||||
SavedObjectsResolveResponse,
|
||||
} from '../saved_objects_client';
|
||||
import {
|
||||
SavedObject,
|
||||
|
@ -55,6 +56,7 @@ import {
|
|||
SavedObjectsMigrationVersion,
|
||||
MutatingOperationRefreshSetting,
|
||||
} from '../../types';
|
||||
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
|
||||
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import { validateConvertFilterToKueryNode } from './filter_utils';
|
||||
import {
|
||||
|
@ -920,25 +922,7 @@ export class SavedObjectsRepository {
|
|||
} as any) as SavedObject<T>;
|
||||
}
|
||||
|
||||
const { originId, updated_at: updatedAt } = doc._source;
|
||||
let namespaces = [];
|
||||
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,
|
||||
};
|
||||
return this.getSavedObjectFromSource(type, id, doc);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -978,26 +962,122 @@ export class SavedObjectsRepository {
|
|||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
|
||||
}
|
||||
|
||||
const { originId, updated_at: updatedAt } = body._source;
|
||||
return this.getSavedObjectFromSource(type, id, body);
|
||||
}
|
||||
|
||||
let namespaces: string[] = [];
|
||||
if (!this._registry.isNamespaceAgnostic(type)) {
|
||||
namespaces = body._source.namespaces ?? [
|
||||
SavedObjectsUtils.namespaceIdToString(body._source.namespace),
|
||||
];
|
||||
/**
|
||||
* Resolves a single object, using any legacy URL alias if it exists
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} id
|
||||
* @param {object} [options={}]
|
||||
* @property {string} [options.namespace]
|
||||
* @returns {promise} - { saved_object, outcome }
|
||||
*/
|
||||
async resolve<T = unknown>(
|
||||
type: string,
|
||||
id: string,
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
): Promise<SavedObjectsResolveResponse<T>> {
|
||||
if (!this._allowedTypes.includes(type)) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
namespaces,
|
||||
...(originId && { originId }),
|
||||
...(updatedAt && { updated_at: updatedAt }),
|
||||
version: encodeHitVersion(body),
|
||||
attributes: body._source[type],
|
||||
references: body._source.references || [],
|
||||
migrationVersion: body._source.migrationVersion,
|
||||
};
|
||||
const namespace = normalizeNamespace(options.namespace);
|
||||
if (namespace === undefined) {
|
||||
// legacy URL aliases cannot exist for the default namespace; just attempt to get the object
|
||||
return this.resolveExactMatch(type, id, options);
|
||||
}
|
||||
|
||||
const rawAliasId = this._serializer.generateRawLegacyUrlAliasId(namespace, type, id);
|
||||
const time = this._getCurrentTime();
|
||||
|
||||
// retrieve the alias, and if it is not disabled, update it
|
||||
const aliasResponse = await this.client.update(
|
||||
{
|
||||
id: rawAliasId,
|
||||
index: this.getIndexForType(LEGACY_URL_ALIAS_TYPE),
|
||||
refresh: false,
|
||||
_source: 'true',
|
||||
body: {
|
||||
script: {
|
||||
source: `
|
||||
if (ctx._source[params.type].disabled != true) {
|
||||
if (ctx._source[params.type].resolveCounter == null) {
|
||||
ctx._source[params.type].resolveCounter = 1;
|
||||
}
|
||||
else {
|
||||
ctx._source[params.type].resolveCounter += 1;
|
||||
}
|
||||
ctx._source[params.type].lastResolved = params.time;
|
||||
ctx._source.updated_at = params.time;
|
||||
}
|
||||
`,
|
||||
lang: 'painless',
|
||||
params: {
|
||||
type: LEGACY_URL_ALIAS_TYPE,
|
||||
time,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ ignore: [404] }
|
||||
);
|
||||
|
||||
if (
|
||||
aliasResponse.statusCode === 404 ||
|
||||
aliasResponse.body.get.found === false ||
|
||||
aliasResponse.body.get._source[LEGACY_URL_ALIAS_TYPE]?.disabled === true
|
||||
) {
|
||||
// no legacy URL alias exists, or one exists but it's disabled; just attempt to get the object
|
||||
return this.resolveExactMatch(type, id, options);
|
||||
}
|
||||
const legacyUrlAlias: LegacyUrlAlias = aliasResponse.body.get._source[LEGACY_URL_ALIAS_TYPE];
|
||||
const objectIndex = this.getIndexForType(type);
|
||||
const bulkGetResponse = await this.client.mget(
|
||||
{
|
||||
body: {
|
||||
docs: [
|
||||
{
|
||||
// attempt to find an exact match for the given ID
|
||||
_id: this._serializer.generateRawId(namespace, type, id),
|
||||
_index: objectIndex,
|
||||
},
|
||||
{
|
||||
// also attempt to find a match for the legacy URL alias target ID
|
||||
_id: this._serializer.generateRawId(namespace, type, legacyUrlAlias.targetId),
|
||||
_index: objectIndex,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ ignore: [404] }
|
||||
);
|
||||
|
||||
const exactMatchDoc = bulkGetResponse?.body.docs[0];
|
||||
const aliasMatchDoc = bulkGetResponse?.body.docs[1];
|
||||
const foundExactMatch =
|
||||
exactMatchDoc.found && this.rawDocExistsInNamespace(exactMatchDoc, namespace);
|
||||
const foundAliasMatch =
|
||||
aliasMatchDoc.found && this.rawDocExistsInNamespace(aliasMatchDoc, namespace);
|
||||
|
||||
if (foundExactMatch && foundAliasMatch) {
|
||||
return {
|
||||
saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc),
|
||||
outcome: 'conflict',
|
||||
};
|
||||
} else if (foundExactMatch) {
|
||||
return {
|
||||
saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc),
|
||||
outcome: 'exactMatch',
|
||||
};
|
||||
} else if (foundAliasMatch) {
|
||||
return {
|
||||
saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc),
|
||||
outcome: 'aliasMatch',
|
||||
};
|
||||
}
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1718,7 +1798,7 @@ export class SavedObjectsRepository {
|
|||
if (this._registry.isSingleNamespace(type)) {
|
||||
savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)];
|
||||
}
|
||||
return omit(savedObject, 'namespace') as SavedObject<T>;
|
||||
return omit(savedObject, ['namespace']) as SavedObject<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1814,6 +1894,43 @@ export class SavedObjectsRepository {
|
|||
}
|
||||
return body as SavedObjectsRawDoc;
|
||||
}
|
||||
|
||||
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,
|
||||
options: SavedObjectsBaseOptions
|
||||
): Promise<SavedObjectsResolveResponse<T>> {
|
||||
const object = await this.get<T>(type, id, options);
|
||||
return { saved_object: object, outcome: 'exactMatch' };
|
||||
}
|
||||
}
|
||||
|
||||
function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) {
|
||||
|
|
|
@ -20,6 +20,7 @@ const create = () =>
|
|||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
resolve: jest.fn(),
|
||||
update: jest.fn(),
|
||||
addToNamespaces: jest.fn(),
|
||||
deleteFromNamespaces: jest.fn(),
|
||||
|
|
|
@ -115,6 +115,22 @@ test(`#get`, async () => {
|
|||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#resolve`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = {
|
||||
resolve: jest.fn().mockResolvedValue(returnValue),
|
||||
};
|
||||
const client = new SavedObjectsClient(mockRepository);
|
||||
|
||||
const type = Symbol();
|
||||
const id = Symbol();
|
||||
const options = Symbol();
|
||||
const result = await client.resolve(type, id, options);
|
||||
|
||||
expect(mockRepository.resolve).toHaveBeenCalledWith(type, id, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#update`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = {
|
||||
|
|
|
@ -34,6 +34,16 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
|
|||
version?: string;
|
||||
/** {@inheritDoc SavedObjectsMigrationVersion} */
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
/**
|
||||
* A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current
|
||||
* Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the
|
||||
* current Kibana version, it will result in an error.
|
||||
*
|
||||
* @remarks
|
||||
* Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion`
|
||||
* field set and you want to create it again.
|
||||
*/
|
||||
coreMigrationVersion?: string;
|
||||
references?: SavedObjectReference[];
|
||||
/** The Elasticsearch Refresh setting for this operation */
|
||||
refresh?: MutatingOperationRefreshSetting;
|
||||
|
@ -60,6 +70,16 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
|
|||
references?: SavedObjectReference[];
|
||||
/** {@inheritDoc SavedObjectsMigrationVersion} */
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
/**
|
||||
* A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current
|
||||
* Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the
|
||||
* current Kibana version, it will result in an error.
|
||||
*
|
||||
* @remarks
|
||||
* Do not attempt to set this manually. It should only be used if you retrieved an existing object that had the `coreMigrationVersion`
|
||||
* field set and you want to create it again.
|
||||
*/
|
||||
coreMigrationVersion?: string;
|
||||
/** Optional ID of the original saved object, if this object's `id` was regenerated */
|
||||
originId?: string;
|
||||
/**
|
||||
|
@ -273,6 +293,24 @@ export interface SavedObjectsUpdateResponse<T = unknown>
|
|||
references: SavedObjectReference[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsResolveResponse<T = unknown> {
|
||||
saved_object: SavedObject<T>;
|
||||
/**
|
||||
* The outcome for a successful `resolve` call is one of the following values:
|
||||
*
|
||||
* * `'exactMatch'` -- One document exactly matched the given ID.
|
||||
* * `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different
|
||||
* than the given ID.
|
||||
* * `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the
|
||||
* `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.
|
||||
*/
|
||||
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
|
@ -379,6 +417,21 @@ export class SavedObjectsClient {
|
|||
return await this._repository.get(type, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a single object, using any legacy URL alias if it exists
|
||||
*
|
||||
* @param type - The type of SavedObject to retrieve
|
||||
* @param id - The ID of the SavedObject to retrieve
|
||||
* @param options
|
||||
*/
|
||||
async resolve<T = unknown>(
|
||||
type: string,
|
||||
id: string,
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
): Promise<SavedObjectsResolveResponse<T>> {
|
||||
return await this._repository.resolve(type, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an SavedObject
|
||||
*
|
||||
|
|
|
@ -242,6 +242,41 @@ export interface SavedObjectsType {
|
|||
* An optional map of {@link SavedObjectMigrationFn | migrations} or a function returning a map of {@link SavedObjectMigrationFn | migrations} to be used to migrate the type.
|
||||
*/
|
||||
migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap);
|
||||
/**
|
||||
* If defined, objects of this type will be converted to multi-namespace objects when migrating to this version.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* 1. This string value must be a valid semver version
|
||||
* 2. This type must have previously specified {@link SavedObjectsNamespaceType | `namespaceType: 'single'`}
|
||||
* 3. This type must also specify {@link SavedObjectsNamespaceType | `namespaceType: 'multiple'`}
|
||||
*
|
||||
* Example of a single-namespace type in 7.10:
|
||||
*
|
||||
* ```ts
|
||||
* {
|
||||
* name: 'foo',
|
||||
* hidden: false,
|
||||
* namespaceType: 'single',
|
||||
* mappings: {...}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Example after converting to a multi-namespace type in 7.11:
|
||||
*
|
||||
* ```ts
|
||||
* {
|
||||
* name: 'foo',
|
||||
* hidden: false,
|
||||
* namespaceType: 'multiple',
|
||||
* mappings: {...},
|
||||
* convertToMultiNamespaceTypeVersion: '7.11.0'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Note: a migration function can be optionally specified for the same version.
|
||||
*/
|
||||
convertToMultiNamespaceTypeVersion?: string;
|
||||
/**
|
||||
* An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type.
|
||||
*/
|
||||
|
|
|
@ -681,6 +681,20 @@ export interface CoreUsageStats {
|
|||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.total'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.total'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.namespace.default.total'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolve.total'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number;
|
||||
|
@ -2032,6 +2046,7 @@ export type SafeRouteMethod = 'get' | 'options';
|
|||
// @public (undocumented)
|
||||
export interface SavedObject<T = unknown> {
|
||||
attributes: T;
|
||||
coreMigrationVersion?: string;
|
||||
// Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
@ -2116,6 +2131,7 @@ export interface SavedObjectsBaseOptions {
|
|||
export interface SavedObjectsBulkCreateObject<T = unknown> {
|
||||
// (undocumented)
|
||||
attributes: T;
|
||||
coreMigrationVersion?: string;
|
||||
// (undocumented)
|
||||
id?: string;
|
||||
initialNamespaces?: string[];
|
||||
|
@ -2206,6 +2222,7 @@ export class SavedObjectsClient {
|
|||
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
|
||||
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
|
||||
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
||||
|
@ -2276,6 +2293,7 @@ export interface SavedObjectsCoreFieldMapping {
|
|||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
|
||||
coreMigrationVersion?: string;
|
||||
id?: string;
|
||||
initialNamespaces?: string[];
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
|
@ -2722,6 +2740,11 @@ export interface SavedObjectsRawDoc {
|
|||
_source: SavedObjectsRawDocSource;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsRawDocParseOptions {
|
||||
namespaceTreatment?: 'strict' | 'lax';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsRemoveReferencesToOptions extends SavedObjectsBaseOptions {
|
||||
refresh?: boolean;
|
||||
|
@ -2752,6 +2775,7 @@ export class SavedObjectsRepository {
|
|||
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFields: Array<string | SavedObjectsIncrementCounterField>, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
resolve<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObjectsResolveResponse<T>>;
|
||||
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
||||
|
@ -2769,13 +2793,21 @@ export interface SavedObjectsResolveImportErrorsOptions {
|
|||
retries: SavedObjectsImportRetry[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsResolveResponse<T = unknown> {
|
||||
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
|
||||
// (undocumented)
|
||||
saved_object: SavedObject<T>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class SavedObjectsSerializer {
|
||||
// @internal
|
||||
constructor(registry: ISavedObjectTypeRegistry);
|
||||
generateRawId(namespace: string | undefined, type: string, id: string): string;
|
||||
isRawSavedObject(rawDoc: SavedObjectsRawDoc): boolean;
|
||||
rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc;
|
||||
generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string;
|
||||
isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean;
|
||||
rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc;
|
||||
savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc;
|
||||
}
|
||||
|
||||
|
@ -2810,6 +2842,7 @@ export interface SavedObjectStatusMeta {
|
|||
// @public (undocumented)
|
||||
export interface SavedObjectsType {
|
||||
convertToAliasScript?: string;
|
||||
convertToMultiNamespaceTypeVersion?: string;
|
||||
hidden: boolean;
|
||||
indexPattern?: string;
|
||||
management?: SavedObjectsTypeManagementDefinition;
|
||||
|
|
|
@ -82,6 +82,8 @@ export interface SavedObject<T = unknown> {
|
|||
references: SavedObjectReference[];
|
||||
/** {@inheritdoc SavedObjectsMigrationVersion} */
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
/** A semver value that is used when upgrading objects between Kibana versions. */
|
||||
coreMigrationVersion?: string;
|
||||
/** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */
|
||||
namespaces?: string[];
|
||||
/**
|
||||
|
|
|
@ -1142,7 +1142,7 @@ export class Plugin implements Plugin_2<PluginSetup, PluginStart, DataPluginSetu
|
|||
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
|
||||
};
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "resolve" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import("../public").IndexPatternsService>;
|
||||
};
|
||||
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
|
||||
};
|
||||
|
|
|
@ -154,6 +154,13 @@ export function getCoreUsageCollector(
|
|||
'apiCalls.savedObjectsGet.namespace.custom.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.namespace.default.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsUpdate.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsUpdate.namespace.default.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes': { type: 'long' },
|
||||
|
|
|
@ -3864,6 +3864,27 @@
|
|||
"apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.namespace.default.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.namespace.default.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.namespace.custom.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolve.namespace.custom.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsUpdate.total": {
|
||||
"type": "long"
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -21,6 +22,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
attributes: {
|
||||
title: 'An existing visualization',
|
||||
},
|
||||
coreMigrationVersion: '1.2.3',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
|
@ -32,6 +34,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
];
|
||||
|
||||
describe('_bulk_create', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
@ -65,6 +73,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
migrationVersion: {
|
||||
dashboard: resp.body.saved_objects[1].migrationVersion.dashboard,
|
||||
},
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
references: [],
|
||||
namespaces: ['default'],
|
||||
},
|
||||
|
@ -112,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
migrationVersion: {
|
||||
visualization: resp.body.saved_objects[0].migrationVersion.visualization,
|
||||
},
|
||||
coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
|
@ -126,6 +136,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
migrationVersion: {
|
||||
dashboard: resp.body.saved_objects[1].migrationVersion.dashboard,
|
||||
},
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -30,6 +31,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
];
|
||||
|
||||
describe('_bulk_get', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
@ -58,6 +65,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta,
|
||||
},
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
namespaces: ['default'],
|
||||
references: [
|
||||
{
|
||||
|
@ -87,6 +95,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
namespaces: ['default'],
|
||||
migrationVersion: resp.body.saved_objects[2].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -15,6 +16,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('create', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
@ -42,6 +49,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
id: resp.body.id,
|
||||
type: 'visualization',
|
||||
migrationVersion: resp.body.migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
updated_at: resp.body.updated_at,
|
||||
version: resp.body.version,
|
||||
attributes: {
|
||||
|
@ -53,6 +61,21 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(resp.body.migrationVersion).to.be.ok();
|
||||
});
|
||||
});
|
||||
|
||||
it('result should be updated to the latest coreMigrationVersion', async () => {
|
||||
await supertest
|
||||
.post(`/api/saved_objects/visualization`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
},
|
||||
coreMigrationVersion: '1.2.3',
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
|
@ -86,6 +109,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
id: resp.body.id,
|
||||
type: 'visualization',
|
||||
migrationVersion: resp.body.migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
updated_at: resp.body.updated_at,
|
||||
version: resp.body.version,
|
||||
attributes: {
|
||||
|
@ -99,6 +123,21 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true);
|
||||
});
|
||||
|
||||
it('result should have the latest coreMigrationVersion', async () => {
|
||||
await supertest
|
||||
.post(`/api/saved_objects/visualization`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
},
|
||||
coreMigrationVersion: '1.2.3',
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
function ndjsonToObject(input: string) {
|
||||
return input.split('\n').map((str) => JSON.parse(str));
|
||||
|
@ -18,6 +19,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('export', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
describe('basic amount of saved objects', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
|
@ -312,6 +319,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
|
@ -371,6 +379,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
|
@ -435,6 +444,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { SavedObject } from '../../../../src/core/server';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -16,6 +17,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('find', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
@ -39,6 +46,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
score: 0,
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
namespaces: ['default'],
|
||||
references: [
|
||||
{
|
||||
|
@ -134,6 +142,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
title: 'Count of requests',
|
||||
},
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
namespaces: ['default'],
|
||||
score: 0,
|
||||
references: [
|
||||
|
@ -170,6 +179,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
title: 'Count of requests',
|
||||
},
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
namespaces: ['default'],
|
||||
score: 0,
|
||||
references: [
|
||||
|
@ -187,6 +197,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
namespaces: ['foo-ns'],
|
||||
references: [
|
||||
{
|
||||
|
@ -202,7 +213,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(resp.body.saved_objects[0].migrationVersion).to.be.ok();
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -244,6 +254,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
],
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
version: 'WzIsMV0=',
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -15,6 +16,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('get', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
@ -30,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
version: resp.body.version,
|
||||
migrationVersion: resp.body.migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
attributes: {
|
||||
title: 'Count of requests',
|
||||
description: '',
|
||||
|
|
|
@ -12,15 +12,16 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('saved_objects', () => {
|
||||
loadTestFile(require.resolve('./bulk_create'));
|
||||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
loadTestFile(require.resolve('./resolve'));
|
||||
loadTestFile(require.resolve('./resolve_import_errors'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export async function getKibanaVersion(getService: FtrProviderContext['getService']) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const kibanaVersion = await kibanaServer.version.get();
|
||||
expect(typeof kibanaVersion).to.eql('string');
|
||||
expect(kibanaVersion.length).to.be.greaterThan(0);
|
||||
return kibanaVersion;
|
||||
}
|
|
@ -10,10 +10,11 @@
|
|||
* Smokescreen tests for core migration logic
|
||||
*/
|
||||
|
||||
import uuidv5 from 'uuid/v5';
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import _ from 'lodash';
|
||||
import expect from '@kbn/expect';
|
||||
import { ElasticsearchClient, SavedObjectMigrationMap, SavedObjectsType } from 'src/core/server';
|
||||
import { ElasticsearchClient, SavedObjectsType } from 'src/core/server';
|
||||
import { SearchResponse } from '../../../../src/core/server/elasticsearch/client';
|
||||
import {
|
||||
DocumentMigrator,
|
||||
|
@ -28,6 +29,26 @@ import {
|
|||
} from '../../../../src/core/server/saved_objects';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const KIBANA_VERSION = '99.9.9';
|
||||
const FOO_TYPE: SavedObjectsType = {
|
||||
name: 'foo',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: { properties: {} },
|
||||
};
|
||||
const BAR_TYPE: SavedObjectsType = {
|
||||
name: 'bar',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: { properties: {} },
|
||||
};
|
||||
const BAZ_TYPE: SavedObjectsType = {
|
||||
name: 'baz',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: { properties: {} },
|
||||
};
|
||||
|
||||
function getLogMock() {
|
||||
return {
|
||||
debug() {},
|
||||
|
@ -61,16 +82,22 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
bar: { properties: { mynum: { type: 'integer' } } },
|
||||
};
|
||||
|
||||
const migrations: Record<string, SavedObjectMigrationMap> = {
|
||||
foo: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()),
|
||||
const savedObjectTypes: SavedObjectsType[] = [
|
||||
{
|
||||
...FOO_TYPE,
|
||||
migrations: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()),
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1),
|
||||
'1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }),
|
||||
'1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2),
|
||||
{
|
||||
...BAR_TYPE,
|
||||
migrations: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1),
|
||||
'1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }),
|
||||
'1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2),
|
||||
},
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
await createIndex({ esClient, index });
|
||||
await createDocs({ esClient, index, docs: originalDocs });
|
||||
|
@ -107,7 +134,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const result = await migrateIndex({
|
||||
esClient,
|
||||
index,
|
||||
migrations,
|
||||
savedObjectTypes,
|
||||
mappingProperties,
|
||||
obsoleteIndexTemplatePattern: 'migration_a*',
|
||||
});
|
||||
|
@ -129,13 +156,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
// The docs in the original index are unchanged
|
||||
expect(await fetchDocs(esClient, `${index}_1`)).to.eql([
|
||||
{ id: 'bar:i', type: 'bar', bar: { nomnom: 33 } },
|
||||
{ id: 'bar:o', type: 'bar', bar: { nomnom: 2 } },
|
||||
{ id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' } },
|
||||
{ id: 'foo:a', type: 'foo', foo: { name: 'Foo A' } },
|
||||
{ id: 'foo:e', type: 'foo', foo: { name: 'Fooey' } },
|
||||
]);
|
||||
expect(await fetchDocs(esClient, `${index}_1`)).to.eql(originalDocs.sort(sortByTypeAndId));
|
||||
|
||||
// The docs in the alias have been migrated
|
||||
expect(await fetchDocs(esClient, index)).to.eql([
|
||||
|
@ -145,6 +166,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { bar: '1.9.0' },
|
||||
bar: { mynum: 68 },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'bar:o',
|
||||
|
@ -152,14 +174,22 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { bar: '1.9.0' },
|
||||
bar: { mynum: 6 },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'baz:u',
|
||||
type: 'baz',
|
||||
baz: { title: 'Terrific!' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{ id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' }, references: [] },
|
||||
{
|
||||
id: 'foo:a',
|
||||
type: 'foo',
|
||||
migrationVersion: { foo: '1.0.0' },
|
||||
foo: { name: 'FOO A' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'foo:e',
|
||||
|
@ -167,6 +197,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { foo: '1.0.0' },
|
||||
foo: { name: 'FOOEY' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -185,28 +216,46 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
bar: { properties: { mynum: { type: 'integer' } } },
|
||||
};
|
||||
|
||||
const migrations: Record<string, SavedObjectMigrationMap> = {
|
||||
foo: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()),
|
||||
let savedObjectTypes: SavedObjectsType[] = [
|
||||
{
|
||||
...FOO_TYPE,
|
||||
migrations: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()),
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1),
|
||||
'1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }),
|
||||
'1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2),
|
||||
{
|
||||
...BAR_TYPE,
|
||||
migrations: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1),
|
||||
'1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }),
|
||||
'1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2),
|
||||
},
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
await createIndex({ esClient, index });
|
||||
await createDocs({ esClient, index, docs: originalDocs });
|
||||
|
||||
await migrateIndex({ esClient, index, migrations, mappingProperties });
|
||||
await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties });
|
||||
|
||||
// @ts-expect-error name doesn't exist on mynum type
|
||||
mappingProperties.bar.properties.name = { type: 'keyword' };
|
||||
migrations.foo['2.0.1'] = (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`);
|
||||
migrations.bar['2.3.4'] = (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`);
|
||||
savedObjectTypes = [
|
||||
{
|
||||
...FOO_TYPE,
|
||||
migrations: {
|
||||
'2.0.1': (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`),
|
||||
},
|
||||
},
|
||||
{
|
||||
...BAR_TYPE,
|
||||
migrations: {
|
||||
'2.3.4': (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await migrateIndex({ esClient, index, migrations, mappingProperties });
|
||||
await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties });
|
||||
|
||||
// The index for the initial migration has not been destroyed...
|
||||
expect(await fetchDocs(esClient, `${index}_2`)).to.eql([
|
||||
|
@ -216,6 +265,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { bar: '1.9.0' },
|
||||
bar: { mynum: 68 },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'bar:o',
|
||||
|
@ -223,6 +273,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { bar: '1.9.0' },
|
||||
bar: { mynum: 6 },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'foo:a',
|
||||
|
@ -230,6 +281,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { foo: '1.0.0' },
|
||||
foo: { name: 'FOO A' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'foo:e',
|
||||
|
@ -237,6 +289,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { foo: '1.0.0' },
|
||||
foo: { name: 'FOOEY' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -248,6 +301,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { bar: '2.3.4' },
|
||||
bar: { mynum: 68, name: 'NAME i' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'bar:o',
|
||||
|
@ -255,6 +309,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { bar: '2.3.4' },
|
||||
bar: { mynum: 6, name: 'NAME o' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'foo:a',
|
||||
|
@ -262,6 +317,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { foo: '2.0.1' },
|
||||
foo: { name: 'FOO Av2' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'foo:e',
|
||||
|
@ -269,6 +325,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { foo: '2.0.1' },
|
||||
foo: { name: 'FOOEYv2' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -281,18 +338,21 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
foo: { properties: { name: { type: 'text' } } },
|
||||
};
|
||||
|
||||
const migrations: Record<string, SavedObjectMigrationMap> = {
|
||||
foo: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'),
|
||||
const savedObjectTypes: SavedObjectsType[] = [
|
||||
{
|
||||
...FOO_TYPE,
|
||||
migrations: {
|
||||
'1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'),
|
||||
},
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
await createIndex({ esClient, index });
|
||||
await createDocs({ esClient, index, docs: originalDocs });
|
||||
|
||||
const result = await Promise.all([
|
||||
migrateIndex({ esClient, index, migrations, mappingProperties }),
|
||||
migrateIndex({ esClient, index, migrations, mappingProperties }),
|
||||
migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }),
|
||||
migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }),
|
||||
]);
|
||||
|
||||
// The polling instance and the migrating instance should both
|
||||
|
@ -327,9 +387,170 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
migrationVersion: { foo: '1.0.0' },
|
||||
foo: { name: 'LOTR' },
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Correctly applies reference transforms and conversion transforms', async () => {
|
||||
const index = '.migration-d';
|
||||
const originalDocs = [
|
||||
{ id: 'foo:1', type: 'foo', foo: { name: 'Foo 1 default' } },
|
||||
{ id: 'spacex:foo:1', type: 'foo', foo: { name: 'Foo 1 spacex' }, namespace: 'spacex' },
|
||||
{
|
||||
id: 'bar:1',
|
||||
type: 'bar',
|
||||
bar: { nomnom: 1 },
|
||||
references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }],
|
||||
},
|
||||
{
|
||||
id: 'spacex:bar:1',
|
||||
type: 'bar',
|
||||
bar: { nomnom: 2 },
|
||||
references: [{ type: 'foo', id: '1', name: 'Foo 1 spacex' }],
|
||||
namespace: 'spacex',
|
||||
},
|
||||
{
|
||||
id: 'baz:1',
|
||||
type: 'baz',
|
||||
baz: { title: 'Baz 1 default' },
|
||||
references: [{ type: 'bar', id: '1', name: 'Bar 1 default' }],
|
||||
},
|
||||
{
|
||||
id: 'spacex:baz:1',
|
||||
type: 'baz',
|
||||
baz: { title: 'Baz 1 spacex' },
|
||||
references: [{ type: 'bar', id: '1', name: 'Bar 1 spacex' }],
|
||||
namespace: 'spacex',
|
||||
},
|
||||
];
|
||||
|
||||
const mappingProperties = {
|
||||
foo: { properties: { name: { type: 'text' } } },
|
||||
bar: { properties: { nomnom: { type: 'integer' } } },
|
||||
baz: { properties: { title: { type: 'keyword' } } },
|
||||
};
|
||||
|
||||
const savedObjectTypes: SavedObjectsType[] = [
|
||||
{
|
||||
...FOO_TYPE,
|
||||
namespaceType: 'multiple',
|
||||
convertToMultiNamespaceTypeVersion: '1.0.0',
|
||||
},
|
||||
{
|
||||
...BAR_TYPE,
|
||||
namespaceType: 'multiple',
|
||||
convertToMultiNamespaceTypeVersion: '2.0.0',
|
||||
},
|
||||
BAZ_TYPE, // must be registered for reference transforms to be applied to objects of this type
|
||||
];
|
||||
|
||||
await createIndex({ esClient, index });
|
||||
await createDocs({ esClient, index, docs: originalDocs });
|
||||
|
||||
await migrateIndex({
|
||||
esClient,
|
||||
index,
|
||||
savedObjectTypes,
|
||||
mappingProperties,
|
||||
obsoleteIndexTemplatePattern: 'migration_a*',
|
||||
});
|
||||
|
||||
// The docs in the original index are unchanged
|
||||
expect(await fetchDocs(esClient, `${index}_1`)).to.eql(originalDocs.sort(sortByTypeAndId));
|
||||
|
||||
// The docs in the alias have been migrated
|
||||
const migratedDocs = await fetchDocs(esClient, index);
|
||||
|
||||
// each newly converted multi-namespace object in a non-default space has its ID deterministically regenerated, and a legacy-url-alias
|
||||
// object is created which links the old ID to the new ID
|
||||
const newFooId = uuidv5('spacex:foo:1', uuidv5.DNS);
|
||||
const newBarId = uuidv5('spacex:bar:1', uuidv5.DNS);
|
||||
|
||||
expect(migratedDocs).to.eql(
|
||||
[
|
||||
{
|
||||
id: 'foo:1',
|
||||
type: 'foo',
|
||||
foo: { name: 'Foo 1 default' },
|
||||
references: [],
|
||||
namespaces: ['default'],
|
||||
migrationVersion: { foo: '1.0.0' },
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: `foo:${newFooId}`,
|
||||
type: 'foo',
|
||||
foo: { name: 'Foo 1 spacex' },
|
||||
references: [],
|
||||
namespaces: ['spacex'],
|
||||
originId: '1',
|
||||
migrationVersion: { foo: '1.0.0' },
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
// new object
|
||||
id: 'legacy-url-alias:spacex:foo:1',
|
||||
type: 'legacy-url-alias',
|
||||
'legacy-url-alias': {
|
||||
targetId: newFooId,
|
||||
targetNamespace: 'spacex',
|
||||
targetType: 'foo',
|
||||
},
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'bar:1',
|
||||
type: 'bar',
|
||||
bar: { nomnom: 1 },
|
||||
references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }],
|
||||
namespaces: ['default'],
|
||||
migrationVersion: { bar: '2.0.0' },
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: `bar:${newBarId}`,
|
||||
type: 'bar',
|
||||
bar: { nomnom: 2 },
|
||||
references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }],
|
||||
namespaces: ['spacex'],
|
||||
originId: '1',
|
||||
migrationVersion: { bar: '2.0.0' },
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
// new object
|
||||
id: 'legacy-url-alias:spacex:bar:1',
|
||||
type: 'legacy-url-alias',
|
||||
'legacy-url-alias': {
|
||||
targetId: newBarId,
|
||||
targetNamespace: 'spacex',
|
||||
targetType: 'bar',
|
||||
},
|
||||
migrationVersion: {},
|
||||
references: [],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'baz:1',
|
||||
type: 'baz',
|
||||
baz: { title: 'Baz 1 default' },
|
||||
references: [{ type: 'bar', id: '1', name: 'Bar 1 default' }],
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
{
|
||||
id: 'spacex:baz:1',
|
||||
type: 'baz',
|
||||
baz: { title: 'Baz 1 spacex' },
|
||||
references: [{ type: 'bar', id: newBarId, name: 'Bar 1 spacex' }],
|
||||
namespace: 'spacex',
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
},
|
||||
].sort(sortByTypeAndId)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -340,6 +561,30 @@ async function createIndex({ esClient, index }: { esClient: ElasticsearchClient;
|
|||
foo: { properties: { name: { type: 'keyword' } } },
|
||||
bar: { properties: { nomnom: { type: 'integer' } } },
|
||||
baz: { properties: { title: { type: 'keyword' } } },
|
||||
'legacy-url-alias': {
|
||||
properties: {
|
||||
targetNamespace: { type: 'text' },
|
||||
targetType: { type: 'text' },
|
||||
targetId: { type: 'text' },
|
||||
lastResolved: { type: 'date' },
|
||||
resolveCounter: { type: 'integer' },
|
||||
disabled: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
namespace: { type: 'keyword' },
|
||||
namespaces: { type: 'keyword' },
|
||||
originId: { type: 'keyword' },
|
||||
references: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
name: { type: 'keyword' },
|
||||
type: { type: 'keyword' },
|
||||
id: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
coreMigrationVersion: {
|
||||
type: 'keyword',
|
||||
},
|
||||
};
|
||||
await esClient.indices.create({
|
||||
index,
|
||||
|
@ -369,23 +614,23 @@ async function createDocs({
|
|||
async function migrateIndex({
|
||||
esClient,
|
||||
index,
|
||||
migrations,
|
||||
savedObjectTypes,
|
||||
mappingProperties,
|
||||
obsoleteIndexTemplatePattern,
|
||||
}: {
|
||||
esClient: ElasticsearchClient;
|
||||
index: string;
|
||||
migrations: Record<string, SavedObjectMigrationMap>;
|
||||
savedObjectTypes: SavedObjectsType[];
|
||||
mappingProperties: SavedObjectsTypeMappingDefinitions;
|
||||
obsoleteIndexTemplatePattern?: string;
|
||||
}) {
|
||||
const typeRegistry = new SavedObjectTypeRegistry();
|
||||
const types = migrationsToTypes(migrations);
|
||||
types.forEach((type) => typeRegistry.registerType(type));
|
||||
savedObjectTypes.forEach((type) => typeRegistry.registerType(type));
|
||||
|
||||
const documentMigrator = new DocumentMigrator({
|
||||
kibanaVersion: '99.9.9',
|
||||
kibanaVersion: KIBANA_VERSION,
|
||||
typeRegistry,
|
||||
minimumConvertVersion: '0.0.0', // bypass the restriction of a minimum version of 8.0.0 for these integration tests
|
||||
log: getLogMock(),
|
||||
});
|
||||
|
||||
|
@ -395,6 +640,7 @@ async function migrateIndex({
|
|||
client: createMigrationEsClient(esClient, getLogMock()),
|
||||
documentMigrator,
|
||||
index,
|
||||
kibanaVersion: KIBANA_VERSION,
|
||||
obsoleteIndexTemplatePattern,
|
||||
mappingProperties,
|
||||
batchSize: 10,
|
||||
|
@ -407,18 +653,6 @@ async function migrateIndex({
|
|||
return await migrator.migrate();
|
||||
}
|
||||
|
||||
function migrationsToTypes(
|
||||
migrations: Record<string, SavedObjectMigrationMap>
|
||||
): SavedObjectsType[] {
|
||||
return Object.entries(migrations).map(([type, migrationsMap]) => ({
|
||||
name: type,
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: { properties: {} },
|
||||
migrations: { ...migrationsMap },
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchDocs(esClient: ElasticsearchClient, index: string) {
|
||||
const { body } = await esClient.search<SearchResponse<any>>({ index });
|
||||
|
||||
|
@ -427,5 +661,9 @@ async function fetchDocs(esClient: ElasticsearchClient, index: string) {
|
|||
...h._source,
|
||||
id: h._id,
|
||||
}))
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
.sort(sortByTypeAndId);
|
||||
}
|
||||
|
||||
function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) {
|
||||
return a.type.localeCompare(b.type) || a.id.localeCompare(b.id);
|
||||
}
|
||||
|
|
104
test/api_integration/apis/saved_objects/resolve.ts
Normal file
104
test/api_integration/apis/saved_objects/resolve.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('resolve', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await getKibanaVersion(getService);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200', async () =>
|
||||
await supertest
|
||||
.get(`/api/saved_objects/resolve/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
saved_object: {
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
version: resp.body.saved_object.version,
|
||||
migrationVersion: resp.body.saved_object.migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
attributes: {
|
||||
title: 'Count of requests',
|
||||
description: '',
|
||||
version: 1,
|
||||
// cheat for some of the more complex attributes
|
||||
visState: resp.body.saved_object.attributes.visState,
|
||||
uiStateJSON: resp.body.saved_object.attributes.uiStateJSON,
|
||||
kibanaSavedObjectMeta: resp.body.saved_object.attributes.kibanaSavedObjectMeta,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
namespaces: ['default'],
|
||||
},
|
||||
outcome: 'exactMatch',
|
||||
});
|
||||
expect(resp.body.saved_object.migrationVersion).to.be.ok();
|
||||
}));
|
||||
|
||||
describe('doc does not exist', () => {
|
||||
it('should return same generic error as when index does not exist', async () =>
|
||||
await supertest
|
||||
.get(`/api/saved_objects/resolve/visualization/foobar`)
|
||||
.expect(404)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [visualization/foobar] not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(
|
||||
async () =>
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
);
|
||||
|
||||
it('should return basic 404 without mentioning index', async () =>
|
||||
await supertest
|
||||
.get('/api/saved_objects/resolve/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab')
|
||||
.expect(404)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Not Found',
|
||||
message:
|
||||
'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -14,8 +14,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('find', () => {
|
||||
let KIBANA_VERSION: string;
|
||||
|
||||
before(async () => {
|
||||
KIBANA_VERSION = await kibanaServer.version.get();
|
||||
expect(typeof KIBANA_VERSION).to.eql('string');
|
||||
expect(KIBANA_VERSION.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
@ -38,6 +47,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
title: 'Count of requests',
|
||||
},
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
coreMigrationVersion: KIBANA_VERSION,
|
||||
namespaces: ['default'],
|
||||
references: [
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue