Introducing a SavedObjectRepository (#19013)
* Create a separate SavedObjectRepository that the SavedObjectClient uses * Moving the repository into lib/ * Fixing test after moving the repository * Revising tests based on peer review * Removing awaits * Adding warning comments regarding the repository's impact on the SOC
This commit is contained in:
parent
c2e91e6d7b
commit
cc707eabe2
|
@ -1,7 +1,4 @@
|
|||
export { getSearchDsl } from './search_dsl';
|
||||
export { trimIdPrefix } from './trim_id_prefix';
|
||||
export { includedFields } from './included_fields';
|
||||
export { decorateEsError } from './decorate_es_error';
|
||||
export { SavedObjectsRepository } from './repository';
|
||||
|
||||
import * as errors from './errors';
|
||||
export { errors };
|
||||
|
|
416
src/server/saved_objects/client/lib/repository.js
Normal file
416
src/server/saved_objects/client/lib/repository.js
Normal file
|
@ -0,0 +1,416 @@
|
|||
import uuid from 'uuid';
|
||||
|
||||
import { getRootType } from '../../../mappings';
|
||||
import { getSearchDsl } from './search_dsl';
|
||||
import { trimIdPrefix } from './trim_id_prefix';
|
||||
import { includedFields } from './included_fields';
|
||||
import { decorateEsError } from './decorate_es_error';
|
||||
import * as errors from './errors';
|
||||
|
||||
|
||||
// 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.
|
||||
|
||||
export class SavedObjectsRepository {
|
||||
constructor(options) {
|
||||
const {
|
||||
index,
|
||||
mappings,
|
||||
callCluster,
|
||||
onBeforeWrite = () => {},
|
||||
} = options;
|
||||
|
||||
this._index = index;
|
||||
this._mappings = mappings;
|
||||
this._type = getRootType(this._mappings);
|
||||
this._onBeforeWrite = onBeforeWrite;
|
||||
this._unwrappedCallCluster = callCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists an object
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {object} attributes
|
||||
* @param {object} [options={}]
|
||||
* @property {string} [options.id] - force id on creation, not recommended
|
||||
* @property {boolean} [options.overwrite=false]
|
||||
* @returns {promise} - { id, type, version, attributes }
|
||||
*/
|
||||
async create(type, attributes = {}, options = {}) {
|
||||
const {
|
||||
id,
|
||||
overwrite = false
|
||||
} = options;
|
||||
|
||||
const method = id && !overwrite ? 'create' : 'index';
|
||||
const time = this._getCurrentTime();
|
||||
|
||||
try {
|
||||
const response = await this._writeToCluster(method, {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
type,
|
||||
updated_at: time,
|
||||
[type]: attributes
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: trimIdPrefix(response._id, type),
|
||||
type,
|
||||
updated_at: time,
|
||||
version: response._version,
|
||||
attributes
|
||||
};
|
||||
} catch (error) {
|
||||
if (errors.isNotFoundError(error)) {
|
||||
// See "503s from missing index" above
|
||||
throw errors.createEsAutoCreateIndexError();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple documents at once
|
||||
*
|
||||
* @param {array} objects - [{ type, id, attributes }]
|
||||
* @param {object} [options={}]
|
||||
* @property {boolean} [options.overwrite=false] - overwrites existing documents
|
||||
* @returns {promise} - [{ id, type, version, attributes, error: { message } }]
|
||||
*/
|
||||
async bulkCreate(objects, options = {}) {
|
||||
const {
|
||||
overwrite = false
|
||||
} = options;
|
||||
const time = this._getCurrentTime();
|
||||
const objectToBulkRequest = (object) => {
|
||||
const method = object.id && !overwrite ? 'create' : 'index';
|
||||
|
||||
return [
|
||||
{
|
||||
[method]: {
|
||||
_id: this._generateEsId(object.type, object.id),
|
||||
_type: this._type,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: object.type,
|
||||
updated_at: time,
|
||||
[object.type]: object.attributes
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const { items } = await this._writeToCluster('bulk', {
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
body: objects.reduce((acc, object) => ([
|
||||
...acc,
|
||||
...objectToBulkRequest(object)
|
||||
]), []),
|
||||
});
|
||||
|
||||
return items.map((response, i) => {
|
||||
const {
|
||||
error,
|
||||
_id: responseId,
|
||||
_version: version,
|
||||
} = Object.values(response)[0];
|
||||
|
||||
const {
|
||||
id = responseId,
|
||||
type,
|
||||
attributes,
|
||||
} = objects[i];
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: {
|
||||
message: error.reason || JSON.stringify(error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updated_at: time,
|
||||
version,
|
||||
attributes
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an object
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} id
|
||||
* @returns {promise}
|
||||
*/
|
||||
async delete(type, id) {
|
||||
const response = await this._writeToCluster('delete', {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
const deleted = response.result === 'deleted';
|
||||
if (deleted) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const docNotFound = response.result === 'not_found';
|
||||
const indexNotFound = response.error && response.error.type === 'index_not_found_exception';
|
||||
if (docNotFound || indexNotFound) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response, })}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [options={}]
|
||||
* @property {string} [options.type]
|
||||
* @property {string} [options.search]
|
||||
* @property {Array<string>} [options.searchFields] - see Elasticsearch Simple Query String
|
||||
* Query field argument for more information
|
||||
* @property {integer} [options.page=1]
|
||||
* @property {integer} [options.perPage=20]
|
||||
* @property {string} [options.sortField]
|
||||
* @property {string} [options.sortOrder]
|
||||
* @property {Array<string>} [options.fields]
|
||||
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
|
||||
*/
|
||||
async find(options = {}) {
|
||||
const {
|
||||
type,
|
||||
search,
|
||||
searchFields,
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
sortField,
|
||||
sortOrder,
|
||||
fields,
|
||||
includeTypes,
|
||||
} = options;
|
||||
|
||||
if (searchFields && !Array.isArray(searchFields)) {
|
||||
throw new TypeError('options.searchFields must be an array');
|
||||
}
|
||||
|
||||
if (fields && !Array.isArray(fields)) {
|
||||
throw new TypeError('options.searchFields must be an array');
|
||||
}
|
||||
|
||||
const esOptions = {
|
||||
index: this._index,
|
||||
size: perPage,
|
||||
from: perPage * (page - 1),
|
||||
_source: includedFields(type, fields),
|
||||
ignore: [404],
|
||||
body: {
|
||||
version: true,
|
||||
...getSearchDsl(this._mappings, {
|
||||
search,
|
||||
searchFields,
|
||||
type,
|
||||
includeTypes,
|
||||
sortField,
|
||||
sortOrder
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const response = await this._callCluster('search', esOptions);
|
||||
|
||||
if (response.status === 404) {
|
||||
// 404 is only possible here if the index is missing, which
|
||||
// we don't want to leak, see "404s from missing index" above
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total: response.hits.total,
|
||||
saved_objects: response.hits.hits.map(hit => {
|
||||
const { type, updated_at: updatedAt } = hit._source;
|
||||
return {
|
||||
id: trimIdPrefix(hit._id, type),
|
||||
type,
|
||||
...updatedAt && { updated_at: updatedAt },
|
||||
version: hit._version,
|
||||
attributes: hit._source[type],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects by id
|
||||
*
|
||||
* @param {array} objects - an array ids, or an array of objects containing id and optionally type
|
||||
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
|
||||
* @example
|
||||
*
|
||||
* bulkGet([
|
||||
* { id: 'one', type: 'config' },
|
||||
* { id: 'foo', type: 'index-pattern' }
|
||||
* ])
|
||||
*/
|
||||
async bulkGet(objects = []) {
|
||||
if (objects.length === 0) {
|
||||
return { saved_objects: [] };
|
||||
}
|
||||
|
||||
const response = await this._callCluster('mget', {
|
||||
index: this._index,
|
||||
body: {
|
||||
docs: objects.map(object => ({
|
||||
_id: this._generateEsId(object.type, object.id),
|
||||
_type: this._type,
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
saved_objects: response.docs.map((doc, i) => {
|
||||
const { id, type } = objects[i];
|
||||
|
||||
if (!doc.found) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: { statusCode: 404, message: 'Not found' }
|
||||
};
|
||||
}
|
||||
|
||||
const time = doc._source.updated_at;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
...time && { updated_at: time },
|
||||
version: doc._version,
|
||||
attributes: doc._source[type]
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single object
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} id
|
||||
* @returns {promise} - { id, type, version, attributes }
|
||||
*/
|
||||
async get(type, id) {
|
||||
const response = await this._callCluster('get', {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
ignore: [404]
|
||||
});
|
||||
|
||||
const docNotFound = response.found === false;
|
||||
const indexNotFound = response.status === 404;
|
||||
if (docNotFound || indexNotFound) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
const { updated_at: updatedAt } = response._source;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
...updatedAt && { updated_at: updatedAt },
|
||||
version: response._version,
|
||||
attributes: response._source[type]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an object
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} id
|
||||
* @param {object} [options={}]
|
||||
* @property {integer} options.version - ensures version matches that of persisted object
|
||||
* @returns {promise}
|
||||
*/
|
||||
async update(type, id, attributes, options = {}) {
|
||||
const time = this._getCurrentTime();
|
||||
const response = await this._writeToCluster('update', {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
version: options.version,
|
||||
refresh: 'wait_for',
|
||||
ignore: [404],
|
||||
body: {
|
||||
doc: {
|
||||
updated_at: time,
|
||||
[type]: attributes
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updated_at: time,
|
||||
version: response._version,
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
async _writeToCluster(method, params) {
|
||||
try {
|
||||
await this._onBeforeWrite();
|
||||
return await this._callCluster(method, params);
|
||||
} catch (err) {
|
||||
throw decorateEsError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async _callCluster(method, params) {
|
||||
try {
|
||||
return await this._unwrappedCallCluster(method, params);
|
||||
} catch (err) {
|
||||
throw decorateEsError(err);
|
||||
}
|
||||
}
|
||||
|
||||
_generateEsId(type, id) {
|
||||
return `${type}:${id || uuid.v1()}`;
|
||||
}
|
||||
|
||||
_getCurrentTime() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
}
|
647
src/server/saved_objects/client/lib/repository.test.js
Normal file
647
src/server/saved_objects/client/lib/repository.test.js
Normal file
|
@ -0,0 +1,647 @@
|
|||
import sinon from 'sinon';
|
||||
import { delay } from 'bluebird';
|
||||
import { SavedObjectsRepository } from './repository';
|
||||
import * as getSearchDslNS from './search_dsl/search_dsl';
|
||||
import { getSearchDsl } from './search_dsl';
|
||||
import * as errors from './errors';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
|
||||
// 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.
|
||||
|
||||
describe('SavedObjectsRepository', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
let callAdminCluster;
|
||||
let onBeforeWrite;
|
||||
let savedObjectsRepository;
|
||||
const mockTimestamp = '2017-08-14T15:49:14.886Z';
|
||||
const mockTimestampFields = { updated_at: mockTimestamp };
|
||||
const searchResults = {
|
||||
hits: {
|
||||
total: 3,
|
||||
hits: [{
|
||||
_index: '.kibana',
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:logstash-*',
|
||||
_score: 1,
|
||||
_source: {
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
'index-pattern': {
|
||||
title: 'logstash-*',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
_index: '.kibana',
|
||||
_type: 'doc',
|
||||
_id: 'config:6.0.0-alpha1',
|
||||
_score: 1,
|
||||
_source: {
|
||||
type: 'config',
|
||||
...mockTimestampFields,
|
||||
config: {
|
||||
buildNum: 8467,
|
||||
defaultIndex: 'logstash-*'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
_index: '.kibana',
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:stocks-*',
|
||||
_score: 1,
|
||||
_source: {
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
'index-pattern': {
|
||||
title: 'stocks-*',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const mappings = {
|
||||
doc: {
|
||||
properties: {
|
||||
'index-pattern': {
|
||||
properties: {
|
||||
someField: {
|
||||
type: 'keyword'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
callAdminCluster = sandbox.stub();
|
||||
onBeforeWrite = sandbox.stub();
|
||||
|
||||
savedObjectsRepository = new SavedObjectsRepository({
|
||||
index: '.kibana-test',
|
||||
mappings,
|
||||
callCluster: callAdminCluster,
|
||||
onBeforeWrite
|
||||
});
|
||||
|
||||
sandbox.stub(savedObjectsRepository, '_getCurrentTime').returns(mockTimestamp);
|
||||
sandbox.stub(getSearchDslNS, 'getSearchDsl').returns({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
|
||||
describe('#create', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:logstash-*',
|
||||
_version: 2
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
const response = await savedObjectsRepository.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
type: 'index-pattern',
|
||||
id: 'logstash-*',
|
||||
...mockTimestampFields,
|
||||
version: 2,
|
||||
attributes: {
|
||||
title: 'Logstash',
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use ES index action', async () => {
|
||||
await savedObjectsRepository.create('index-pattern', {
|
||||
id: 'logstash-*',
|
||||
title: 'Logstash'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWith(callAdminCluster, 'index');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('should use create action if ID defined and overwrite=false', async () => {
|
||||
await savedObjectsRepository.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
}, {
|
||||
id: 'logstash-*',
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWith(callAdminCluster, 'create');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('allows for id to be provided', async () => {
|
||||
await savedObjectsRepository.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
}, { id: 'logstash-*' });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: 'index-pattern:logstash-*'
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('self-generates an ID', async () => {
|
||||
await savedObjectsRepository.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: sinon.match(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/)
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkCreate', () => {
|
||||
it('formats Elasticsearch request', async () => {
|
||||
callAdminCluster.returns({ items: [] });
|
||||
|
||||
await savedObjectsRepository.bulkCreate([
|
||||
{ type: 'config', id: 'one', attributes: { title: 'Test One' } },
|
||||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
{ create: { _type: 'doc', _id: 'config:one' } },
|
||||
{ type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
|
||||
{ create: { _type: 'doc', _id: 'index-pattern:two' } },
|
||||
{ type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('should overwrite objects if overwrite is truthy', async () => {
|
||||
callAdminCluster.returns({ items: [] });
|
||||
|
||||
await savedObjectsRepository.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { overwrite: false });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
// uses create because overwriting is not allowed
|
||||
{ create: { _type: 'doc', _id: 'foo:bar' } },
|
||||
{ type: 'foo', ...mockTimestampFields, 'foo': {} },
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
callAdminCluster.resetHistory();
|
||||
onBeforeWrite.resetHistory();
|
||||
|
||||
await savedObjectsRepository.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { overwrite: true });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
// uses index because overwriting is allowed
|
||||
{ index: { _type: 'doc', _id: 'foo:bar' } },
|
||||
{ type: 'foo', ...mockTimestampFields, 'foo': {} },
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('returns document errors', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
errors: false,
|
||||
items: [{
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'config:one',
|
||||
error: {
|
||||
reason: 'type[config] missing'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:two',
|
||||
_version: 2
|
||||
}
|
||||
}]
|
||||
}));
|
||||
|
||||
const response = await savedObjectsRepository.bulkCreate([
|
||||
{ type: 'config', id: 'one', attributes: { title: 'Test One' } },
|
||||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
id: 'one',
|
||||
type: 'config',
|
||||
error: { message: 'type[config] missing' }
|
||||
}, {
|
||||
id: 'two',
|
||||
type: 'index-pattern',
|
||||
version: 2,
|
||||
...mockTimestampFields,
|
||||
attributes: { title: 'Test Two' },
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
errors: false,
|
||||
items: [{
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'config:one',
|
||||
_version: 2
|
||||
}
|
||||
}, {
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:two',
|
||||
_version: 2
|
||||
}
|
||||
}]
|
||||
}));
|
||||
|
||||
const response = await savedObjectsRepository.bulkCreate([
|
||||
{ type: 'config', id: 'one', attributes: { title: 'Test One' } },
|
||||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
id: 'one',
|
||||
type: 'config',
|
||||
version: 2,
|
||||
...mockTimestampFields,
|
||||
attributes: { title: 'Test One' },
|
||||
}, {
|
||||
id: 'two',
|
||||
type: 'index-pattern',
|
||||
version: 2,
|
||||
...mockTimestampFields,
|
||||
attributes: { title: 'Test Two' },
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('throws notFound when ES is unable to find the document', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
result: 'not_found'
|
||||
}));
|
||||
|
||||
try {
|
||||
await savedObjectsRepository.delete('index-pattern', 'logstash-*');
|
||||
} catch(e) {
|
||||
expect(e.output.statusCode).toEqual(404);
|
||||
}
|
||||
});
|
||||
|
||||
it('passes the parameters to callAdminCluster', async () => {
|
||||
callAdminCluster.returns({
|
||||
result: 'deleted'
|
||||
});
|
||||
await savedObjectsRepository.delete('index-pattern', 'logstash-*');
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'delete', {
|
||||
type: 'doc',
|
||||
id: 'index-pattern:logstash-*',
|
||||
refresh: 'wait_for',
|
||||
index: '.kibana-test',
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.returns(searchResults);
|
||||
});
|
||||
|
||||
it('requires searchFields be an array if defined', async () => {
|
||||
try {
|
||||
await savedObjectsRepository.find({ searchFields: 'string' });
|
||||
throw new Error('expected find() to reject');
|
||||
} catch (error) {
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
expect(error.message).toMatch('must be an array');
|
||||
}
|
||||
});
|
||||
|
||||
it('requires fields be an array if defined', async () => {
|
||||
try {
|
||||
await savedObjectsRepository.find({ fields: 'string' });
|
||||
throw new Error('expected find() to reject');
|
||||
} catch (error) {
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
expect(error.message).toMatch('must be an array');
|
||||
}
|
||||
});
|
||||
|
||||
it('passes mappings, search, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => {
|
||||
const relevantOpts = {
|
||||
search: 'foo*',
|
||||
searchFields: ['foo'],
|
||||
type: 'bar',
|
||||
sortField: 'name',
|
||||
sortOrder: 'desc',
|
||||
includeTypes: ['index-pattern', 'dashboard'],
|
||||
};
|
||||
|
||||
await savedObjectsRepository.find(relevantOpts);
|
||||
sinon.assert.calledOnce(getSearchDsl);
|
||||
sinon.assert.calledWithExactly(getSearchDsl, mappings, relevantOpts);
|
||||
});
|
||||
|
||||
it('merges output of getSearchDsl into es request body', async () => {
|
||||
getSearchDsl.returns({ query: 1, aggregations: 2 });
|
||||
await savedObjectsRepository.find();
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'search', sinon.match({
|
||||
body: sinon.match({
|
||||
query: 1,
|
||||
aggregations: 2,
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
const count = searchResults.hits.hits.length;
|
||||
|
||||
const response = await savedObjectsRepository.find();
|
||||
|
||||
expect(response.total).toBe(count);
|
||||
expect(response.saved_objects).toHaveLength(count);
|
||||
|
||||
searchResults.hits.hits.forEach((doc, i) => {
|
||||
expect(response.saved_objects[i]).toEqual({
|
||||
id: doc._id.replace(/(index-pattern|config)\:/, ''),
|
||||
type: doc._source.type,
|
||||
...mockTimestampFields,
|
||||
version: doc._version,
|
||||
attributes: doc._source[doc._source.type]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts per_page/page', async () => {
|
||||
await savedObjectsRepository.find({ perPage: 10, page: 6 });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
size: 10,
|
||||
from: 50
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('can filter by fields', async () => {
|
||||
await savedObjectsRepository.find({ fields: ['title'] });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
_source: [
|
||||
'*.title', 'type', 'title'
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
_id: 'index-pattern:logstash-*',
|
||||
_type: 'doc',
|
||||
_version: 2,
|
||||
_source: {
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
'index-pattern': {
|
||||
title: 'Testing'
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
const response = await savedObjectsRepository.get('index-pattern', 'logstash-*');
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
expect(response).toEqual({
|
||||
id: 'logstash-*',
|
||||
type: 'index-pattern',
|
||||
updated_at: mockTimestamp,
|
||||
version: 2,
|
||||
attributes: {
|
||||
title: 'Testing'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('prepends type to the id', async () => {
|
||||
await savedObjectsRepository.get('index-pattern', 'logstash-*');
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: 'index-pattern:logstash-*',
|
||||
type: 'doc'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkGet', () => {
|
||||
it('accepts a array of mixed type and ids', async () => {
|
||||
callAdminCluster.returns({ docs: [] });
|
||||
|
||||
await savedObjectsRepository.bulkGet([
|
||||
{ id: 'one', type: 'config' },
|
||||
{ id: 'two', type: 'index-pattern' }
|
||||
]);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
body: {
|
||||
docs: [
|
||||
{ _type: 'doc', _id: 'config:one' },
|
||||
{ _type: 'doc', _id: 'index-pattern:two' }
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('returns early for empty objects argument', async () => {
|
||||
callAdminCluster.returns({ docs: [] });
|
||||
|
||||
const response = await savedObjectsRepository.bulkGet([]);
|
||||
|
||||
expect(response.saved_objects).toHaveLength(0);
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('reports error on missed objects', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
docs: [{
|
||||
_type: 'doc',
|
||||
_id: 'config:good',
|
||||
found: true,
|
||||
_version: 2,
|
||||
_source: { ...mockTimestampFields, config: { title: 'Test' } }
|
||||
}, {
|
||||
_type: 'doc',
|
||||
_id: 'config:bad',
|
||||
found: false
|
||||
}]
|
||||
}));
|
||||
|
||||
const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet(
|
||||
[{ id: 'good', type: 'config' }, { id: 'bad', type: 'config' }]
|
||||
);
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
|
||||
expect(savedObjects).toHaveLength(2);
|
||||
expect(savedObjects[0]).toEqual({
|
||||
id: 'good',
|
||||
type: 'config',
|
||||
...mockTimestampFields,
|
||||
version: 2,
|
||||
attributes: { title: 'Test' }
|
||||
});
|
||||
expect(savedObjects[1]).toEqual({
|
||||
id: 'bad',
|
||||
type: 'config',
|
||||
error: { statusCode: 404, message: 'Not found' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
result: 'updated'
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns current ES document version', async () => {
|
||||
const response = await savedObjectsRepository.update('index-pattern', 'logstash-*', attributes);
|
||||
expect(response).toEqual({
|
||||
id,
|
||||
type,
|
||||
...mockTimestampFields,
|
||||
version: newVersion,
|
||||
attributes
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts version', async () => {
|
||||
await savedObjectsRepository.update(
|
||||
type,
|
||||
id,
|
||||
{ title: 'Testing' },
|
||||
{ version: newVersion - 1 }
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
version: newVersion - 1
|
||||
}));
|
||||
});
|
||||
|
||||
it('passes the parameters to callAdminCluster', async () => {
|
||||
await savedObjectsRepository.update('index-pattern', 'logstash-*', { title: 'Testing' });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
|
||||
type: 'doc',
|
||||
id: 'index-pattern:logstash-*',
|
||||
version: undefined,
|
||||
body: {
|
||||
doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } }
|
||||
},
|
||||
ignore: [404],
|
||||
refresh: 'wait_for',
|
||||
index: '.kibana-test'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBeforeWrite', () => {
|
||||
it('blocks calls to callCluster of requests', async () => {
|
||||
onBeforeWrite.returns(delay(500));
|
||||
callAdminCluster.returns({ result: 'deleted', found: true });
|
||||
|
||||
const deletePromise = savedObjectsRepository.delete('type', 'id');
|
||||
await delay(100);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
await deletePromise;
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
});
|
||||
|
||||
it('can throw es errors and have them decorated as SavedObjectsClient errors', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const es401 = new elasticsearch.errors[401];
|
||||
expect(errors.isNotAuthorizedError(es401)).toBe(false);
|
||||
onBeforeWrite.throws(es401);
|
||||
|
||||
try {
|
||||
await savedObjectsRepository.delete('type', 'id');
|
||||
} catch (error) {
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
expect(error).toBe(es401);
|
||||
expect(errors.isNotAuthorizedError(error)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,29 +1,11 @@
|
|||
import uuid from 'uuid';
|
||||
|
||||
import { getRootType } from '../../mappings';
|
||||
|
||||
import {
|
||||
getSearchDsl,
|
||||
trimIdPrefix,
|
||||
includedFields,
|
||||
decorateEsError,
|
||||
SavedObjectsRepository,
|
||||
errors,
|
||||
} from './lib';
|
||||
|
||||
export class SavedObjectsClient {
|
||||
constructor(options) {
|
||||
const {
|
||||
index,
|
||||
mappings,
|
||||
callCluster,
|
||||
onBeforeWrite = () => {},
|
||||
} = options;
|
||||
|
||||
this._index = index;
|
||||
this._mappings = mappings;
|
||||
this._type = getRootType(this._mappings);
|
||||
this._onBeforeWrite = onBeforeWrite;
|
||||
this._unwrappedCallCluster = callCluster;
|
||||
this._repository = new SavedObjectsRepository(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,42 +87,7 @@ export class SavedObjectsClient {
|
|||
* @returns {promise} - { id, type, version, attributes }
|
||||
*/
|
||||
async create(type, attributes = {}, options = {}) {
|
||||
const {
|
||||
id,
|
||||
overwrite = false
|
||||
} = options;
|
||||
|
||||
const method = id && !overwrite ? 'create' : 'index';
|
||||
const time = this._getCurrentTime();
|
||||
|
||||
try {
|
||||
const response = await this._writeToCluster(method, {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
type,
|
||||
updated_at: time,
|
||||
[type]: attributes
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: trimIdPrefix(response._id, type),
|
||||
type,
|
||||
updated_at: time,
|
||||
version: response._version,
|
||||
attributes
|
||||
};
|
||||
} catch (error) {
|
||||
if (errors.isNotFoundError(error)) {
|
||||
// See "503s from missing index" above
|
||||
throw errors.createEsAutoCreateIndexError();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
return this._repository.create(type, attributes, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,68 +99,7 @@ export class SavedObjectsClient {
|
|||
* @returns {promise} - [{ id, type, version, attributes, error: { message } }]
|
||||
*/
|
||||
async bulkCreate(objects, options = {}) {
|
||||
const {
|
||||
overwrite = false
|
||||
} = options;
|
||||
const time = this._getCurrentTime();
|
||||
const objectToBulkRequest = (object) => {
|
||||
const method = object.id && !overwrite ? 'create' : 'index';
|
||||
|
||||
return [
|
||||
{
|
||||
[method]: {
|
||||
_id: this._generateEsId(object.type, object.id),
|
||||
_type: this._type,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: object.type,
|
||||
updated_at: time,
|
||||
[object.type]: object.attributes
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const { items } = await this._writeToCluster('bulk', {
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
body: objects.reduce((acc, object) => ([
|
||||
...acc,
|
||||
...objectToBulkRequest(object)
|
||||
]), []),
|
||||
});
|
||||
|
||||
return items.map((response, i) => {
|
||||
const {
|
||||
error,
|
||||
_id: responseId,
|
||||
_version: version,
|
||||
} = Object.values(response)[0];
|
||||
|
||||
const {
|
||||
id = responseId,
|
||||
type,
|
||||
attributes,
|
||||
} = objects[i];
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: {
|
||||
message: error.reason || JSON.stringify(error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updated_at: time,
|
||||
version,
|
||||
attributes
|
||||
};
|
||||
});
|
||||
return this._repository.bulkCreate(objects, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -224,29 +110,7 @@ export class SavedObjectsClient {
|
|||
* @returns {promise}
|
||||
*/
|
||||
async delete(type, id) {
|
||||
const response = await this._writeToCluster('delete', {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
const deleted = response.result === 'deleted';
|
||||
if (deleted) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const docNotFound = response.result === 'not_found';
|
||||
const indexNotFound = response.error && response.error.type === 'index_not_found_exception';
|
||||
if (docNotFound || indexNotFound) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response, })}`
|
||||
);
|
||||
return this._repository.delete(type, id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -263,73 +127,7 @@ export class SavedObjectsClient {
|
|||
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
|
||||
*/
|
||||
async find(options = {}) {
|
||||
const {
|
||||
type,
|
||||
search,
|
||||
searchFields,
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
sortField,
|
||||
sortOrder,
|
||||
fields,
|
||||
includeTypes,
|
||||
} = options;
|
||||
|
||||
if (searchFields && !Array.isArray(searchFields)) {
|
||||
throw new TypeError('options.searchFields must be an array');
|
||||
}
|
||||
|
||||
if (fields && !Array.isArray(fields)) {
|
||||
throw new TypeError('options.searchFields must be an array');
|
||||
}
|
||||
|
||||
const esOptions = {
|
||||
index: this._index,
|
||||
size: perPage,
|
||||
from: perPage * (page - 1),
|
||||
_source: includedFields(type, fields),
|
||||
ignore: [404],
|
||||
body: {
|
||||
version: true,
|
||||
...getSearchDsl(this._mappings, {
|
||||
search,
|
||||
searchFields,
|
||||
type,
|
||||
includeTypes,
|
||||
sortField,
|
||||
sortOrder
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const response = await this._callCluster('search', esOptions);
|
||||
|
||||
if (response.status === 404) {
|
||||
// 404 is only possible here if the index is missing, which
|
||||
// we don't want to leak, see "404s from missing index" above
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total: response.hits.total,
|
||||
saved_objects: response.hits.hits.map(hit => {
|
||||
const { type, updated_at: updatedAt } = hit._source;
|
||||
return {
|
||||
id: trimIdPrefix(hit._id, type),
|
||||
type,
|
||||
...updatedAt && { updated_at: updatedAt },
|
||||
version: hit._version,
|
||||
attributes: hit._source[type],
|
||||
};
|
||||
}),
|
||||
};
|
||||
return this._repository.find(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -345,42 +143,7 @@ export class SavedObjectsClient {
|
|||
* ])
|
||||
*/
|
||||
async bulkGet(objects = []) {
|
||||
if (objects.length === 0) {
|
||||
return { saved_objects: [] };
|
||||
}
|
||||
|
||||
const response = await this._callCluster('mget', {
|
||||
index: this._index,
|
||||
body: {
|
||||
docs: objects.map(object => ({
|
||||
_id: this._generateEsId(object.type, object.id),
|
||||
_type: this._type,
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
saved_objects: response.docs.map((doc, i) => {
|
||||
const { id, type } = objects[i];
|
||||
|
||||
if (!doc.found) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: { statusCode: 404, message: 'Not found' }
|
||||
};
|
||||
}
|
||||
|
||||
const time = doc._source.updated_at;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
...time && { updated_at: time },
|
||||
version: doc._version,
|
||||
attributes: doc._source[type]
|
||||
};
|
||||
})
|
||||
};
|
||||
return this._repository.bulkGet(objects);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -391,29 +154,7 @@ export class SavedObjectsClient {
|
|||
* @returns {promise} - { id, type, version, attributes }
|
||||
*/
|
||||
async get(type, id) {
|
||||
const response = await this._callCluster('get', {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
ignore: [404]
|
||||
});
|
||||
|
||||
const docNotFound = response.found === false;
|
||||
const indexNotFound = response.status === 404;
|
||||
if (docNotFound || indexNotFound) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
const { updated_at: updatedAt } = response._source;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
...updatedAt && { updated_at: updatedAt },
|
||||
version: response._version,
|
||||
attributes: response._source[type]
|
||||
};
|
||||
return this._repository.get(type, id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -426,58 +167,6 @@ export class SavedObjectsClient {
|
|||
* @returns {promise}
|
||||
*/
|
||||
async update(type, id, attributes, options = {}) {
|
||||
const time = this._getCurrentTime();
|
||||
const response = await this._writeToCluster('update', {
|
||||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
version: options.version,
|
||||
refresh: 'wait_for',
|
||||
ignore: [404],
|
||||
body: {
|
||||
doc: {
|
||||
updated_at: time,
|
||||
[type]: attributes
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updated_at: time,
|
||||
version: response._version,
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
async _writeToCluster(method, params) {
|
||||
try {
|
||||
await this._onBeforeWrite();
|
||||
return await this._callCluster(method, params);
|
||||
} catch (err) {
|
||||
throw decorateEsError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async _callCluster(method, params) {
|
||||
try {
|
||||
return await this._unwrappedCallCluster(method, params);
|
||||
} catch (err) {
|
||||
throw decorateEsError(err);
|
||||
}
|
||||
}
|
||||
|
||||
_generateEsId(type, id) {
|
||||
return `${type}:${id || uuid.v1()}`;
|
||||
}
|
||||
|
||||
_getCurrentTime() {
|
||||
return new Date().toISOString();
|
||||
return this._repository.update(type, id, attributes, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,643 +1,124 @@
|
|||
import sinon from 'sinon';
|
||||
import { delay } from 'bluebird';
|
||||
import { SavedObjectsClient } from './saved_objects_client';
|
||||
import * as getSearchDslNS from './lib/search_dsl/search_dsl';
|
||||
import { getSearchDsl } from './lib';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
import { SavedObjectsRepository } from './lib/repository';
|
||||
jest.mock('./lib/repository');
|
||||
|
||||
describe('SavedObjectsClient', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
let callAdminCluster;
|
||||
let onBeforeWrite;
|
||||
let savedObjectsClient;
|
||||
const mockTimestamp = '2017-08-14T15:49:14.886Z';
|
||||
const mockTimestampFields = { updated_at: mockTimestamp };
|
||||
const searchResults = {
|
||||
hits: {
|
||||
total: 3,
|
||||
hits: [{
|
||||
_index: '.kibana',
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:logstash-*',
|
||||
_score: 1,
|
||||
_source: {
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
'index-pattern': {
|
||||
title: 'logstash-*',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
_index: '.kibana',
|
||||
_type: 'doc',
|
||||
_id: 'config:6.0.0-alpha1',
|
||||
_score: 1,
|
||||
_source: {
|
||||
type: 'config',
|
||||
...mockTimestampFields,
|
||||
config: {
|
||||
buildNum: 8467,
|
||||
defaultIndex: 'logstash-*'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
_index: '.kibana',
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:stocks-*',
|
||||
_score: 1,
|
||||
_source: {
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
'index-pattern': {
|
||||
title: 'stocks-*',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const mappings = {
|
||||
doc: {
|
||||
properties: {
|
||||
'index-pattern': {
|
||||
properties: {
|
||||
someField: {
|
||||
type: 'keyword'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
callAdminCluster = sandbox.stub();
|
||||
onBeforeWrite = sandbox.stub();
|
||||
|
||||
savedObjectsClient = new SavedObjectsClient({
|
||||
index: '.kibana-test',
|
||||
mappings,
|
||||
callCluster: callAdminCluster,
|
||||
onBeforeWrite
|
||||
});
|
||||
|
||||
sandbox.stub(savedObjectsClient, '_getCurrentTime').returns(mockTimestamp);
|
||||
sandbox.stub(getSearchDslNS, 'getSearchDsl').returns({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
|
||||
describe('#create', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:logstash-*',
|
||||
_version: 2
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
const response = await savedObjectsClient.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
type: 'index-pattern',
|
||||
id: 'logstash-*',
|
||||
...mockTimestampFields,
|
||||
version: 2,
|
||||
attributes: {
|
||||
title: 'Logstash',
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use ES index action', async () => {
|
||||
await savedObjectsClient.create('index-pattern', {
|
||||
id: 'logstash-*',
|
||||
title: 'Logstash'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWith(callAdminCluster, 'index');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('should use create action if ID defined and overwrite=false', async () => {
|
||||
await savedObjectsClient.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
}, {
|
||||
id: 'logstash-*',
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWith(callAdminCluster, 'create');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('allows for id to be provided', async () => {
|
||||
await savedObjectsClient.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
}, { id: 'logstash-*' });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: 'index-pattern:logstash-*'
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('self-generates an ID', async () => {
|
||||
await savedObjectsClient.create('index-pattern', {
|
||||
title: 'Logstash'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: sinon.match(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/)
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkCreate', () => {
|
||||
it('formats Elasticsearch request', async () => {
|
||||
callAdminCluster.returns({ items: [] });
|
||||
|
||||
await savedObjectsClient.bulkCreate([
|
||||
{ type: 'config', id: 'one', attributes: { title: 'Test One' } },
|
||||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
{ create: { _type: 'doc', _id: 'config:one' } },
|
||||
{ type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
|
||||
{ create: { _type: 'doc', _id: 'index-pattern:two' } },
|
||||
{ type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('should overwrite objects if overwrite is truthy', async () => {
|
||||
callAdminCluster.returns({ items: [] });
|
||||
|
||||
await savedObjectsClient.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { overwrite: false });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
// uses create because overwriting is not allowed
|
||||
{ create: { _type: 'doc', _id: 'foo:bar' } },
|
||||
{ type: 'foo', ...mockTimestampFields, 'foo': {} },
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
callAdminCluster.resetHistory();
|
||||
onBeforeWrite.resetHistory();
|
||||
|
||||
await savedObjectsClient.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { overwrite: true });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
// uses index because overwriting is allowed
|
||||
{ index: { _type: 'doc', _id: 'foo:bar' } },
|
||||
{ type: 'foo', ...mockTimestampFields, 'foo': {} },
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('returns document errors', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
errors: false,
|
||||
items: [{
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'config:one',
|
||||
error: {
|
||||
reason: 'type[config] missing'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:two',
|
||||
_version: 2
|
||||
}
|
||||
}]
|
||||
}));
|
||||
|
||||
const response = await savedObjectsClient.bulkCreate([
|
||||
{ type: 'config', id: 'one', attributes: { title: 'Test One' } },
|
||||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
id: 'one',
|
||||
type: 'config',
|
||||
error: { message: 'type[config] missing' }
|
||||
}, {
|
||||
id: 'two',
|
||||
type: 'index-pattern',
|
||||
version: 2,
|
||||
...mockTimestampFields,
|
||||
attributes: { title: 'Test Two' },
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
errors: false,
|
||||
items: [{
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'config:one',
|
||||
_version: 2
|
||||
}
|
||||
}, {
|
||||
create: {
|
||||
_type: 'doc',
|
||||
_id: 'index-pattern:two',
|
||||
_version: 2
|
||||
}
|
||||
}]
|
||||
}));
|
||||
|
||||
const response = await savedObjectsClient.bulkCreate([
|
||||
{ type: 'config', id: 'one', attributes: { title: 'Test One' } },
|
||||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
id: 'one',
|
||||
type: 'config',
|
||||
version: 2,
|
||||
...mockTimestampFields,
|
||||
attributes: { title: 'Test One' },
|
||||
}, {
|
||||
id: 'two',
|
||||
type: 'index-pattern',
|
||||
version: 2,
|
||||
...mockTimestampFields,
|
||||
attributes: { title: 'Test Two' },
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('throws notFound when ES is unable to find the document', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
result: 'not_found'
|
||||
}));
|
||||
|
||||
try {
|
||||
await savedObjectsClient.delete('index-pattern', 'logstash-*');
|
||||
} catch(e) {
|
||||
expect(e.output.statusCode).toEqual(404);
|
||||
}
|
||||
});
|
||||
|
||||
it('passes the parameters to callAdminCluster', async () => {
|
||||
callAdminCluster.returns({
|
||||
result: 'deleted'
|
||||
});
|
||||
await savedObjectsClient.delete('index-pattern', 'logstash-*');
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'delete', {
|
||||
type: 'doc',
|
||||
id: 'index-pattern:logstash-*',
|
||||
refresh: 'wait_for',
|
||||
index: '.kibana-test',
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.returns(searchResults);
|
||||
});
|
||||
|
||||
it('requires searchFields be an array if defined', async () => {
|
||||
try {
|
||||
await savedObjectsClient.find({ searchFields: 'string' });
|
||||
throw new Error('expected find() to reject');
|
||||
} catch (error) {
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
expect(error.message).toMatch('must be an array');
|
||||
}
|
||||
});
|
||||
|
||||
it('requires fields be an array if defined', async () => {
|
||||
try {
|
||||
await savedObjectsClient.find({ fields: 'string' });
|
||||
throw new Error('expected find() to reject');
|
||||
} catch (error) {
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
expect(error.message).toMatch('must be an array');
|
||||
}
|
||||
});
|
||||
|
||||
it('passes mappings, search, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => {
|
||||
const relevantOpts = {
|
||||
search: 'foo*',
|
||||
searchFields: ['foo'],
|
||||
type: 'bar',
|
||||
sortField: 'name',
|
||||
sortOrder: 'desc',
|
||||
includeTypes: ['index-pattern', 'dashboard'],
|
||||
};
|
||||
|
||||
await savedObjectsClient.find(relevantOpts);
|
||||
sinon.assert.calledOnce(getSearchDsl);
|
||||
sinon.assert.calledWithExactly(getSearchDsl, mappings, relevantOpts);
|
||||
});
|
||||
|
||||
it('merges output of getSearchDsl into es request body', async () => {
|
||||
getSearchDsl.returns({ query: 1, aggregations: 2 });
|
||||
await savedObjectsClient.find();
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'search', sinon.match({
|
||||
body: sinon.match({
|
||||
query: 1,
|
||||
aggregations: 2,
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
const count = searchResults.hits.hits.length;
|
||||
|
||||
const response = await savedObjectsClient.find();
|
||||
|
||||
expect(response.total).toBe(count);
|
||||
expect(response.saved_objects).toHaveLength(count);
|
||||
|
||||
searchResults.hits.hits.forEach((doc, i) => {
|
||||
expect(response.saved_objects[i]).toEqual({
|
||||
id: doc._id.replace(/(index-pattern|config)\:/, ''),
|
||||
type: doc._source.type,
|
||||
...mockTimestampFields,
|
||||
version: doc._version,
|
||||
attributes: doc._source[doc._source.type]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts per_page/page', async () => {
|
||||
await savedObjectsClient.find({ perPage: 10, page: 6 });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
size: 10,
|
||||
from: 50
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('can filter by fields', async () => {
|
||||
await savedObjectsClient.find({ fields: ['title'] });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
_source: [
|
||||
'*.title', 'type', 'title'
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
_id: 'index-pattern:logstash-*',
|
||||
_type: 'doc',
|
||||
_version: 2,
|
||||
_source: {
|
||||
type: 'index-pattern',
|
||||
...mockTimestampFields,
|
||||
'index-pattern': {
|
||||
title: 'Testing'
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
const response = await savedObjectsClient.get('index-pattern', 'logstash-*');
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
expect(response).toEqual({
|
||||
id: 'logstash-*',
|
||||
type: 'index-pattern',
|
||||
updated_at: mockTimestamp,
|
||||
version: 2,
|
||||
attributes: {
|
||||
title: 'Testing'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('prepends type to the id', async () => {
|
||||
await savedObjectsClient.get('index-pattern', 'logstash-*');
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: 'index-pattern:logstash-*',
|
||||
type: 'doc'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkGet', () => {
|
||||
it('accepts a array of mixed type and ids', async () => {
|
||||
callAdminCluster.returns({ docs: [] });
|
||||
|
||||
await savedObjectsClient.bulkGet([
|
||||
{ id: 'one', type: 'config' },
|
||||
{ id: 'two', type: 'index-pattern' }
|
||||
]);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
body: {
|
||||
docs: [
|
||||
{ _type: 'doc', _id: 'config:one' },
|
||||
{ _type: 'doc', _id: 'index-pattern:two' }
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('returns early for empty objects argument', async () => {
|
||||
callAdminCluster.returns({ docs: [] });
|
||||
|
||||
const response = await savedObjectsClient.bulkGet([]);
|
||||
|
||||
expect(response.saved_objects).toHaveLength(0);
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('reports error on missed objects', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
docs: [{
|
||||
_type: 'doc',
|
||||
_id: 'config:good',
|
||||
found: true,
|
||||
_version: 2,
|
||||
_source: { ...mockTimestampFields, config: { title: 'Test' } }
|
||||
}, {
|
||||
_type: 'doc',
|
||||
_id: 'config:bad',
|
||||
found: false
|
||||
}]
|
||||
}));
|
||||
|
||||
const { saved_objects: savedObjects } = await savedObjectsClient.bulkGet(
|
||||
[{ id: 'good', type: 'config' }, { id: 'bad', type: 'config' }]
|
||||
);
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
|
||||
expect(savedObjects).toHaveLength(2);
|
||||
expect(savedObjects[0]).toEqual({
|
||||
id: 'good',
|
||||
type: 'config',
|
||||
...mockTimestampFields,
|
||||
version: 2,
|
||||
attributes: { title: 'Test' }
|
||||
});
|
||||
expect(savedObjects[1]).toEqual({
|
||||
id: 'bad',
|
||||
type: 'config',
|
||||
error: { statusCode: 404, message: 'Not found' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
result: 'updated'
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns current ES document version', async () => {
|
||||
const response = await savedObjectsClient.update('index-pattern', 'logstash-*', attributes);
|
||||
expect(response).toEqual({
|
||||
id,
|
||||
type,
|
||||
...mockTimestampFields,
|
||||
version: newVersion,
|
||||
attributes
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts version', async () => {
|
||||
await savedObjectsClient.update(
|
||||
type,
|
||||
id,
|
||||
{ title: 'Testing' },
|
||||
{ version: newVersion - 1 }
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
version: newVersion - 1
|
||||
}));
|
||||
});
|
||||
|
||||
it('passes the parameters to callAdminCluster', async () => {
|
||||
await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
|
||||
type: 'doc',
|
||||
id: 'index-pattern:logstash-*',
|
||||
version: undefined,
|
||||
body: {
|
||||
doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } }
|
||||
},
|
||||
ignore: [404],
|
||||
refresh: 'wait_for',
|
||||
index: '.kibana-test'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBeforeWrite', () => {
|
||||
it('blocks calls to callCluster of requests', async () => {
|
||||
onBeforeWrite.returns(delay(500));
|
||||
callAdminCluster.returns({ result: 'deleted', found: true });
|
||||
|
||||
const deletePromise = savedObjectsClient.delete('type', 'id');
|
||||
await delay(100);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
await deletePromise;
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
});
|
||||
|
||||
it('can throw es errors and have them decorated as SavedObjectsClient errors', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const es401 = new elasticsearch.errors[401];
|
||||
expect(SavedObjectsClient.errors.isNotAuthorizedError(es401)).toBe(false);
|
||||
onBeforeWrite.throws(es401);
|
||||
|
||||
try {
|
||||
await savedObjectsClient.delete('type', 'id');
|
||||
} catch (error) {
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
expect(error).toBe(es401);
|
||||
expect(SavedObjectsClient.errors.isNotAuthorizedError(error)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
SavedObjectsRepository.mockClear();
|
||||
});
|
||||
|
||||
const setupMockRepository = (mock) => {
|
||||
SavedObjectsRepository.mockImplementation(() => mock);
|
||||
return mock;
|
||||
};
|
||||
|
||||
test(`#constructor`, () => {
|
||||
const options = {};
|
||||
new SavedObjectsClient(options);
|
||||
expect(SavedObjectsRepository).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
test(`#create`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
create: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const type = 'foo';
|
||||
const attributes = {};
|
||||
const options = {};
|
||||
const result = await client.create(type, attributes, options);
|
||||
|
||||
expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#bulkCreate`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
bulkCreate: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const objects = [];
|
||||
const options = {};
|
||||
const result = await client.bulkCreate(objects, options);
|
||||
|
||||
expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#delete`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
delete: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const type = 'foo';
|
||||
const id = 1;
|
||||
const result = await client.delete(type, id);
|
||||
|
||||
expect(mockRepository.delete).toHaveBeenCalledWith(type, id);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#find`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
find: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const options = {};
|
||||
const result = await client.find(options);
|
||||
|
||||
expect(mockRepository.find).toHaveBeenCalledWith(options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#bulkGet`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
bulkGet: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const objects = {};
|
||||
const result = await client.bulkGet(objects);
|
||||
|
||||
expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#get`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
get: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const type = 'foo';
|
||||
const id = 1;
|
||||
const result = await client.get(type, id);
|
||||
|
||||
expect(mockRepository.get).toHaveBeenCalledWith(type, id);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#update`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = setupMockRepository({
|
||||
update: jest.fn().mockReturnValue(Promise.resolve(returnValue)),
|
||||
});
|
||||
const client = new SavedObjectsClient();
|
||||
|
||||
const type = 'foo';
|
||||
const id = 1;
|
||||
const attributes = {};
|
||||
const options = {};
|
||||
const result = await client.update(type, id, attributes, options);
|
||||
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue