boolean
| (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion
| [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) |
| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting
| (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) |
+| [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) | Attributes
| Attributes to use when upserting the document if it doesn't exist. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md
new file mode 100644
index 0000000000000..d5657dd65771f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) > [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md)
+
+## SavedObjectsIncrementCounterOptions.upsertAttributes property
+
+Attributes to use when upserting the document if it doesn't exist.
+
+Signature:
+
+```typescript
+upsertAttributes?: Attributes;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md
index eb18e064c84e2..59d98bf4d607b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md
@@ -9,7 +9,7 @@ Increments all the specified counter fields (by one by default). Creates the doc
Signature:
```typescript
-incrementCounterstring
| The id of the document whose fields should be incremented |
| counterFields | Array<string | SavedObjectsIncrementCounterField>
| An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) |
-| options | SavedObjectsIncrementCounterOptions
| [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) |
+| options | SavedObjectsIncrementCounterOptions<T>
| [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) |
Returns:
@@ -52,5 +52,19 @@ repository
'stats.apiCalls',
])
+// Increment the apiCalls field counter by 4
+repository
+ .incrementCounter('dashboard_counter_type', 'counter_id', [
+ { fieldName: 'stats.apiCalls' incrementBy: 4 },
+ ])
+
+// Initialize the document with arbitrary fields if not present
+repository.incrementCounter<{ appId: string }>(
+ 'dashboard_counter_type',
+ 'counter_id',
+ [ 'stats.apiCalls'],
+ { upsertAttributes: { appId: 'myId' } }
+)
+
```
diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
index 43b6c90452b81..d472f27395ffb 100644
--- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
+++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
@@ -12,9 +12,9 @@ import { get, toPath } from 'lodash';
import { Cluster } from '@kbn/es';
import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix';
import { esTestConfig } from './es_test_config';
+import { Client } from '@elastic/elasticsearch';
import { KIBANA_ROOT } from '../';
-import * as legacyElasticsearch from 'elasticsearch';
const path = require('path');
const del = require('del');
@@ -102,8 +102,8 @@ export function createLegacyEsTestCluster(options = {}) {
* Returns an ES Client to the configured cluster
*/
getClient() {
- return new legacyElasticsearch.Client({
- host: this.getUrl(),
+ return new Client({
+ node: this.getUrl(),
});
}
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 6279d62d2c40e..0bb5ddd29609e 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -191,6 +191,7 @@ export class DocLinksService {
lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`,
lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`,
maps: `${ELASTIC_WEBSITE_URL}maps`,
+ vega: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/vega.html`,
},
observability: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`,
@@ -284,6 +285,7 @@ export class DocLinksService {
registerSourceOnly: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-source-only-repository`,
registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`,
restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`,
+ restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`,
},
ingest: {
pipelines: `${ELASTICSEARCH_DOCS}ingest.html`,
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 37572c83e4c88..ce48e8dc9a317 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -23,6 +23,7 @@ import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock
import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks';
import { esKuery } from '../../es_query';
import { errors as EsErrors } from '@elastic/elasticsearch';
+
const { nodeTypes } = esKuery;
jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() }));
@@ -3654,6 +3655,33 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`uses the 'upsertAttributes' option when specified`, async () => {
+ const upsertAttributes = {
+ foo: 'bar',
+ hello: 'dolly',
+ };
+ await incrementCounterSuccess(type, id, counterFields, { namespace, upsertAttributes });
+ expect(client.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ body: expect.objectContaining({
+ upsert: expect.objectContaining({
+ [type]: {
+ foo: 'bar',
+ hello: 'dolly',
+ ...counterFields.reduce((aggs, field) => {
+ return {
+ ...aggs,
+ [field]: 1,
+ };
+ }, {}),
+ },
+ }),
+ }),
+ }),
+ expect.anything()
+ );
+ });
+
it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => {
await incrementCounterSuccess(type, id, counterFields, { namespace });
expect(client.update).toHaveBeenCalledWith(
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index aa1e62c1652ca..6e2a1d6ec0511 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -76,10 +76,16 @@ import {
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-type Left = { tag: 'Left'; error: Record