Skip to content

Commit de2bb35

Browse files
committed
[Saved objects] Add support for version on create & bulkCreate when overwriting a document (#75172)
Adds support for `version` one the SavedObjectsClient's create api. This sallows us to retain Optimistic concurrency control when using create to overwrite an existing document.
1 parent 8946b68 commit de2bb35

12 files changed

Lines changed: 105 additions & 10 deletions

File tree

docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export interface SavedObjectsBulkCreateObject<T = unknown>
2020
| [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. |
2121
| [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | <code>SavedObjectReference[]</code> | |
2222
| [type](./kibana-plugin-core-server.savedobjectsbulkcreateobject.type.md) | <code>string</code> | |
23+
| [version](./kibana-plugin-core-server.savedobjectsbulkcreateobject.version.md) | <code>string</code> | |
2324

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) &gt; [version](./kibana-plugin-core-server.savedobjectsbulkcreateobject.version.md)
4+
5+
## SavedObjectsBulkCreateObject.version property
6+
7+
<b>Signature:</b>
8+
9+
```typescript
10+
version?: string;
11+
```

docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
2020
| [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | <code>boolean</code> | Overwrite existing documents (defaults to false) |
2121
| [references](./kibana-plugin-core-server.savedobjectscreateoptions.references.md) | <code>SavedObjectReference[]</code> | |
2222
| [refresh](./kibana-plugin-core-server.savedobjectscreateoptions.refresh.md) | <code>MutatingOperationRefreshSetting</code> | The Elasticsearch Refresh setting for this operation |
23+
| [version](./kibana-plugin-core-server.savedobjectscreateoptions.version.md) | <code>string</code> | An opaque version number which changes on each successful write operation. Can be used in conjunction with <code>overwrite</code> for implementing optimistic concurrency control. |
2324
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) &gt; [version](./kibana-plugin-core-server.savedobjectscreateoptions.version.md)
4+
5+
## SavedObjectsCreateOptions.version property
6+
7+
An opaque version number which changes on each successful write operation. Can be used in conjunction with `overwrite` for implementing optimistic concurrency control.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
version?: string;
13+
```

src/core/server/saved_objects/import/import_saved_objects.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
SavedObjectsImportOptions,
2626
} from './types';
2727
import { validateReferences } from './validate_references';
28+
import { SavedObject } from '../types';
2829

2930
/**
3031
* Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more
@@ -67,7 +68,7 @@ export async function importSavedObjectsFromStream({
6768
}
6869

6970
// Create objects in bulk
70-
const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, {
71+
const bulkCreateResult = await savedObjectsClient.bulkCreate(omitVersion(filteredObjects), {
7172
overwrite,
7273
namespace,
7374
});
@@ -82,3 +83,7 @@ export async function importSavedObjectsFromStream({
8283
...(errorAccumulator.length ? { errors: errorAccumulator } : {}),
8384
};
8485
}
86+
87+
export function omitVersion(objects: SavedObject[]): SavedObject[] {
88+
return objects.map(({ version, ...object }) => object);
89+
}

src/core/server/saved_objects/import/resolve_import_errors.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
SavedObjectsResolveImportErrorsOptions,
2727
} from './types';
2828
import { validateReferences } from './validate_references';
29+
import { omitVersion } from './import_saved_objects';
2930

3031
/**
3132
* Resolve and return saved object import errors.
@@ -91,7 +92,7 @@ export async function resolveSavedObjectsImportErrors({
9192
// Bulk create in two batches, overwrites and non-overwrites
9293
const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries);
9394
if (objectsToOverwrite.length) {
94-
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToOverwrite, {
95+
const bulkCreateResult = await savedObjectsClient.bulkCreate(omitVersion(objectsToOverwrite), {
9596
overwrite: true,
9697
namespace,
9798
});
@@ -102,9 +103,12 @@ export async function resolveSavedObjectsImportErrors({
102103
successCount += bulkCreateResult.saved_objects.filter((obj) => !obj.error).length;
103104
}
104105
if (objectsToNotOverwrite.length) {
105-
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite, {
106-
namespace,
107-
});
106+
const bulkCreateResult = await savedObjectsClient.bulkCreate(
107+
omitVersion(objectsToNotOverwrite),
108+
{
109+
namespace,
110+
}
111+
);
108112
errorAccumulator = [
109113
...errorAccumulator,
110114
...extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite),

src/core/server/saved_objects/service/lib/repository.test.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,16 @@ describe('SavedObjectsRepository', () => {
464464
{ method, _index = expect.any(String), getId = () => expect.any(String) }
465465
) => {
466466
const body = [];
467-
for (const { type, id } of objects) {
468-
body.push({ [method]: { _index, _id: getId(type, id) } });
467+
for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) {
468+
body.push({
469+
[method]: {
470+
_index,
471+
_id: getId(type, id),
472+
...(ifPrimaryTerm && ifSeqNo
473+
? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) }
474+
: {}),
475+
},
476+
});
469477
body.push(expect.any(Object));
470478
}
471479
expect(client.bulk).toHaveBeenCalledWith(
@@ -525,6 +533,27 @@ describe('SavedObjectsRepository', () => {
525533
expectClientCallArgsAction([obj1, obj2], { method: 'index' });
526534
});
527535

536+
it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => {
537+
await bulkCreateSuccess(
538+
[
539+
{
540+
...obj1,
541+
version: mockVersion,
542+
},
543+
obj2,
544+
],
545+
{ overwrite: true }
546+
);
547+
548+
const obj1WithSeq = {
549+
...obj1,
550+
if_seq_no: mockVersionProps._seq_no,
551+
if_primary_term: mockVersionProps._primary_term,
552+
};
553+
554+
expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' });
555+
});
556+
528557
it(`should use the ES create method if ID is defined and overwrite=false`, async () => {
529558
await bulkCreateSuccess([obj1, obj2]);
530559
expectClientCallArgsAction([obj1, obj2], { method: 'create' });
@@ -1516,6 +1545,16 @@ describe('SavedObjectsRepository', () => {
15161545
expect(client.index).toHaveBeenCalled();
15171546
});
15181547

1548+
it(`should use the ES index with version if ID and version are defined and overwrite=true`, async () => {
1549+
await createSuccess(type, attributes, { id, overwrite: true, version: mockVersion });
1550+
expect(client.index).toHaveBeenCalled();
1551+
1552+
expect(client.index.mock.calls[0][0]).toMatchObject({
1553+
if_seq_no: mockVersionProps._seq_no,
1554+
if_primary_term: mockVersionProps._primary_term,
1555+
});
1556+
});
1557+
15191558
it(`should use the ES create action if ID is defined and overwrite=false`, async () => {
15201559
await createSuccess(type, attributes, { id });
15211560
expect(client.create).toHaveBeenCalled();

src/core/server/saved_objects/service/lib/repository.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export class SavedObjectsRepository {
220220
overwrite = false,
221221
references = [],
222222
refresh = DEFAULT_REFRESH_SETTING,
223+
version,
223224
} = options;
224225

225226
if (!this._allowedTypes.includes(type)) {
@@ -259,6 +260,7 @@ export class SavedObjectsRepository {
259260
index: this.getIndexForType(type),
260261
refresh,
261262
body: raw._source,
263+
...(overwrite && version ? decodeRequestVersion(version) : {}),
262264
};
263265

264266
const { body } =
@@ -345,7 +347,12 @@ export class SavedObjectsRepository {
345347

346348
let savedObjectNamespace;
347349
let savedObjectNamespaces;
348-
const { esRequestIndex, object, method } = expectedBulkGetResult.value;
350+
let versionProperties;
351+
const {
352+
esRequestIndex,
353+
object: { version, ...object },
354+
method,
355+
} = expectedBulkGetResult.value;
349356
if (esRequestIndex !== undefined) {
350357
const indexFound = bulkGetResponse?.statusCode !== 404;
351358
const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined;
@@ -362,12 +369,14 @@ export class SavedObjectsRepository {
362369
};
363370
}
364371
savedObjectNamespaces = getSavedObjectNamespaces(namespace, docFound && actualResult);
372+
versionProperties = getExpectedVersionProperties(version, actualResult);
365373
} else {
366374
if (this._registry.isSingleNamespace(object.type)) {
367375
savedObjectNamespace = namespace;
368376
} else if (this._registry.isMultiNamespace(object.type)) {
369377
savedObjectNamespaces = getSavedObjectNamespaces(namespace);
370378
}
379+
versionProperties = getExpectedVersionProperties(version);
371380
}
372381

373382
const expectedResult = {
@@ -392,6 +401,7 @@ export class SavedObjectsRepository {
392401
[method]: {
393402
_id: expectedResult.rawMigratedDoc._id,
394403
_index: this.getIndexForType(object.type),
404+
...(overwrite && versionProperties),
395405
},
396406
},
397407
expectedResult.rawMigratedDoc._source

src/core/server/saved_objects/service/saved_objects_client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
3737
id?: string;
3838
/** Overwrite existing documents (defaults to false) */
3939
overwrite?: boolean;
40+
/**
41+
* An opaque version number which changes on each successful write operation.
42+
* Can be used in conjunction with `overwrite` for implementing optimistic concurrency control.
43+
**/
44+
version?: string;
4045
/** {@inheritDoc SavedObjectsMigrationVersion} */
4146
migrationVersion?: SavedObjectsMigrationVersion;
4247
references?: SavedObjectReference[];
@@ -52,6 +57,7 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
5257
id?: string;
5358
type: string;
5459
attributes: T;
60+
version?: string;
5561
references?: SavedObjectReference[];
5662
/** {@inheritDoc SavedObjectsMigrationVersion} */
5763
migrationVersion?: SavedObjectsMigrationVersion;

src/core/server/server.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,6 +2044,8 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
20442044
references?: SavedObjectReference[];
20452045
// (undocumented)
20462046
type: string;
2047+
// (undocumented)
2048+
version?: string;
20472049
}
20482050

20492051
// @public (undocumented)
@@ -2178,6 +2180,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
21782180
// (undocumented)
21792181
references?: SavedObjectReference[];
21802182
refresh?: MutatingOperationRefreshSetting;
2183+
version?: string;
21812184
}
21822185

21832186
// @public (undocumented)

0 commit comments

Comments
 (0)