Remove dependency on doc versions (#29906)

See https://github.com/elastic/elasticsearch/pull/38254

Using the `version` parameter to implement optimistic concurrency is not going to be supported in 7.0, so we need to replace our usage of document version with the new `_seq_no` and `_primary_term` parameters. These fields are returned in the same way that `_version` was returned on all read/write requests except for search, where it needs to be requested by sending `seq_no_primary_term: true` in the body of the search request. These parameters are sent back to Elasticsearch on write requests with the `if_seq_no` and `if_primary_term` parameters, and are functionally equivalent to sending a `version` in a write request before elastic/elasticsearch#38254.

To make these updates I searched the code base for uses of a `version` and `_version`, then triaged each usage, so I'm fairly confident that I got everything but it's possible something slipped through the cracks, so if you know of any usage of the document version field please help me out by double checking that I converted it.

- [x] **Saved Objects**:  @elastic/kibana-platform, @elastic/es-security - for BWC and ergonomics the `version` provided by the Saved Objects client/API was not removed, it was converted from a number to a string whose value is `base64(json([_seq_no, _primary_term]))`. This allows the Saved Objects API and its consumers to remain mostly unmodified, as long as the underlying value in the version field is irrelevant. This was the case for all usages in Kibana, only thing that needed updating was tests and TS types.

- [x] **Reporting/esqueue**: @joelgriffith, @tsullivan - the version parameter was used here specifically for implementing optimistic concurrency, and since its usage was contained within the esqueue module I just updated it to use the new `_seq_no` and `_primary_term` fields.

- [x] **Task Manager**: @tsullivan @njd5475 - Like esqueue this module uses version for optimistic concurrency but the usage is contained with the module so I just updated it to use, store, and request the `_seq_no` and `_primary_term` fields.

- [ ] **ML**: @elastic/ml-ui - Best I could tell the only "version" in the ML code refers to the stack version, 077245fed8

- [ ] **Beats CM**: @elastic/beats - Looks like the references to `_version` in the code is only in the types but not in the code itself. I updated the types to use `_seq_no` and `_primary_term`, and their camelCase equivalents where appropriate. I did find a method that used one of the types referencing version but when investigating its usage it seemed the only consumer of that method was itself so i removed it. 52d890fed7

- [x] **Spaces (tests)**: @elastic/kibana-security - The spaces test helpers use saved objects with versions in a number of places, so I updated them to use the new string versions where the version was predictable, and removed the assertion on version where it wasn't. We test the version in the saved objects code so this should be fine.
This commit is contained in:
Spencer 2019-02-04 21:13:34 -08:00 committed by GitHub
parent de2d0b647c
commit b4725b7d34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 773 additions and 246 deletions

View file

@ -61,7 +61,6 @@ describe('plugins/elasticsearch', () => {
cluster = { callWithInternalUser: sinon.stub() };
cluster.callWithInternalUser.withArgs('index', sinon.match.any).returns(Promise.resolve());
cluster.callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve({ _id: '1', _version: 1 }));
cluster.callWithInternalUser.withArgs('mget', sinon.match.any).returns(Promise.resolve({ ok: true }));
cluster.callWithInternalUser.withArgs('get', sinon.match.any).returns(Promise.resolve({ found: false }));
cluster.callWithInternalUser.withArgs('search', sinon.match.any).returns(Promise.resolve({ hits: { hits: [] } }));

View file

@ -35,7 +35,7 @@ export const createBulkCreateRoute = prereqs => ({
type: Joi.string().required(),
id: Joi.string(),
attributes: Joi.object().required(),
version: Joi.number(),
version: Joi.string(),
migrationVersion: Joi.object().optional(),
references: Joi.array().items(
Joi.object()

View file

@ -59,7 +59,7 @@ describe('POST /api/saved_objects/_bulk_get', () => {
id: 'abc123',
type: 'index-pattern',
title: 'logstash-*',
version: 2,
version: 'foo',
references: [],
}]
};

View file

@ -32,7 +32,7 @@ export const createUpdateRoute = (prereqs) => {
}).required(),
payload: Joi.object({
attributes: Joi.object().required(),
version: Joi.number().min(1),
version: Joi.string(),
references: Joi.array().items(
Joi.object()
.keys({

View file

@ -67,7 +67,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
it('calls upon savedObjectClient.update', async () => {
const attributes = { title: 'Testing' };
const options = { version: 2, references: [] };
const options = { version: 'foo', references: [] };
const request = {
method: 'PUT',
url: '/api/saved_objects/index-pattern/logstash-*',

View file

@ -24,6 +24,7 @@
import uuid from 'uuid';
import { SavedObjectsSchema } from '../schema';
import { decodeVersion, encodeVersion } from '../version';
/**
* A raw document as represented directly in the saved object index.
@ -32,7 +33,8 @@ export interface RawDoc {
_id: string;
_source: any;
_type?: string;
_version?: number;
_seq_no?: number;
_primary_term?: number;
}
/**
@ -64,7 +66,7 @@ interface SavedObjectDoc {
type: string;
namespace?: string;
migrationVersion?: MigrationVersion;
version?: number;
version?: string;
updated_at?: Date;
[rootProp: string]: any;
@ -116,8 +118,15 @@ export class SavedObjectsSerializer {
*
* @param {RawDoc} rawDoc - The raw ES document to be converted to saved object format.
*/
public rawToSavedObject({ _id, _source, _version }: RawDoc): SanitizedSavedObjectDoc {
public rawToSavedObject(doc: RawDoc): SanitizedSavedObjectDoc {
const { _id, _source, _seq_no, _primary_term } = doc;
const { type, namespace } = _source;
const version =
_seq_no != null || _primary_term != null
? encodeVersion(_seq_no!, _primary_term!)
: undefined;
return {
type,
id: this.trimIdPrefix(namespace, type, _id),
@ -126,7 +135,7 @@ export class SavedObjectsSerializer {
references: _source.references || [],
...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }),
...(_source.updated_at && { updated_at: _source.updated_at }),
...(_version != null && { version: _version }),
...(version && { version }),
};
}
@ -158,7 +167,7 @@ export class SavedObjectsSerializer {
return {
_id: this.generateRawId(namespace, type, id),
_source: source,
...(version != null && { _version: version }),
...(version != null && decodeVersion(version)),
};
}

View file

@ -20,6 +20,7 @@
import _ from 'lodash';
import { SavedObjectsSerializer } from '.';
import { SavedObjectsSchema } from '../schema';
import { encodeVersion } from '../version';
describe('saved object conversion', () => {
describe('#rawToSavedObject', () => {
@ -86,7 +87,8 @@ describe('saved object conversion', () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const actual = serializer.rawToSavedObject({
_id: 'hello:world',
_version: 3,
_seq_no: 3,
_primary_term: 1,
_source: {
type: 'hello',
hello: {
@ -103,7 +105,7 @@ describe('saved object conversion', () => {
const expected = {
id: 'world',
type: 'hello',
version: 3,
version: encodeVersion(3, 1),
attributes: {
a: 'b',
c: 'd',
@ -130,17 +132,46 @@ describe('saved object conversion', () => {
expect(actual).not.toHaveProperty('version');
});
test(`if specified it copies _version to version`, () => {
test(`if specified it encodes _seq_no and _primary_term to version`, () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const actual = serializer.rawToSavedObject({
_id: 'foo:bar',
_version: 4,
_seq_no: 4,
_primary_term: 1,
_source: {
type: 'foo',
hello: {},
},
});
expect(actual).toHaveProperty('version', 4);
expect(actual).toHaveProperty('version', encodeVersion(4, 1));
});
test(`if only _seq_no is specified it throws`, () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
expect(() =>
serializer.rawToSavedObject({
_id: 'foo:bar',
_seq_no: 4,
_source: {
type: 'foo',
hello: {},
},
})
).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`);
});
test(`if only _primary_term is throws`, () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
expect(() =>
serializer.rawToSavedObject({
_id: 'foo:bar',
_primary_term: 1,
_source: {
type: 'foo',
hello: {},
},
})
).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`);
});
test('if specified it copies the _source.updated_at property to updated_at', () => {
@ -222,7 +253,8 @@ describe('saved object conversion', () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const raw = {
_id: 'foo-namespace:foo:bar',
_version: 24,
_primary_term: 24,
_seq_no: 42,
_source: {
type: 'foo',
foo: {
@ -473,25 +505,38 @@ describe('saved object conversion', () => {
expect(actual._source).not.toHaveProperty('migrationVersion');
});
test('it copies the version property to _version', () => {
test('it decodes the version property to _seq_no and _primary_term', () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const actual = serializer.savedObjectToRaw({
type: '',
attributes: {},
version: 4,
version: encodeVersion(1, 2),
} as any);
expect(actual).toHaveProperty('_version', 4);
expect(actual).toHaveProperty('_seq_no', 1);
expect(actual).toHaveProperty('_primary_term', 2);
});
test(`if unspecified it doesn't add _version property`, () => {
test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const actual = serializer.savedObjectToRaw({
type: '',
attributes: {},
} as any);
expect(actual).not.toHaveProperty('_version');
expect(actual).not.toHaveProperty('_seq_no');
expect(actual).not.toHaveProperty('_primary_term');
});
test(`if version invalid it throws`, () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
expect(() =>
serializer.savedObjectToRaw({
type: '',
attributes: {},
version: 'foo',
} as any)
).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`);
});
test('it copies attributes to _source[type]', () => {

View file

@ -25,3 +25,6 @@ export function isNotFoundError(maybeError: any): boolean;
export function isConflictError(maybeError: any): boolean;
export function isEsUnavailableError(maybeError: any): boolean;
export function isEsAutoCreateIndexError(maybeError: any): boolean;
export function createInvalidVersionError(version: any): Error;
export function isInvalidVersionError(maybeError: Error): boolean;

View file

@ -50,6 +50,15 @@ export function isBadRequestError(error) {
return error && error[code] === CODE_BAD_REQUEST;
}
// 400 - invalid version
const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion';
export function createInvalidVersionError(versionInput) {
return decorate(Boom.badRequest(`Invalid version [${versionInput}]`), CODE_INVALID_VERSION, 400);
}
export function isInvalidVersionError(error) {
return error && error[code] === CODE_INVALID_VERSION;
}
// 401 - Not Authorized
const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized';

View file

@ -23,6 +23,7 @@ import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
import { decorateEsError } from './decorate_es_error';
import * as errors from './errors';
import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version';
// 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.
@ -173,7 +174,8 @@ export class SavedObjectsRepository {
const {
error,
_id: responseId,
_version: version,
_seq_no: seqNo,
_primary_term: primaryTerm,
} = Object.values(response)[0];
const {
@ -208,7 +210,7 @@ export class SavedObjectsRepository {
id,
type,
updated_at: time,
version,
version: encodeVersion(seqNo, primaryTerm),
attributes,
references,
};
@ -262,7 +264,6 @@ export class SavedObjectsRepository {
* @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures }
*/
async deleteByNamespace(namespace) {
if (!namespace || typeof namespace !== 'string') {
throw new TypeError(`namespace is required, and must be a string`);
}
@ -338,7 +339,7 @@ export class SavedObjectsRepository {
ignore: [404],
rest_total_hits_as_int: true,
body: {
version: true,
seq_no_primary_term: true,
...getSearchDsl(this._mappings, this._schema, {
search,
defaultSearchOperator,
@ -423,7 +424,7 @@ export class SavedObjectsRepository {
id,
type,
...time && { updated_at: time },
version: doc._version,
version: encodeHitVersion(doc),
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
@ -466,7 +467,7 @@ export class SavedObjectsRepository {
id,
type,
...updatedAt && { updated_at: updatedAt },
version: response._version,
version: encodeHitVersion(response),
attributes: response._source[type],
references: response._source.references || [],
migrationVersion: response._source.migrationVersion,
@ -479,7 +480,7 @@ export class SavedObjectsRepository {
* @param {string} type
* @param {string} id
* @param {object} [options={}]
* @property {integer} options.version - ensures version matches that of persisted object
* @property {string} options.version - ensures version matches that of persisted object
* @property {string} [options.namespace]
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise}
@ -496,7 +497,7 @@ export class SavedObjectsRepository {
id: this._serializer.generateRawId(namespace, type, id),
type: this._type,
index: this._index,
version,
...(version && decodeRequestVersion(version)),
refresh: 'wait_for',
ignore: [404],
body: {
@ -517,7 +518,7 @@ export class SavedObjectsRepository {
id,
type,
updated_at: time,
version: response._version,
version: encodeHitVersion(response),
references,
attributes
};
@ -593,7 +594,7 @@ export class SavedObjectsRepository {
type,
updated_at: time,
references: response.get._source.references,
version: response._version,
version: encodeHitVersion(response),
attributes: response.get._source[type],
};

View file

@ -26,6 +26,7 @@ import * as errors from './errors';
import elasticsearch from 'elasticsearch';
import { SavedObjectsSchema } from '../../schema';
import { SavedObjectsSerializer } from '../../serialization';
import { encodeHitVersion } from '../../version';
// 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.
@ -39,6 +40,8 @@ describe('SavedObjectsRepository', () => {
let migrator;
const mockTimestamp = '2017-08-14T15:49:14.886Z';
const mockTimestampFields = { updated_at: mockTimestamp };
const mockVersionProps = { _seq_no: 1, _primary_term: 1 };
const mockVersion = encodeHitVersion(mockVersionProps);
const noNamespaceSearchResults = {
hits: {
total: 4,
@ -47,6 +50,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'index-pattern:logstash-*',
_score: 1,
...mockVersionProps,
_source: {
type: 'index-pattern',
...mockTimestampFields,
@ -61,6 +65,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'config:6.0.0-alpha1',
_score: 1,
...mockVersionProps,
_source: {
type: 'config',
...mockTimestampFields,
@ -74,6 +79,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'index-pattern:stocks-*',
_score: 1,
...mockVersionProps,
_source: {
type: 'index-pattern',
...mockTimestampFields,
@ -88,6 +94,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'globaltype:something',
_score: 1,
...mockVersionProps,
_source: {
type: 'globaltype',
...mockTimestampFields,
@ -107,6 +114,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'foo-namespace:index-pattern:logstash-*',
_score: 1,
...mockVersionProps,
_source: {
namespace: 'foo-namespace',
type: 'index-pattern',
@ -122,6 +130,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'foo-namespace:config:6.0.0-alpha1',
_score: 1,
...mockVersionProps,
_source: {
namespace: 'foo-namespace',
type: 'config',
@ -136,6 +145,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'foo-namespace:index-pattern:stocks-*',
_score: 1,
...mockVersionProps,
_source: {
namespace: 'foo-namespace',
type: 'index-pattern',
@ -151,6 +161,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'globaltype:something',
_score: 1,
...mockVersionProps,
_source: {
type: 'globaltype',
...mockTimestampFields,
@ -237,7 +248,7 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.callsFake((method, params) => ({
_type: '_doc',
_id: params.id,
_version: 2
...mockVersionProps,
}));
});
@ -270,7 +281,7 @@ describe('SavedObjectsRepository', () => {
type: 'index-pattern',
id: 'logstash-*',
...mockTimestampFields,
version: 2,
version: mockVersion,
attributes: {
title: 'Logstash',
},
@ -471,14 +482,16 @@ describe('SavedObjectsRepository', () => {
create: {
error: false,
_id: '1',
_version: 1,
_seq_no: 1,
_primary_term: 1,
}
},
{
create: {
error: false,
_id: '2',
_version: 1,
_seq_no: 1,
_primary_term: 1,
}
}
],
@ -522,7 +535,7 @@ describe('SavedObjectsRepository', () => {
{
id: 'one',
type: 'config',
version: 1,
version: mockVersion,
updated_at: mockTimestamp,
attributes: {
title: 'Test One!!',
@ -532,7 +545,7 @@ describe('SavedObjectsRepository', () => {
{
id: 'two',
type: 'index-pattern',
version: 1,
version: mockVersion,
updated_at: mockTimestamp,
attributes: {
title: 'Test Two!!',
@ -589,7 +602,7 @@ describe('SavedObjectsRepository', () => {
create: {
_type: '_doc',
_id: 'index-pattern:two',
_version: 2
...mockVersionProps,
}
}]
}));
@ -608,7 +621,7 @@ describe('SavedObjectsRepository', () => {
}, {
id: 'two',
type: 'index-pattern',
version: 2,
version: mockVersion,
...mockTimestampFields,
attributes: { title: 'Test Two' },
references: [],
@ -624,13 +637,13 @@ describe('SavedObjectsRepository', () => {
create: {
_type: '_doc',
_id: 'config:one',
_version: 2
...mockVersionProps
}
}, {
create: {
_type: '_doc',
_id: 'index-pattern:two',
_version: 2
...mockVersionProps
}
}]
}));
@ -647,14 +660,14 @@ describe('SavedObjectsRepository', () => {
{
id: 'one',
type: 'config',
version: 2,
version: mockVersion,
...mockTimestampFields,
attributes: { title: 'Test One' },
references: [],
}, {
id: 'two',
type: 'index-pattern',
version: 2,
version: mockVersion,
...mockTimestampFields,
attributes: { title: 'Test Two' },
references: [],
@ -952,7 +965,7 @@ describe('SavedObjectsRepository', () => {
id: doc._id.replace(/(index-pattern|config|globaltype)\:/, ''),
type: doc._source.type,
...mockTimestampFields,
version: doc._version,
version: mockVersion,
attributes: doc._source[doc._source.type],
references: [],
});
@ -976,7 +989,7 @@ describe('SavedObjectsRepository', () => {
id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globaltype)\:/, ''),
type: doc._source.type,
...mockTimestampFields,
version: doc._version,
version: mockVersion,
attributes: doc._source[doc._source.type],
references: [],
});
@ -1022,7 +1035,7 @@ describe('SavedObjectsRepository', () => {
const noNamespaceResult = {
_id: 'index-pattern:logstash-*',
_type: '_doc',
_version: 2,
...mockVersionProps,
_source: {
type: 'index-pattern',
specialProperty: 'specialValue',
@ -1035,7 +1048,7 @@ describe('SavedObjectsRepository', () => {
const namespacedResult = {
_id: 'foo-namespace:index-pattern:logstash-*',
_type: '_doc',
_version: 2,
...mockVersionProps,
_source: {
namespace: 'foo-namespace',
type: 'index-pattern',
@ -1064,7 +1077,7 @@ describe('SavedObjectsRepository', () => {
id: 'logstash-*',
type: 'index-pattern',
updated_at: mockTimestamp,
version: 2,
version: mockVersion,
attributes: {
title: 'Testing'
},
@ -1080,7 +1093,7 @@ describe('SavedObjectsRepository', () => {
id: 'logstash-*',
type: 'index-pattern',
updated_at: mockTimestamp,
version: 2,
version: mockVersion,
attributes: {
title: 'Testing'
},
@ -1209,7 +1222,7 @@ describe('SavedObjectsRepository', () => {
_type: '_doc',
_id: 'config:good',
found: true,
_version: 2,
...mockVersionProps,
_source: { ...mockTimestampFields, config: { title: 'Test' } }
}, {
_type: '_doc',
@ -1230,7 +1243,7 @@ describe('SavedObjectsRepository', () => {
id: 'good',
type: 'config',
...mockTimestampFields,
version: 2,
version: mockVersion,
attributes: { title: 'Test' },
references: [],
});
@ -1245,14 +1258,13 @@ describe('SavedObjectsRepository', () => {
describe('#update', () => {
const id = 'logstash-*';
const type = 'index-pattern';
const newVersion = 2;
const attributes = { title: 'Testing' };
beforeEach(() => {
callAdminCluster.returns(Promise.resolve({
_id: `${type}:${id}`,
_type: '_doc',
_version: newVersion,
...mockVersionProps,
result: 'updated'
}));
});
@ -1267,7 +1279,7 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledOnce(migrator.awaitMigration);
});
it('returns current ES document version', async () => {
it('returns current ES document _seq_no and _primary_term encoded as version', async () => {
const response = await savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, {
namespace: 'foo-namespace',
references: [{
@ -1280,7 +1292,7 @@ describe('SavedObjectsRepository', () => {
id,
type,
...mockTimestampFields,
version: newVersion,
version: mockVersion,
attributes,
references: [{
name: 'ref_0',
@ -1295,12 +1307,18 @@ describe('SavedObjectsRepository', () => {
type,
id,
{ title: 'Testing' },
{ version: newVersion - 1 }
{
version: encodeHitVersion({
_seq_no: 100,
_primary_term: 200
})
}
);
sinon.assert.calledOnce(callAdminCluster);
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
version: newVersion - 1
if_seq_no: 100,
if_primary_term: 200,
}));
});
@ -1320,7 +1338,6 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
type: '_doc',
id: 'foo-namespace:index-pattern:logstash-*',
version: undefined,
body: {
doc: {
updated_at: mockTimestamp,
@ -1355,7 +1372,6 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
type: '_doc',
id: 'index-pattern:logstash-*',
version: undefined,
body: {
doc: {
updated_at: mockTimestamp,
@ -1391,7 +1407,6 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
type: '_doc',
id: 'globaltype:foo',
version: undefined,
body: {
doc: {
updated_at: mockTimestamp,
@ -1417,7 +1432,7 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.callsFake((method, params) => ({
_type: '_doc',
_id: params.id,
_version: 2,
...mockVersionProps,
_index: '.kibana',
get: {
found: true,
@ -1437,7 +1452,7 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.callsFake((method, params) => ({
_type: '_doc',
_id: params.id,
_version: 2,
...mockVersionProps,
_index: '.kibana',
get: {
found: true,
@ -1466,7 +1481,7 @@ describe('SavedObjectsRepository', () => {
type: 'config',
id: '6.0.0-alpha1',
...mockTimestampFields,
version: 2,
version: mockVersion,
attributes: {
buildNum: 8468,
defaultIndex: 'logstash-*'
@ -1539,7 +1554,7 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.callsFake((method, params) => ({
_type: '_doc',
_id: params.id,
_version: 2,
...mockVersionProps,
_index: '.kibana',
get: {
found: true,

View file

@ -58,7 +58,7 @@ export interface FindResponse<T extends SavedObjectAttributes = any> {
}
export interface UpdateOptions extends BaseOptions {
version?: number;
version?: string;
}
export interface BulkGetObject {
@ -78,7 +78,7 @@ export interface SavedObjectAttributes {
export interface SavedObject<T extends SavedObjectAttributes = any> {
id: string;
type: string;
version?: number;
version?: string;
updated_at?: string;
error?: {
message: string;

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const decodeBase64 = (base64: string) => Buffer.from(base64, 'base64').toString('utf8');
export const encodeBase64 = (utf8: string) => Buffer.from(utf8, 'utf8').toString('base64');

View file

@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.mock('./decode_version', () => ({
decodeVersion: jest.fn().mockReturnValue({ _seq_no: 1, _primary_term: 2 }),
}));
import { decodeRequestVersion } from './decode_request_version';
import { decodeVersion } from './decode_version';
it('renames decodeVersion() return value to use if_seq_no and if_primary_term', () => {
expect(decodeRequestVersion('foobar')).toMatchInlineSnapshot(`
Object {
"if_primary_term": 2,
"if_seq_no": 1,
}
`);
expect(decodeVersion).toHaveBeenCalledWith('foobar');
});

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { decodeVersion } from './decode_version';
/**
* Helper for decoding version to request params that are driven
* by the version info
*/
export function decodeRequestVersion(version?: string) {
const decoded = decodeVersion(version);
return {
if_seq_no: decoded._seq_no,
if_primary_term: decoded._primary_term,
};
}

View file

@ -0,0 +1,102 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Boom from 'boom';
import { decodeVersion } from './decode_version';
describe('decodeVersion', () => {
it('parses version back into {_seq_no,_primary_term} object', () => {
expect(decodeVersion('WzQsMV0=')).toMatchInlineSnapshot(`
Object {
"_primary_term": 1,
"_seq_no": 4,
}
`);
});
it('throws Boom error if not in base64', () => {
let error;
try {
decodeVersion('[1,4]');
} catch (err) {
error = err;
}
expect(error.message).toMatchInlineSnapshot(`"Invalid version [[1,4]]"`);
expect(Boom.isBoom(error)).toBe(true);
expect(error.output).toMatchInlineSnapshot(`
Object {
"headers": Object {},
"payload": Object {
"error": "Bad Request",
"message": "Invalid version [[1,4]]",
"statusCode": 400,
},
"statusCode": 400,
}
`);
});
it('throws if not JSON encoded', () => {
let error;
try {
decodeVersion('MSwy');
} catch (err) {
error = err;
}
expect(error.message).toMatchInlineSnapshot(`"Invalid version [MSwy]"`);
expect(Boom.isBoom(error)).toBe(true);
expect(error.output).toMatchInlineSnapshot(`
Object {
"headers": Object {},
"payload": Object {
"error": "Bad Request",
"message": "Invalid version [MSwy]",
"statusCode": 400,
},
"statusCode": 400,
}
`);
});
it('throws if either value is not an integer', () => {
let error;
try {
decodeVersion('WzEsMy41XQ==');
} catch (err) {
error = err;
}
expect(error.message).toMatchInlineSnapshot(`"Invalid version [WzEsMy41XQ==]"`);
expect(Boom.isBoom(error)).toBe(true);
expect(error.output).toMatchInlineSnapshot(`
Object {
"headers": Object {},
"payload": Object {
"error": "Bad Request",
"message": "Invalid version [WzEsMy41XQ==]",
"statusCode": 400,
},
"statusCode": 400,
}
`);
});
});

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createInvalidVersionError } from '../service/lib/errors';
import { decodeBase64 } from './base64';
/**
* Decode the "opaque" version string to the sequence params we
* can use to activate optimistic concurrency in Elasticsearch
*/
export function decodeVersion(version?: string) {
try {
if (typeof version !== 'string') {
throw new TypeError();
}
const seqParams = JSON.parse(decodeBase64(version)) as [number, number];
if (
!Array.isArray(seqParams) ||
seqParams.length !== 2 ||
!Number.isInteger(seqParams[0]) ||
!Number.isInteger(seqParams[1])
) {
throw new TypeError();
}
return {
_seq_no: seqParams[0],
_primary_term: seqParams[1],
};
} catch (_) {
throw createInvalidVersionError(version);
}
}

View file

@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.mock('./encode_version', () => ({
encodeVersion: jest.fn().mockReturnValue('foo'),
}));
import { encodeHitVersion } from './encode_hit_version';
import { encodeVersion } from './encode_version';
it('renames decodeVersion() return value to use if_seq_no and if_primary_term', () => {
expect(encodeHitVersion({ _seq_no: 1, _primary_term: 2 })).toMatchInlineSnapshot(`"foo"`);
expect(encodeVersion).toHaveBeenCalledWith(1, 2);
});

View file

@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { encodeVersion } from './encode_version';
/**
* Helper for encoding a version from a "hit" (hits.hits[#] from _search) or
* "doc" (body from GET, update, etc) object
*/
export function encodeHitVersion(response: { _seq_no: number; _primary_term: number }) {
return encodeVersion(response._seq_no, response._primary_term);
}

View file

@ -0,0 +1,62 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { encodeVersion } from './encode_version';
describe('encodeVersion', () => {
it('throws if primaryTerm is not an integer', () => {
expect(() => encodeVersion(1, undefined as any)).toThrowErrorMatchingInlineSnapshot(
`"_primary_term from elasticsearch must be an integer"`
);
expect(() => encodeVersion(1, null as any)).toThrowErrorMatchingInlineSnapshot(
`"_primary_term from elasticsearch must be an integer"`
);
expect(() => encodeVersion(1, {} as any)).toThrowErrorMatchingInlineSnapshot(
`"_primary_term from elasticsearch must be an integer"`
);
expect(() => encodeVersion(1, [] as any)).toThrowErrorMatchingInlineSnapshot(
`"_primary_term from elasticsearch must be an integer"`
);
expect(() => encodeVersion(1, 2.5 as any)).toThrowErrorMatchingInlineSnapshot(
`"_primary_term from elasticsearch must be an integer"`
);
});
it('throws if seqNo is not an integer', () => {
expect(() => encodeVersion(undefined as any, 1)).toThrowErrorMatchingInlineSnapshot(
`"_seq_no from elasticsearch must be an integer"`
);
expect(() => encodeVersion(null as any, 1)).toThrowErrorMatchingInlineSnapshot(
`"_seq_no from elasticsearch must be an integer"`
);
expect(() => encodeVersion({} as any, 1)).toThrowErrorMatchingInlineSnapshot(
`"_seq_no from elasticsearch must be an integer"`
);
expect(() => encodeVersion([] as any, 1)).toThrowErrorMatchingInlineSnapshot(
`"_seq_no from elasticsearch must be an integer"`
);
expect(() => encodeVersion(2.5 as any, 1)).toThrowErrorMatchingInlineSnapshot(
`"_seq_no from elasticsearch must be an integer"`
);
});
it('returns a base64 encoded, JSON string of seqNo and primaryTerm', () => {
expect(encodeVersion(123, 456)).toMatchInlineSnapshot(`"WzEyMyw0NTZd"`);
});
});

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { encodeBase64 } from './base64';
/**
* Encode the sequence params into an "opaque" version string
* that can be used in the saved object API in place of numeric
* version numbers
*/
export function encodeVersion(seqNo: number, primaryTerm: number) {
if (!Number.isInteger(primaryTerm)) {
throw new TypeError('_primary_term from elasticsearch must be an integer');
}
if (!Number.isInteger(seqNo)) {
throw new TypeError('_seq_no from elasticsearch must be an integer');
}
return encodeBase64(JSON.stringify([seqNo, primaryTerm]));
}

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './encode_version';
export * from './encode_hit_version';
export * from './decode_version';
export * from './decode_request_version';

View file

@ -42,13 +42,13 @@ describe('Saved Object', function () {
* that can be used to stub es calls.
* @param indexPatternId
* @param additionalOptions - object that will be assigned to the mocked doc response.
* @returns {{attributes: {}, type: string, id: *, _version: integer}}
* @returns {{attributes: {}, type: string, id: *, _version: string}}
*/
function getMockedDocResponse(indexPatternId, additionalOptions = {}) {
return {
type: 'dashboard',
id: indexPatternId,
_version: 2,
_version: 'foo',
attributes: {},
...additionalOptions
};
@ -242,7 +242,11 @@ describe('Saved Object', function () {
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
const mockDocResponse = getMockedDocResponse('myId');
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({ type: 'dashboard', id: 'myId', _version: 2 });
return BluebirdPromise.resolve({
type: 'dashboard',
id: 'myId',
_version: 'foo'
});
});
stubESResponse(mockDocResponse);
@ -261,7 +265,9 @@ describe('Saved Object', function () {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.isSaving).to.be(true);
return BluebirdPromise.resolve({
type: 'dashboard', id, version: 2
type: 'dashboard',
id,
version: 'foo'
});
});
expect(savedObject.isSaving).to.be(false);
@ -300,7 +306,7 @@ describe('Saved Object', function () {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({
id,
version: 2,
version: 'foo',
type: 'dashboard',
});
});
@ -544,7 +550,7 @@ describe('Saved Object', function () {
attributes: {
title: 'testIndexPattern'
},
_version: 2
_version: 'foo'
});
const savedObject = new SavedObject(config);

View file

@ -220,7 +220,6 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
return savedObjectsClient.get(esType, this.id)
.then(resp => {
// temporary compatability for savedObjectsClient
return {
_id: resp.id,
_type: resp.type,

View file

@ -86,7 +86,7 @@ jest.mock('../unsupported_time_patterns', () => ({
jest.mock('../../saved_objects', () => {
const object = {
_version: 1,
_version: 'foo',
_id: 'foo',
attributes: {
title: 'something'
@ -106,10 +106,11 @@ jest.mock('../../saved_objects', () => {
}
object.attributes.title = body.title;
object._version += 'a';
return {
id: object._id,
_version: ++object._version,
_version: object._version,
};
}
},
@ -137,13 +138,13 @@ describe('IndexPattern', () => {
const pattern = new IndexPattern('foo');
await pattern.init();
expect(pattern.version).toBe(2);
expect(pattern.version).toBe('fooa');
// Create the same one - we're going to handle concurrency
const samePattern = new IndexPattern('foo');
await samePattern.init();
expect(samePattern.version).toBe(3);
expect(samePattern.version).toBe('fooaa');
// This will conflict because samePattern did a save (from refreshFields)
// but the resave should work fine

View file

@ -47,7 +47,6 @@ describe('SavedObject', () => {
const client = sinon.stub();
const savedObject = new SavedObject(client, { version });
expect(savedObject._version).to.be(version);
});
});

View file

@ -29,7 +29,7 @@ describe('SavedObjectsClient', () => {
id: 'AVwSwFxtcMV38qjDZoQg',
type: 'config',
attributes: { title: 'Example title' },
version: 2
version: 'foo'
};
let kfetchStub;
@ -228,8 +228,8 @@ describe('SavedObjectsClient', () => {
test('makes HTTP call', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
const body = { attributes, version: 2 };
const options = { version: 2 };
const body = { attributes, version: 'foo' };
const options = { version: 'foo' };
savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options);
sinon.assert.calledOnce(kfetchStub);

View file

@ -41,7 +41,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () {
create: sinon.stub().callsFake(async (type, attributes, options = {}) => ({
type,
id: options.id,
version: 1,
version: 'foo',
}))
};

View file

@ -66,7 +66,7 @@ export default function ({ getService }) {
type: 'dashboard',
id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e',
updated_at: resp.body.saved_objects[1].updated_at,
version: 1,
version: 'WzgsMV0=',
attributes: {
title: 'A great new dashboard'
},
@ -99,7 +99,7 @@ export default function ({ getService }) {
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
updated_at: resp.body.saved_objects[0].updated_at,
version: 1,
version: 'WzAsMV0=',
attributes: {
title: 'An existing visualization'
},
@ -109,7 +109,7 @@ export default function ({ getService }) {
type: 'dashboard',
id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e',
updated_at: resp.body.saved_objects[1].updated_at,
version: 1,
version: 'WzEsMV0=',
attributes: {
title: 'A great new dashboard'
},

View file

@ -51,7 +51,7 @@ export default function ({ getService }) {
visualization: '7.0.0'
},
updated_at: resp.body.updated_at,
version: 1,
version: 'WzgsMV0=',
attributes: {
title: 'My favorite vis'
},
@ -96,7 +96,7 @@ export default function ({ getService }) {
visualization: '7.0.0'
},
updated_at: resp.body.updated_at,
version: 1,
version: 'WzAsMV0=',
attributes: {
title: 'My favorite vis'
},

View file

@ -42,7 +42,7 @@ export default function ({ getService }) {
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
version: 1,
version: 'WzIsMV0=',
attributes: {
'title': 'Count of requests'
},

View file

@ -48,7 +48,7 @@ export default function ({ getService }) {
id: resp.body.id,
type: 'visualization',
updated_at: resp.body.updated_at,
version: 2,
version: 'WzgsMV0=',
attributes: {
title: 'My second favorite vis'
},

View file

@ -101,7 +101,8 @@ export interface DatabaseSearchResponse<T> {
_id: string;
_score: number;
_source: T;
_version?: number;
_seq_no?: number;
_primary_term?: number;
_explanation?: DatabaseExplanation;
fields?: any;
highlight?: any;
@ -129,7 +130,8 @@ export interface DatabaseGetDocumentResponse<Source> {
_index: string;
_type: string;
_id: string;
_version: number;
_seq_no: number;
_primary_term: number;
found: boolean;
_source: Source;
}
@ -183,8 +185,8 @@ export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams {
refresh?: DatabaseRefresh;
routing?: string;
timeout?: string;
version?: number;
versionType?: DatabaseVersionType;
ifSeqNo?: number;
ifPrimaryTerm?: number;
index: string;
type: string;
id: string;
@ -195,7 +197,8 @@ export interface DatabaseIndexDocumentResponse {
_index: string;
_type: string;
_id: string;
_version: number;
_seq_no: number;
_primary_term: number;
result: string;
}
@ -204,7 +207,8 @@ export interface DatabaseUpdateDocumentResponse {
_index: string;
_type: string;
_id: string;
_version: number;
_seq_no: number;
_primary_term: number;
result: string;
}
@ -213,7 +217,8 @@ export interface DatabaseDeleteDocumentResponse {
_index: string;
_type: string;
_id: string;
_version: number;
_seq_no: number;
_primary_term: number;
result: string;
}
@ -226,8 +231,8 @@ export interface DatabaseIndexDocumentParams<T> extends DatabaseGenericParams {
timeout?: string;
timestamp?: Date | number;
ttl?: string;
version?: number;
versionType?: DatabaseVersionType;
ifSeqNo?: number;
ifPrimaryTerm?: number;
pipeline?: string;
id?: string;
index: string;
@ -247,8 +252,8 @@ export interface DatabaseCreateDocumentParams extends DatabaseGenericParams {
timeout?: string;
timestamp?: Date | number;
ttl?: string;
version?: number;
versionType?: DatabaseVersionType;
ifSeqNo?: number;
ifPrimaryTerm?: number;
pipeline?: string;
id?: string;
index: string;
@ -266,8 +271,8 @@ export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams {
refresh?: DatabaseRefresh;
routing?: string;
timeout?: string;
version?: number;
versionType?: DatabaseVersionType;
ifSeqNo?: number;
ifPrimaryTerm?: number;
index: string;
type: string;
id: string;
@ -283,8 +288,8 @@ export interface DatabaseGetParams extends DatabaseGenericParams {
_source?: DatabaseNameList;
_sourceExclude?: DatabaseNameList;
_source_includes?: DatabaseNameList;
version?: number;
versionType?: DatabaseVersionType;
ifSeqNo?: number;
ifPrimaryTerm?: number;
id: string;
index: string;
type: string;
@ -292,7 +297,6 @@ export interface DatabaseGetParams extends DatabaseGenericParams {
export type DatabaseNameList = string | string[] | boolean;
export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | '';
export type DatabaseVersionType = 'internal' | 'external' | 'external_gte' | 'force';
export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all';
export type DefaultOperator = 'AND' | 'OR';
export type DatabaseConflicts = 'abort' | 'proceed';
@ -311,6 +315,7 @@ export interface DatabaseDeleteDocumentResponse {
_index: string;
_type: string;
_id: string;
_version: number;
_seq_no: number;
_primary_term: number;
result: string;
}

View file

@ -3,7 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { FrameworkUser } from '../framework/adapter_types';
import { internalAuthData } from './../framework/adapter_types';
import {
@ -113,43 +112,6 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter {
return result;
}
private async fetchAllFromScroll<Source>(
user: FrameworkUser,
response: DatabaseSearchResponse<Source>,
hits: DatabaseSearchResponse<Source>['hits']['hits'] = []
): Promise<
Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: Source;
_version?: number;
fields?: any;
highlight?: any;
inner_hits?: any;
sort?: string[];
}>
> {
const newHits = get(response, 'hits.hits', []);
const scrollId = get(response, '_scroll_id');
if (newHits.length > 0) {
hits.push(...newHits);
return this.callWithUser(user, 'scroll', {
body: {
scroll: '30s',
scroll_id: scrollId,
},
}).then((innerResponse: DatabaseSearchResponse<Source>) => {
return this.fetchAllFromScroll(user, innerResponse, hits);
});
}
return Promise.resolve(hits);
}
private callWithUser(user: FrameworkUser, esMethod: string, options: any = {}): any {
if (user.kind === 'authenticated') {
return this.es.callWithRequest(

View file

@ -89,7 +89,7 @@
"name": "version",
"description": "The version number the source configuration was last persisted with",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -430,6 +430,16 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",
"description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Float",
@ -511,16 +521,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",
"description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceFields",

View file

@ -19,7 +19,7 @@ export interface InfraSource {
/** The id of the source */
id: string;
/** The version number the source configuration was last persisted with */
version?: number | null;
version?: string | null;
/** The timestamp the source configuration was last persisted at */
updatedAt?: number | null;
/** The raw configuration of the source */

View file

@ -12,7 +12,7 @@ export const sourcesSchema = gql`
"The id of the source"
id: ID!
"The version number the source configuration was last persisted with"
version: Float
version: String
"The timestamp the source configuration was last persisted at"
updatedAt: Float
"The raw configuration of the source"

View file

@ -47,7 +47,7 @@ export interface InfraSource {
/** The id of the source */
id: string;
/** The version number the source configuration was last persisted with */
version?: number | null;
version?: string | null;
/** The timestamp the source configuration was last persisted at */
updatedAt?: number | null;
/** The raw configuration of the source */
@ -606,7 +606,7 @@ export namespace InfraSourceResolvers {
/** The id of the source */
id?: IdResolver<string, TypeParent, Context>;
/** The version number the source configuration was last persisted with */
version?: VersionResolver<number | null, TypeParent, Context>;
version?: VersionResolver<string | null, TypeParent, Context>;
/** The timestamp the source configuration was last persisted at */
updatedAt?: UpdatedAtResolver<number | null, TypeParent, Context>;
/** The raw configuration of the source */
@ -635,7 +635,7 @@ export namespace InfraSourceResolvers {
Context
>;
export type VersionResolver<
R = number | null,
R = string | null,
Parent = InfraSource,
Context = InfraContext
> = Resolver<R, Parent, Context>;

View file

@ -14,7 +14,7 @@ describe('the InfraSources lib', () => {
configuration: createMockStaticConfiguration({}),
savedObjects: createMockSavedObjectsService({
id: 'TEST_ID',
version: 1,
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
metricAlias: 'METRIC_ALIAS',
@ -34,7 +34,7 @@ describe('the InfraSources lib', () => {
expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({
id: 'TEST_ID',
version: 1,
version: 'foo',
updatedAt: 946684800000,
configuration: {
metricAlias: 'METRIC_ALIAS',
@ -66,7 +66,7 @@ describe('the InfraSources lib', () => {
}),
savedObjects: createMockSavedObjectsService({
id: 'TEST_ID',
version: 1,
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
fields: {
@ -80,7 +80,7 @@ describe('the InfraSources lib', () => {
expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({
id: 'TEST_ID',
version: 1,
version: 'foo',
updatedAt: 946684800000,
configuration: {
metricAlias: 'METRIC_ALIAS',
@ -101,7 +101,7 @@ describe('the InfraSources lib', () => {
configuration: createMockStaticConfiguration({}),
savedObjects: createMockSavedObjectsService({
id: 'TEST_ID',
version: 1,
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {},
}),
@ -111,7 +111,7 @@ describe('the InfraSources lib', () => {
expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({
id: 'TEST_ID',
version: 1,
version: 'foo',
updatedAt: 946684800000,
configuration: {
metricAlias: expect.any(String),

View file

@ -51,7 +51,7 @@ export const InfraSavedSourceConfigurationRuntimeType = runtimeTypes.intersectio
attributes: PartialInfraSourceConfigurationRuntimeType,
}),
runtimeTypes.partial({
version: runtimeTypes.number,
version: runtimeTypes.string,
updated_at: TimestampFromString,
}),
]);

View file

@ -17,7 +17,7 @@ describe('field format map', function () {
const indexPatternSavedObject = {
id: 'logstash-*',
type: 'index-pattern',
version: 4,
version: 'abc',
attributes: {
title: 'logstash-*',
timeFieldName: '@timestamp',

View file

@ -18,7 +18,8 @@ ClientMock.prototype.index = function (params = {}) {
_index: params.index || 'index',
_type: params.type || constants.DEFAULT_SETTING_DOCTYPE,
_id: params.id || uniqueId('testDoc'),
_version: 1,
_seq_no: 1,
_primary_term: 1,
_shards: { total: shardCount, successful: shardCount, failed: 0 },
created: true
});
@ -53,7 +54,8 @@ ClientMock.prototype.get = function (params = {}, source = {}) {
_index: params.index || 'index',
_type: params.type || constants.DEFAULT_SETTING_DOCTYPE,
_id: params.id || 'AVRPRLnlp7Ur1SZXfT-T',
_version: params.version || 1,
_seq_no: params._seq_no || 1,
_primary_term: params._primary_term || 1,
found: true,
_source: _source
});
@ -65,7 +67,8 @@ ClientMock.prototype.search = function (params = {}, count = 5, source = {}) {
_index: params.index || 'index',
_type: params.type || constants.DEFAULT_SETTING_DOCTYPE,
_id: uniqueId('documentId'),
_version: random(1, 5),
_seq_no: random(1, 5),
_primar_term: random(1, 5),
_score: null,
_source: {
created_at: new Date().toString(),
@ -96,7 +99,8 @@ ClientMock.prototype.update = function (params = {}) {
_index: params.index || 'index',
_type: params.type || constants.DEFAULT_SETTING_DOCTYPE,
_id: params.id || uniqueId('testDoc'),
_version: params.version + 1 || 2,
_seq_no: params.if_seq_no + 1 || 2,
_primary_term: params.if_primary_term + 1 || 2,
_shards: { total: shardCount, successful: shardCount, failed: 0 },
created: true
});

View file

@ -135,7 +135,8 @@ describe('Job Class', function () {
expect(jobDoc).to.have.property('id');
expect(jobDoc).to.have.property('index');
expect(jobDoc).to.have.property('type');
expect(jobDoc).to.have.property('version');
expect(jobDoc).to.have.property('_seq_no');
expect(jobDoc).to.have.property('_primary_term');
done();
} catch (e) {
done(e);
@ -383,7 +384,8 @@ describe('Job Class', function () {
expect(doc).to.have.property('index', index);
expect(doc).to.have.property('type', jobDoc.type);
expect(doc).to.have.property('id', jobDoc.id);
expect(doc).to.have.property('version', jobDoc.version);
expect(doc).to.have.property('_seq_no', jobDoc._seq_no);
expect(doc).to.have.property('_primary_term', jobDoc._primary_term);
expect(doc).to.have.property('created_by', defaultCreatedBy);
expect(doc).to.have.property('payload');

View file

@ -336,11 +336,6 @@ describe('Worker class', function () {
searchStub = sinon.stub(mockQueue.client, 'search').callsFake(() => Promise.resolve({ hits: { hits: [] } }));
});
it('should query with version', function () {
const params = getSearchParams();
expect(params).to.have.property('version', true);
});
it('should query by default doctype', function () {
const params = getSearchParams();
expect(params).to.have.property('type', constants.DEFAULT_SETTING_DOCTYPE);
@ -367,6 +362,11 @@ describe('Worker class', function () {
clock.restore();
});
it('should query with seq_no_primary_term', function () {
const { body } = getSearchParams(jobtype);
expect(body).to.have.property('seq_no_primary_term', true);
});
it('should filter unwanted source data', function () {
const excludedFields = [ 'output.content' ];
const { body } = getSearchParams(jobtype);
@ -432,7 +432,6 @@ describe('Worker class', function () {
index: 'myIndex',
type: 'test',
id: 12345,
version: 3
};
return mockQueue.client.get(params)
.then((jobDoc) => {
@ -446,13 +445,14 @@ describe('Worker class', function () {
clock.restore();
});
it('should use version on update', function () {
it('should use seqNo and primaryTerm on update', function () {
worker._claimJob(job);
const query = updateSpy.firstCall.args[0];
expect(query).to.have.property('index', job._index);
expect(query).to.have.property('type', job._type);
expect(query).to.have.property('id', job._id);
expect(query).to.have.property('version', job._version);
expect(query).to.have.property('if_seq_no', job._seq_no);
expect(query).to.have.property('if_primary_term', job._primary_term);
});
it('should increment the job attempts', function () {
@ -500,7 +500,7 @@ describe('Worker class', function () {
expect(msg).to.equal(false);
});
it('should reject the promise on version errors', function () {
it('should reject the promise on conflict errors', function () {
mockQueue.client.update.restore();
sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 409 }));
return worker._claimJob(job)
@ -524,7 +524,8 @@ describe('Worker class', function () {
_index: 'myIndex',
_type: 'test',
_id: 12345,
_version: 3,
_seq_no: 3,
_primary_term: 3,
found: true,
_source: {
jobtype: 'jobtype',
@ -608,13 +609,14 @@ describe('Worker class', function () {
clock.restore();
});
it('should use version on update', function () {
it('should use _seq_no and _primary_term on update', function () {
worker._failJob(job);
const query = updateSpy.firstCall.args[0];
expect(query).to.have.property('index', job._index);
expect(query).to.have.property('type', job._type);
expect(query).to.have.property('id', job._id);
expect(query).to.have.property('version', job._version);
expect(query).to.have.property('if_seq_no', job._seq_no);
expect(query).to.have.property('if_primary_term', job._primary_term);
});
it('should set status to failed', function () {
@ -631,7 +633,7 @@ describe('Worker class', function () {
expect(doc.output).to.have.property('content', msg);
});
it('should return true on version mismatch errors', function () {
it('should return true on conflict errors', function () {
mockQueue.client.update.restore();
sinon.stub(mockQueue.client, 'update').returns(Promise.reject({ statusCode: 409 }));
return worker._failJob(job)
@ -735,7 +737,8 @@ describe('Worker class', function () {
expect(query).to.have.property('index', job._index);
expect(query).to.have.property('type', job._type);
expect(query).to.have.property('id', job._id);
expect(query).to.have.property('version', job._version);
expect(query).to.have.property('if_seq_no', job._seq_no);
expect(query).to.have.property('if_primary_term', job._primary_term);
expect(query.body.doc).to.have.property('output');
expect(query.body.doc.output).to.have.property('content_type', false);
expect(query.body.doc.output).to.have.property('content', payload);

View file

@ -82,7 +82,8 @@ export class Job extends events.EventEmitter {
id: doc._id,
type: doc._type,
index: doc._index,
version: doc._version,
_seq_no: doc._seq_no,
_primary_term: doc._primary_term,
};
this.debug(`Job created in index ${this.index}`);
@ -118,7 +119,8 @@ export class Job extends events.EventEmitter {
index: doc._index,
id: doc._id,
type: doc._type,
version: doc._version,
_seq_no: doc._seq_no,
_primary_term: doc._primary_term,
});
});
}

View file

@ -130,7 +130,8 @@ export class Worker extends events.EventEmitter {
index: job._index,
type: job._type,
id: job._id,
version: job._version,
if_seq_no: job._seq_no,
if_primary_term: job._primary_term,
body: { doc }
})
.then((response) => {
@ -167,7 +168,8 @@ export class Worker extends events.EventEmitter {
index: job._index,
type: job._type,
id: job._id,
version: job._version,
if_seq_no: job._seq_no,
if_primary_term: job._primary_term,
body: { doc }
})
.then(() => true)
@ -244,7 +246,8 @@ export class Worker extends events.EventEmitter {
index: job._index,
type: job._type,
id: job._id,
version: job._version,
if_seq_no: job._seq_no,
if_primary_term: job._primary_term,
body: { doc }
})
.then(() => {
@ -351,6 +354,7 @@ export class Worker extends events.EventEmitter {
_getPendingJobs() {
const nowTime = moment().toISOString();
const query = {
seq_no_primary_term: true,
_source: {
excludes: [ 'output.content' ]
},
@ -385,7 +389,6 @@ export class Worker extends events.EventEmitter {
return this.client.search({
index: `${this.queue.index}-*`,
type: this.doctype,
version: true,
body: query
})
.then((results) => {

View file

@ -217,7 +217,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient {
* @param {string} type
* @param {string} id
* @param {object} [options={}]
* @property {integer} options.version - ensures version matches that of persisted object
* @property {string} options.version - ensures version matches that of persisted object
* @property {string} [options.namespace]
* @returns {promise}
*/

View file

@ -20,7 +20,8 @@ const getMockTaskInstance = () => ({
const getMockConcreteTaskInstance = () => {
const concrete: {
id: string;
version: number;
sequenceNumber: number;
primaryTerm: number;
attempts: number;
status: TaskStatus;
runAt: Date;
@ -29,7 +30,8 @@ const getMockConcreteTaskInstance = () => {
params: any;
} = {
id: 'hy8o99o83',
version: 1,
sequenceNumber: 1,
primaryTerm: 1,
attempts: 0,
status: 'idle',
runAt: new Date(moment('2018-09-18T05:33:09.588Z').valueOf()),
@ -146,11 +148,12 @@ Object {
"params": Object {
"abc": "def",
},
"primaryTerm": 1,
"runAt": 2018-09-18T05:33:09.588Z,
"sequenceNumber": 1,
"state": Object {},
"status": "idle",
"taskType": "nice_task",
"version": 1,
},
}
`);

View file

@ -206,9 +206,14 @@ export interface ConcreteTaskInstance extends TaskInstance {
id: string;
/**
* The version of the Elaticsearch document.
* The sequence number from the Elaticsearch document.
*/
version: number;
sequenceNumber: number;
/**
* The primary term from the Elaticsearch document.
*/
primaryTerm: number;
/**
* The number of unsuccessful attempts since the last successful run. This

View file

@ -230,7 +230,8 @@ describe('TaskManagerRunner', () => {
{
id: 'foo',
taskType: 'bar',
version: 32,
sequenceNumber: 32,
primaryTerm: 32,
runAt: new Date(),
attempts: 0,
params: {},

View file

@ -42,7 +42,8 @@ describe('TaskStore', () => {
const callCluster = sinon.spy(() =>
Promise.resolve({
_id: 'testid',
_version: 3344,
_seq_no: 3344,
_primary_term: 3344,
})
);
const store = new TaskStore({
@ -89,7 +90,8 @@ describe('TaskStore', () => {
expect(result).toMatchObject({
...task,
version: 3344,
sequenceNumber: 3344,
primaryTerm: 3344,
id: 'testid',
});
});
@ -255,7 +257,8 @@ describe('TaskStore', () => {
status: 'idle',
taskType: 'foo',
user: 'jimbo',
version: undefined,
sequenceNumber: undefined,
primaryTerm: undefined,
},
{
attempts: 2,
@ -268,7 +271,8 @@ describe('TaskStore', () => {
status: 'running',
taskType: 'bar',
user: 'dabo',
version: undefined,
sequenceNumber: undefined,
primaryTerm: undefined,
},
],
searchAfter: ['b', 2],
@ -345,7 +349,7 @@ describe('TaskStore', () => {
},
size: 10,
sort: { 'task.runAt': { order: 'asc' } },
version: true,
seq_no_primary_term: true,
},
index,
});
@ -405,7 +409,8 @@ describe('TaskStore', () => {
status: 'idle',
taskType: 'foo',
user: 'jimbo',
version: undefined,
sequenceNumber: undefined,
primaryTerm: undefined,
},
{
attempts: 2,
@ -418,7 +423,8 @@ describe('TaskStore', () => {
status: 'running',
taskType: 'bar',
user: 'dabo',
version: undefined,
sequenceNumber: undefined,
primaryTerm: undefined,
},
]);
});
@ -433,12 +439,17 @@ describe('TaskStore', () => {
params: { hello: 'world' },
state: { foo: 'bar' },
taskType: 'report',
version: 2,
sequenceNumber: 2,
primaryTerm: 2,
attempts: 3,
status: 'idle' as TaskStatus,
};
const callCluster = sinon.spy(async () => ({ _version: task.version + 1 }));
const callCluster = sinon.spy(async () => ({
_seq_no: task.sequenceNumber + 1,
_primary_term: task.primaryTerm + 1,
}));
const store = new TaskStore({
callCluster,
index: 'tasky',
@ -454,12 +465,13 @@ describe('TaskStore', () => {
expect(callCluster.args[0][1]).toMatchObject({
id: task.id,
index: 'tasky',
version: 2,
if_seq_no: 2,
if_primary_term: 2,
refresh: true,
body: {
doc: {
task: {
...['id', 'version'].reduce((acc, prop) => _.omit(acc, prop), task),
..._.omit(task, ['id', 'sequenceNumber', 'primaryTerm']),
params: JSON.stringify(task.params),
state: JSON.stringify(task.state),
},
@ -467,7 +479,11 @@ describe('TaskStore', () => {
},
});
expect(result).toEqual({ ...task, version: 3 });
expect(result).toEqual({
...task,
sequenceNumber: 3,
primaryTerm: 3,
});
});
});
@ -478,7 +494,8 @@ describe('TaskStore', () => {
Promise.resolve({
_index: 'myindex',
_id: id,
_version: 32,
_seq_no: 32,
_primary_term: 32,
result: 'deleted',
})
);
@ -496,7 +513,8 @@ describe('TaskStore', () => {
expect(result).toEqual({
id,
index: 'myindex',
version: 32,
sequenceNumber: 32,
primaryTerm: 32,
result: 'deleted',
});

View file

@ -33,7 +33,8 @@ export interface FetchResult {
export interface RemoveResult {
index: string;
id: string;
version: string;
sequenceNumber: number;
primaryTerm: number;
result: string;
}
@ -41,7 +42,8 @@ export interface RemoveResult {
export interface RawTaskDoc {
_id: string;
_index: string;
_version: number;
_seq_no: number;
_primary_term: number;
_source: {
type: string;
task: {
@ -179,7 +181,8 @@ export class TaskStore {
return {
...taskInstance,
id: result._id,
version: result._version,
sequenceNumber: result._seq_no,
primaryTerm: result._primary_term,
attempts: 0,
status: task.status,
runAt: task.runAt,
@ -225,7 +228,7 @@ export class TaskStore {
},
size: 10,
sort: { 'task.runAt': { order: 'asc' } },
version: true,
seq_no_primary_term: true,
});
return docs;
@ -241,13 +244,14 @@ export class TaskStore {
public async update(doc: ConcreteTaskInstance): Promise<ConcreteTaskInstance> {
const rawDoc = taskDocToRaw(doc, this.index);
const { _version } = await this.callCluster('update', {
const result = await this.callCluster('update', {
body: {
doc: rawDoc._source,
},
id: doc.id,
index: this.index,
version: doc.version,
if_seq_no: doc.sequenceNumber,
if_primary_term: doc.primaryTerm,
// The refresh is important so that if we immediately look for work,
// we don't pick up this task.
refresh: true,
@ -255,7 +259,8 @@ export class TaskStore {
return {
...doc,
version: _version,
sequenceNumber: result._seq_no,
primaryTerm: result._primary_term,
};
}
@ -277,7 +282,8 @@ export class TaskStore {
return {
index: result._index,
id: result._id,
version: result._version,
sequenceNumber: result._seq_no,
primaryTerm: result._primary_term,
result: result.result,
};
}
@ -333,7 +339,8 @@ function rawSource(doc: TaskInstance) {
};
delete (source as any).id;
delete (source as any).version;
delete (source as any).sequenceNumber;
delete (source as any).primaryTerm;
delete (source as any).type;
return {
@ -350,7 +357,8 @@ function taskDocToRaw(doc: ConcreteTaskInstance, index: string): RawTaskDoc {
_id: doc.id,
_index: index,
_source: { type, task },
_version: doc.version,
_seq_no: doc.sequenceNumber,
_primary_term: doc.primaryTerm,
};
}
@ -358,7 +366,8 @@ function rawToTaskDoc(doc: RawTaskDoc): ConcreteTaskInstance {
return {
...doc._source.task,
id: doc._id,
version: doc._version,
sequenceNumber: doc._seq_no,
primaryTerm: doc._primary_term,
params: parseJSONField(doc._source.task.params, 'params', doc),
state: parseJSONField(doc._source.task.state, 'state', doc),
};

View file

@ -87,7 +87,7 @@ describe('ReindexActions', () => {
type: REINDEX_OP_TYPE,
id: '9',
attributes: { indexName: 'hi', locked: moment().format() },
version: 1,
version: 'foo',
} as ReindexSavedObject,
{ newIndexName: 'test' }
);
@ -97,7 +97,7 @@ describe('ReindexActions', () => {
expect(args[1]).toEqual('9');
expect(args[2].indexName).toEqual('hi');
expect(args[2].newIndexName).toEqual('test');
expect(args[3]).toEqual({ version: 1 });
expect(args[3]).toEqual({ version: 'foo' });
});
it('throws if the reindexOp is not locked', async () => {
@ -107,7 +107,7 @@ describe('ReindexActions', () => {
type: REINDEX_OP_TYPE,
id: '10',
attributes: { indexName: 'hi', locked: null },
version: 1,
version: 'foo',
} as ReindexSavedObject,
{ newIndexName: 'test' }
)

View file

@ -73,7 +73,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version, updatedAt, configuration, status } =
response.data && response.data.createSource.source;
expect(version).to.be.greaterThan(0);
expect(version).to.be.a('string');
expect(updatedAt).to.be.greaterThan(0);
expect(configuration.name).to.be('NAME');
expect(configuration.description).to.be('DESCRIPTION');
@ -102,7 +102,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version, updatedAt, configuration, status } =
response.data && response.data.createSource.source;
expect(version).to.be.greaterThan(0);
expect(version).to.be.a('string');
expect(updatedAt).to.be.greaterThan(0);
expect(configuration.name).to.be('NAME');
expect(configuration.description).to.be('');
@ -163,7 +163,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version } = creationResponse.data && creationResponse.data.createSource.source;
expect(version).to.be.greaterThan(0);
expect(version).to.be.a('string');
const deletionResponse = await client.mutate<any>({
mutation: deleteSourceMutation,
@ -193,7 +193,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version: initialVersion, updatedAt: createdAt } =
creationResponse.data && creationResponse.data.createSource.source;
expect(initialVersion).to.be.greaterThan(0);
expect(initialVersion).to.be.a('string');
expect(createdAt).to.be.greaterThan(0);
const updateResponse = await client.mutate<any>({
@ -233,7 +233,8 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version, updatedAt, configuration, status } =
updateResponse.data && updateResponse.data.updateSource.source;
expect(version).to.be.greaterThan(initialVersion);
expect(version).to.be.a('string');
expect(version).to.not.be(initialVersion);
expect(updatedAt).to.be.greaterThan(createdAt);
expect(configuration.name).to.be('UPDATED_NAME');
expect(configuration.description).to.be('UPDATED_DESCRIPTION');
@ -262,7 +263,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version: initialVersion, updatedAt: createdAt } =
creationResponse.data && creationResponse.data.createSource.source;
expect(initialVersion).to.be.greaterThan(0);
expect(initialVersion).to.be.a('string');
expect(createdAt).to.be.greaterThan(0);
const updateResponse = await client.mutate<any>({
@ -282,7 +283,8 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version, updatedAt, configuration, status } =
updateResponse.data && updateResponse.data.updateSource.source;
expect(version).to.be.greaterThan(initialVersion);
expect(version).to.be.a('string');
expect(version).to.not.be(initialVersion);
expect(updatedAt).to.be.greaterThan(createdAt);
expect(configuration.metricAlias).to.be('metricbeat-**');
expect(configuration.logAlias).to.be('filebeat-*');
@ -304,7 +306,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version: initialVersion, updatedAt: createdAt } =
creationResponse.data && creationResponse.data.createSource.source;
expect(initialVersion).to.be.greaterThan(0);
expect(initialVersion).to.be.a('string');
expect(createdAt).to.be.greaterThan(0);
const updateResponse = await client.mutate<any>({
@ -324,7 +326,8 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const { version, updatedAt, configuration } =
updateResponse.data && updateResponse.data.updateSource.source;
expect(version).to.be.greaterThan(initialVersion);
expect(version).to.be.a('string');
expect(version).to.not.be(initialVersion);
expect(updatedAt).to.be.greaterThan(createdAt);
expect(configuration.fields.container).to.be('UPDATED_CONTAINER');
expect(configuration.fields.host).to.be('host.name');

View file

@ -84,7 +84,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest:
type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
updated_at: resp.body.saved_objects[1].updated_at,
version: 1,
version: resp.body.saved_objects[1].version,
attributes: {
title: 'A great new dashboard',
},
@ -94,7 +94,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest:
type: 'globaltype',
id: `05976c65-1145-4858-bbf0-d225cc78a06e`,
updated_at: resp.body.saved_objects[2].updated_at,
version: 1,
version: resp.body.saved_objects[2].version,
attributes: {
name: 'A new globaltype object',
},

View file

@ -63,7 +63,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
},
type: spaceAwareType,
updated_at: resp.body.updated_at,
version: 1,
version: resp.body.version,
attributes: {
title: 'My favorite vis',
},
@ -104,7 +104,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
id: resp.body.id,
type: notSpaceAwareType,
updated_at: resp.body.updated_at,
version: 1,
version: resp.body.version,
attributes: {
name: `Can't be contained to a space`,
},

View file

@ -63,7 +63,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
{
type: 'globaltype',
id: `8121a00-8efd-21e7-1cb3-34ab966434445`,
version: 1,
version: resp.body.saved_objects[0].version,
attributes: {
name: 'My favorite global object',
},
@ -96,7 +96,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
{
type: 'visualization',
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
version: 1,
version: resp.body.saved_objects[0].version,
attributes: {
title: 'Count of requests',
},

View file

@ -74,7 +74,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
id: resp.body.id,
type: 'globaltype',
updated_at: resp.body.updated_at,
version: 2,
version: resp.body.version,
attributes: {
name: 'My second favorite',
},
@ -99,7 +99,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
id: resp.body.id,
type: 'visualization',
updated_at: resp.body.updated_at,
version: 2,
version: resp.body.version,
attributes: {
title: 'My second favorite vis',
},