From b250ad9297c47fb83ab39ec78e3fe9e45f2c0c99 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Sun, 14 May 2017 20:36:15 -0700 Subject: [PATCH 01/25] Adds saved object API Signed-off-by: Tyler Smalley --- package.json | 4 +- src/server/kbn_server.js | 8 + .../client/__tests__/saved_objects_client.js | 260 ++++++++++++++ src/server/saved_objects/client/index.js | 1 + .../lib/__tests__/create_filter_path.js | 25 ++ .../client/lib/__tests__/create_find_query.js | 78 +++++ .../client/lib/create_filter_path.js | 9 + .../client/lib/create_find_query.js | 39 +++ .../client/lib/handle_es_error.js | 46 +++ src/server/saved_objects/client/lib/index.js | 3 + .../client/saved_objects_client.js | 102 ++++++ src/server/saved_objects/index.js | 1 + .../saved_objects/routes/__tests__/create.js | 67 ++++ .../saved_objects/routes/__tests__/delete.js | 57 +++ .../saved_objects/routes/__tests__/find.js | 148 ++++++++ .../routes/__tests__/mock_server.js | 18 + .../saved_objects/routes/__tests__/read.js | 63 ++++ .../saved_objects/routes/__tests__/update.js | 62 ++++ src/server/saved_objects/routes/create.js | 30 ++ src/server/saved_objects/routes/delete.js | 21 ++ src/server/saved_objects/routes/find.js | 31 ++ src/server/saved_objects/routes/index.js | 5 + src/server/saved_objects/routes/read.js | 21 ++ src/server/saved_objects/routes/update.js | 24 ++ .../saved_objects/saved_objects_mixin.js | 30 ++ .../__tests__/saved_objects_client.js | 324 ++++++++++++++++++ src/ui/public/saved_objects/index.js | 4 + src/ui/public/saved_objects/saved_object.js | 36 ++ .../saved_objects/saved_objects_client.js | 95 +++++ .../saved_objects_client_provider.js | 7 + 30 files changed, 1617 insertions(+), 2 deletions(-) create mode 100644 src/server/saved_objects/client/__tests__/saved_objects_client.js create mode 100644 src/server/saved_objects/client/index.js create mode 100644 src/server/saved_objects/client/lib/__tests__/create_filter_path.js create mode 100644 src/server/saved_objects/client/lib/__tests__/create_find_query.js create mode 100644 src/server/saved_objects/client/lib/create_filter_path.js create mode 100644 src/server/saved_objects/client/lib/create_find_query.js create mode 100644 src/server/saved_objects/client/lib/handle_es_error.js create mode 100644 src/server/saved_objects/client/lib/index.js create mode 100644 src/server/saved_objects/client/saved_objects_client.js create mode 100644 src/server/saved_objects/index.js create mode 100644 src/server/saved_objects/routes/__tests__/create.js create mode 100644 src/server/saved_objects/routes/__tests__/delete.js create mode 100644 src/server/saved_objects/routes/__tests__/find.js create mode 100644 src/server/saved_objects/routes/__tests__/mock_server.js create mode 100644 src/server/saved_objects/routes/__tests__/read.js create mode 100644 src/server/saved_objects/routes/__tests__/update.js create mode 100644 src/server/saved_objects/routes/create.js create mode 100644 src/server/saved_objects/routes/delete.js create mode 100644 src/server/saved_objects/routes/find.js create mode 100644 src/server/saved_objects/routes/index.js create mode 100644 src/server/saved_objects/routes/read.js create mode 100644 src/server/saved_objects/routes/update.js create mode 100644 src/server/saved_objects/saved_objects_mixin.js create mode 100644 src/ui/public/saved_objects/__tests__/saved_objects_client.js create mode 100644 src/ui/public/saved_objects/index.js create mode 100644 src/ui/public/saved_objects/saved_object.js create mode 100644 src/ui/public/saved_objects/saved_objects_client.js create mode 100644 src/ui/public/saved_objects/saved_objects_client_provider.js diff --git a/package.json b/package.json index c5f63d1662861..13fdf8e6df5cb 100644 --- a/package.json +++ b/package.json @@ -115,8 +115,8 @@ "d3": "3.5.6", "d3-cloud": "1.2.1", "dragula": "3.7.0", - "elasticsearch": "13.0.0-beta2", - "elasticsearch-browser": "13.0.0-beta2", + "elasticsearch": "13.0.1", + "elasticsearch-browser": "13.0.1", "encode-uri-query": "1.0.0", "even-better": "7.0.2", "expiry-js": "0.1.7", diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 28b16c0b9a15d..6ddd73aced884 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -19,6 +19,7 @@ import uiMixin from '../ui'; import uiSettingsMixin from '../ui/settings'; import optimizeMixin from '../optimize'; import pluginsInitializeMixin from './plugins/initialize'; +import { savedObjectsMixin } from './saved_objects'; const rootDir = fromRoot('.'); @@ -38,8 +39,10 @@ module.exports = class KbnServer { loggingMixin, warningsMixin, statusMixin, + // writes pid file pidMixin, + // find plugins and set this.plugins pluginsScanMixin, @@ -51,15 +54,20 @@ module.exports = class KbnServer { // tell the config we are done loading plugins configCompleteMixin, + // setup this.uiExports and this.bundles uiMixin, + // setup saved object routes + savedObjectsMixin, + // setup server.uiSettings uiSettingsMixin, // ensure that all bundles are built, or that the // lazy bundle server is running optimizeMixin, + // finally, initialize the plugins pluginsInitializeMixin, () => { diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js new file mode 100644 index 0000000000000..4acc8be47fed7 --- /dev/null +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -0,0 +1,260 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { SavedObjectsClient } from '../saved_objects_client'; + +describe('SavedObjectsClient', () => { + let callWithRequest; + let savedObjectsClient; + const docs = { + hits: { + total: 3, + hits: [{ + _index: '.kibana', + _type: 'index-pattern', + _id: 'logstash-*', + _score: 1, + _source: { + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true + } + }, { + _index: '.kibana', + _type: 'config', + _id: '6.0.0-alpha1', + _score: 1, + _source: { + buildNum: 8467, + defaultIndex: 'logstash-*' + } + }, { + _index: '.kibana', + _type: 'index-pattern', + _id: 'stocks-*', + _score: 1, + _source: { + title: 'stocks-*', + timeFieldName: '@timestamp', + notExpandable: true + } + }] + } + }; + + beforeEach(() => { + callWithRequest = sinon.mock(); + savedObjectsClient = new SavedObjectsClient('.kibana-test', {}, callWithRequest); + }); + + afterEach(() => { + callWithRequest.reset(); + }); + + + describe('#create', () => { + it('formats Elasticsearch response', async () => { + callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*' }); + + const response = await savedObjectsClient.create('index-pattern', { + id: 'logstash-*', + title: 'Logstash' + }); + + expect(response).to.eql({ + type: 'index-pattern', + id: 'logstash-*', + title: 'Logstash' + }); + }); + + it('should use ES create action with specifying an id', async () => { + callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*' }); + + await savedObjectsClient.create('index-pattern', { + id: 'logstash-*', + title: 'Logstash' + }); + + expect(callWithRequest.calledOnce).to.be(true); + + const args = callWithRequest.getCall(0).args; + expect(args[1]).to.be('create'); + }); + + it('should use ES index action with specifying an id', async () => { + callWithRequest.returns({ _type: 'index-pattern', _id: 'abc123' }); + + await savedObjectsClient.create('index-pattern', { title: 'Logstash' }); + expect(callWithRequest.calledOnce).to.be(true); + + const args = callWithRequest.getCall(0).args; + expect(args[1]).to.be('index'); + }); + }); + + describe('#delete', () => { + it('returns based on ES success', async () => { + callWithRequest.returns(Promise.resolve({ deleted: 'testing' })); + const response = await savedObjectsClient.delete('index-pattern', 'logstash-*'); + + expect(response).to.be('testing'); + }); + + it('throws notFound when ES is unable to find the document', (done) => { + callWithRequest.returns(Promise.resolve({ found: false })); + + savedObjectsClient.delete('index-pattern', 'logstash-*').then(() => { + done('failed'); + }).catch(e => { + expect(e.output.statusCode).to.be(404); + done(); + }); + }); + + it('passes the parameters to callWithRequest', async () => { + await savedObjectsClient.delete('index-pattern', 'logstash-*'); + + expect(callWithRequest.calledOnce).to.be(true); + + const args = callWithRequest.getCall(0).args; + expect(args[1]).to.be('delete'); + expect(args[2]).to.eql({ + type: 'index-pattern', + id: 'logstash-*', + refresh: 'wait_for', + index: '.kibana-test' + }); + }); + }); + + describe('#find', () => { + it('formats Elasticsearch response', async () => { + const count = docs.hits.hits.length; + + callWithRequest.returns(Promise.resolve(docs)); + const response = await savedObjectsClient.find(); + + expect(response.total).to.be(count); + expect(response.data).to.have.length(count); + docs.hits.hits.forEach((doc, i) => { + expect(response.data[i]).to.eql(Object.assign( + { id: doc._id, type: doc._type }, + doc._source) + ); + }); + }); + + it('accepts per_page/page', async () => { + await savedObjectsClient.find({ per_page: 10, page: 6 }); + + expect(callWithRequest.calledOnce).to.be(true); + + const options = callWithRequest.getCall(0).args[2]; + expect(options).to.eql({ + index: '.kibana-test', + body: { query: { match_all: {} } }, + from: 50, + size: 10 + }); + }); + + it('accepts type', async () => { + await savedObjectsClient.find({ type: 'index-pattern' }); + + expect(callWithRequest.calledOnce).to.be(true); + + const options = callWithRequest.getCall(0).args[2]; + const expectedQuery = { + bool: { + must: [{ match_all: {} }], + filter: [{ term: { _type: 'index-pattern' } }] + } + }; + + expect(options).to.eql({ + from: 0, + index: '.kibana-test', + size: 20, + body: { query: expectedQuery }, + type: 'index-pattern', + }); + }); + + it('accepts fields as a string', async () => { + await savedObjectsClient.find({ fields: 'title' }); + + expect(callWithRequest.calledOnce).to.be(true); + + const options = callWithRequest.getCall(0).args[2]; + expect(options.filterPath).to.eql([ + 'hits.total', + 'hits.hits._id', + 'hits.hits._type', + 'hits.hits._source.title' + ]); + }); + + it('accepts fields as an array', async () => { + await savedObjectsClient.find({ fields: ['title', 'description'] }); + + expect(callWithRequest.calledOnce).to.be(true); + + const options = callWithRequest.getCall(0).args[2]; + expect(options.filterPath).to.eql([ + 'hits.hits._source.title', + 'hits.hits._source.description', + 'hits.total', + 'hits.hits._id', + 'hits.hits._type' + ]); + }); + }); + + describe('#get', () => { + it('formats Elasticsearch response', async () => { + callWithRequest.returns(Promise.resolve({ + _id: 'logstash-*', + _type: 'index-pattern', + _source: { + title: 'Testing' + } + })); + + const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); + expect(response).to.eql({ + id: 'logstash-*', + type: 'index-pattern', + title: 'Testing' + }); + }); + }); + + describe('#update', () => { + it('returns based on ES success', async () => { + callWithRequest.returns(Promise.resolve({ + _id: 'logstash-*', + _type: 'index-pattern', + result: 'updated' + })); + + const response = await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' }); + expect(response).to.be(true); + }); + + it('passes the parameters to callWithRequest', async () => { + await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' }); + + expect(callWithRequest.calledOnce).to.be(true); + + const args = callWithRequest.getCall(0).args; + expect(args[1]).to.be('update'); + expect(args[2]).to.eql({ + type: 'index-pattern', + id: 'logstash-*', + body: { title: 'Testing' }, + refresh: true, + index: '.kibana-test' + }); + }); + }); +}); diff --git a/src/server/saved_objects/client/index.js b/src/server/saved_objects/client/index.js new file mode 100644 index 0000000000000..4b4ac9b5dcb17 --- /dev/null +++ b/src/server/saved_objects/client/index.js @@ -0,0 +1 @@ +export { SavedObjectsClient } from './saved_objects_client'; diff --git a/src/server/saved_objects/client/lib/__tests__/create_filter_path.js b/src/server/saved_objects/client/lib/__tests__/create_filter_path.js new file mode 100644 index 0000000000000..391ade7c9e2b8 --- /dev/null +++ b/src/server/saved_objects/client/lib/__tests__/create_filter_path.js @@ -0,0 +1,25 @@ +import expect from 'expect.js'; +import { createFilterPath } from '../create_filter_path'; + +describe('createFilterPath', () => { + it('handles a string', () => { + const fields = createFilterPath('foo'); + expect(fields).to.eql([ + 'hits.total', + 'hits.hits._id', + 'hits.hits._type', + 'hits.hits._source.foo' + ]); + }); + + it('handles an array', () => { + const fields = createFilterPath(['foo', 'bar']); + expect(fields).to.eql([ + 'hits.hits._source.foo', + 'hits.hits._source.bar', + 'hits.total', + 'hits.hits._id', + 'hits.hits._type' + ]); + }); +}); diff --git a/src/server/saved_objects/client/lib/__tests__/create_find_query.js b/src/server/saved_objects/client/lib/__tests__/create_find_query.js new file mode 100644 index 0000000000000..5e6dd84451093 --- /dev/null +++ b/src/server/saved_objects/client/lib/__tests__/create_find_query.js @@ -0,0 +1,78 @@ +import expect from 'expect.js'; +import { createFindQuery } from '../create_find_query'; + +describe('createFindQuery', () => { + it('matches all when there is no type or filter', () => { + const query = createFindQuery(); + expect(query).to.eql({ query: { match_all: {} } }); + }); + + it('adds bool filter for type', () => { + const query = createFindQuery({ type: 'index-pattern' }); + expect(query).to.eql({ + query: { + bool: { + filter: [{ + term: { + _type: 'index-pattern' + } + }], + must: [{ + match_all: {} + }] + } + } + }); + }); + + it('can search across all fields', () => { + const query = createFindQuery({ search: 'foo' }); + expect(query).to.eql({ + query: { + bool: { + filter: [], + must: [{ + simple_query_string: { + query: 'foo', + all_fields: true + } + }] + } + } + }); + }); + + it('can search a single field', () => { + const query = createFindQuery({ search: 'foo', searchFields: 'title' }); + expect(query).to.eql({ + query: { + bool: { + filter: [], + must: [{ + simple_query_string: { + query: 'foo', + fields: ['title'] + } + }] + } + } + }); + }); + + it('can search across multiple fields', () => { + const query = createFindQuery({ search: 'foo', searchFields: ['title', 'description'] }); + expect(query).to.eql({ + query: { + bool: { + filter: [], + must: [{ + simple_query_string: { + query: 'foo', + fields: ['title', 'description'] + } + }] + } + } + }); + }); +}); diff --git a/src/server/saved_objects/client/lib/create_filter_path.js b/src/server/saved_objects/client/lib/create_filter_path.js new file mode 100644 index 0000000000000..39c33b5717460 --- /dev/null +++ b/src/server/saved_objects/client/lib/create_filter_path.js @@ -0,0 +1,9 @@ +export function createFilterPath(fields) { + const baseKeys = ['hits.total', 'hits.hits._id', 'hits.hits._type']; + + if (Array.isArray(fields)) { + return fields.map(f => `hits.hits._source.${f}`).concat(baseKeys); + } else if (fields) { + return baseKeys.concat([`hits.hits._source.${fields}`]); + } +} diff --git a/src/server/saved_objects/client/lib/create_find_query.js b/src/server/saved_objects/client/lib/create_find_query.js new file mode 100644 index 0000000000000..67ee22a0ef36f --- /dev/null +++ b/src/server/saved_objects/client/lib/create_find_query.js @@ -0,0 +1,39 @@ +export function createFindQuery(options = {}) { + const { type, search, searchFields } = options; + + if (!type && !search) { + return { query: { match_all: {} } }; + } + + const bool = { must: [], filter: [] }; + + if (type) { + bool.filter.push({ + term: { + _type: type + } + }); + } + + if (search) { + const simpleQueryString = { + query: search + }; + + if (!searchFields) { + simpleQueryString.all_fields = true; + } else if (Array.isArray(searchFields)) { + simpleQueryString.fields = searchFields; + } else { + simpleQueryString.fields = [searchFields]; + } + + bool.must.push({ simple_query_string: simpleQueryString }); + } else { + bool.must.push({ + match_all: {} + }); + } + + return { query: { bool } }; +} diff --git a/src/server/saved_objects/client/lib/handle_es_error.js b/src/server/saved_objects/client/lib/handle_es_error.js new file mode 100644 index 0000000000000..0bea7eac7922d --- /dev/null +++ b/src/server/saved_objects/client/lib/handle_es_error.js @@ -0,0 +1,46 @@ +import elasticsearch from 'elasticsearch'; +import Boom from 'boom'; + +const { + ConnectionFault, + ServiceUnavailable, + NoConnections, + RequestTimeout, + Conflict, + 403: Forbidden, + NotFound, + BadRequest +} = elasticsearch.errors; + +export function handleEsError(error) { + if (!(error instanceof Error)) { + throw new Error('Expected an instance of Error'); + } + + if ( + error instanceof ConnectionFault || + error instanceof ServiceUnavailable || + error instanceof NoConnections || + error instanceof RequestTimeout + ) { + throw Boom.serverTimeout(error); + } + + if (error instanceof Conflict || error.message.includes('index_template_already_exists')) { + throw Boom.conflict(error); + } + + if (error instanceof Forbidden) { + throw Boom.forbidden(error); + } + + if (error instanceof NotFound) { + throw Boom.notFound(error); + } + + if (error instanceof BadRequest) { + throw Boom.badRequest(error); + } + + throw error; +} diff --git a/src/server/saved_objects/client/lib/index.js b/src/server/saved_objects/client/lib/index.js new file mode 100644 index 0000000000000..f7d3299bc0259 --- /dev/null +++ b/src/server/saved_objects/client/lib/index.js @@ -0,0 +1,3 @@ +export { createFindQuery } from './create_find_query'; +export { createFilterPath } from './create_filter_path'; +export { handleEsError } from './handle_es_error'; diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js new file mode 100644 index 0000000000000..2752967418266 --- /dev/null +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -0,0 +1,102 @@ +import Boom from 'boom'; +import { get, omit, pick } from 'lodash'; + +import { + createFindQuery, + createFilterPath, + handleEsError, +} from './lib'; + +export class SavedObjectsClient { + constructor(kibanaIndex, request, callWithRequest) { + this._kibanaIndex = kibanaIndex; + this._request = request; + this._callWithRequest = callWithRequest; + } + + async create(type, options = {}) { + const body = omit(options, 'id'); + const id = get(options, 'id'); + const method = id ? 'create' : 'index'; + + const response = await this._withKibanaIndex(method, { + type, + id, + body + }); + + return Object.assign({ type: response._type, id: response._id }, body); + } + + async delete(type, id) { + const response = await this._withKibanaIndex('delete', { + type, + id, + refresh: 'wait_for' + }); + + if (get(response, 'found') === false) { + throw Boom.notFound(); + } + + return get(response, 'deleted', false); + } + + async find(options = {}) { + const { search, searchFields, type, fields } = options; + const esOptions = pick(options, ['type']); + const perPage = get(options, 'per_page', 20); + const page = get(options, 'page', 1); + + if (fields) { + esOptions.filterPath = createFilterPath(fields); + } + + esOptions.size = perPage; + esOptions.from = esOptions.size * (page - 1); + esOptions.body = createFindQuery({ search, searchFields, type }); + + const response = await this._withKibanaIndex('search', esOptions); + + return { + data: get(response, 'hits.hits', []).map(r => { + return Object.assign({ id: r._id, type: r._type }, r._source); + }), + total: get(response, 'hits.total', 0), + per_page: perPage, + page + + }; + } + + async get(type, id) { + const response = await this._withKibanaIndex('get', { + type, + id, + }); + + return Object.assign({ id: response._id, type: response._type }, response._source); + } + + async update(type, id, body) { + const response = await this._withKibanaIndex('update', { + type, + id, + body, + refresh: true, + }); + + return get(response, 'result') === 'updated'; + } + + async _withKibanaIndex(method, params) { + try { + return await this._callWithRequest(this._request, method, { + ...params, + index: this._kibanaIndex, + }); + } catch (err) { + throw handleEsError(err); + } + } +} diff --git a/src/server/saved_objects/index.js b/src/server/saved_objects/index.js new file mode 100644 index 0000000000000..13f15b7ddfcca --- /dev/null +++ b/src/server/saved_objects/index.js @@ -0,0 +1 @@ +export { savedObjectsMixin } from './saved_objects_mixin'; diff --git a/src/server/saved_objects/routes/__tests__/create.js b/src/server/saved_objects/routes/__tests__/create.js new file mode 100644 index 0000000000000..40bfd24a4fad8 --- /dev/null +++ b/src/server/saved_objects/routes/__tests__/create.js @@ -0,0 +1,67 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { createCreateRoute } from '../create'; +import { MockServer } from './mock_server'; + +describe('POST /api/kibana/saved_objects/{type}/{id?}', () => { + const savedObjectsClient = { create: sinon.stub() }; + let server; + + beforeEach(() => { + server = new MockServer(); + + const prereqs = { + getSavedObjectsClient: { + assign: 'savedObjectsClient', + method(request, reply) { + reply(savedObjectsClient); + } + }, + }; + + server.route(createCreateRoute(prereqs)); + }); + + afterEach(() => { + savedObjectsClient.create.reset(); + }); + + it('formats successful response', async () => { + const request = { + method: 'POST', + url: '/api/kibana/saved_objects/index-pattern', + payload: { + title: 'Testing' + } + }; + const clientResponse = { + type: 'index-pattern', + id: 'logstash-*', + title: 'Testing' + }; + + savedObjectsClient.create.returns(Promise.resolve(clientResponse)); + + const { payload, statusCode } = await server.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).to.be(200); + expect(response).to.eql(clientResponse); + }); + + it('calls upon savedObjectClient.create', async () => { + const request = { + method: 'POST', + url: '/api/kibana/saved_objects/index-pattern/logstash-*', + payload: { + title: 'Testing' + } + }; + + await server.inject(request); + expect(savedObjectsClient.create.calledOnce).to.be(true); + + const args = savedObjectsClient.create.getCall(0).args; + expect(args).to.eql(['index-pattern', { title: 'Testing', id: 'logstash-*' }]); + }); +}); diff --git a/src/server/saved_objects/routes/__tests__/delete.js b/src/server/saved_objects/routes/__tests__/delete.js new file mode 100644 index 0000000000000..64bc301ff926a --- /dev/null +++ b/src/server/saved_objects/routes/__tests__/delete.js @@ -0,0 +1,57 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { createDeleteRoute } from '../delete'; +import { MockServer } from './mock_server'; + +describe('DELETE /api/kibana/saved_objects/{type}/{id}', () => { + const savedObjectsClient = { delete: sinon.stub() }; + let server; + + beforeEach(() => { + server = new MockServer(); + + const prereqs = { + getSavedObjectsClient: { + assign: 'savedObjectsClient', + method(request, reply) { + reply(savedObjectsClient); + } + }, + }; + + server.route(createDeleteRoute(prereqs)); + }); + + afterEach(() => { + savedObjectsClient.delete.reset(); + }); + + it('formats successful response', async () => { + const request = { + method: 'DELETE', + url: '/api/kibana/saved_objects/index-pattern/logstash-*' + }; + const clientResponse = true; + + savedObjectsClient.delete.returns(Promise.resolve(clientResponse)); + + const { payload, statusCode } = await server.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).to.be(200); + expect(response).to.eql(clientResponse); + }); + + it('calls upon savedObjectClient.delete', async () => { + const request = { + method: 'DELETE', + url: '/api/kibana/saved_objects/index-pattern/logstash-*' + }; + + await server.inject(request); + expect(savedObjectsClient.delete.calledOnce).to.be(true); + + const args = savedObjectsClient.delete.getCall(0).args; + expect(args).to.eql(['index-pattern', 'logstash-*']); + }); +}); diff --git a/src/server/saved_objects/routes/__tests__/find.js b/src/server/saved_objects/routes/__tests__/find.js new file mode 100644 index 0000000000000..eecaecb4ce669 --- /dev/null +++ b/src/server/saved_objects/routes/__tests__/find.js @@ -0,0 +1,148 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { createFindRoute } from '../find'; +import { MockServer } from './mock_server'; + +describe('GET /api/kibana/saved_objects/{type?}', () => { + const savedObjectsClient = { find: sinon.stub() }; + let server; + + beforeEach(() => { + server = new MockServer(); + + const prereqs = { + getSavedObjectsClient: { + assign: 'savedObjectsClient', + method(request, reply) { + reply(savedObjectsClient); + } + }, + }; + + server.route(createFindRoute(prereqs)); + }); + + afterEach(() => { + savedObjectsClient.find.reset(); + }); + + it('formats successful response', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects' + }; + + const clientResponse = { + total: 2, + data: [ + { + type: 'index-pattern', + id: 'logstash-*', + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true + }, { + type: 'index-pattern', + id: 'stocks-*', + title: 'stocks-*', + timeFieldName: '@timestamp', + notExpandable: true + } + ] + }; + + savedObjectsClient.find.returns(Promise.resolve(clientResponse)); + + const { payload, statusCode } = await server.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).to.be(200); + expect(response).to.eql(clientResponse); + }); + + it('calls upon savedObjectClient.find with defaults', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ per_page: 20, page: 1 }); + }); + + it('accepts the query parameter page/per_page', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects?per_page=10&page=50' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ per_page: 10, page: 50 }); + }); + + it('accepts the query parameter fields as a string', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects?fields=title' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ per_page: 20, page: 1, fields: 'title' }); + }); + + it('accepts the query parameter fields as an array', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects?fields=title&fields=description' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ + per_page: 20, page: 1, fields: ['title', 'description'] + }); + }); + + it('accepts the type as a query parameter', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects?type=index-pattern' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ per_page: 20, page: 1, type: 'index-pattern' }); + }); + + it('accepts the type as a URL parameter', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects/index-pattern' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ per_page: 20, page: 1, type: 'index-pattern' }); + }); +}); diff --git a/src/server/saved_objects/routes/__tests__/mock_server.js b/src/server/saved_objects/routes/__tests__/mock_server.js new file mode 100644 index 0000000000000..f6bd1c8b92cf0 --- /dev/null +++ b/src/server/saved_objects/routes/__tests__/mock_server.js @@ -0,0 +1,18 @@ +const Hapi = require('hapi'); +const defaultConfig = { + 'kibana.index': '.kibana' +}; + +export function MockServer(config = defaultConfig) { + const server = new Hapi.Server(); + server.connection({ port: 8080 }); + server.config = function () { + return { + get: (key) => { + return config[key]; + } + }; + }; + + return server; +} diff --git a/src/server/saved_objects/routes/__tests__/read.js b/src/server/saved_objects/routes/__tests__/read.js new file mode 100644 index 0000000000000..574185942ce46 --- /dev/null +++ b/src/server/saved_objects/routes/__tests__/read.js @@ -0,0 +1,63 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { createReadRoute } from '../read'; +import { MockServer } from './mock_server'; + +describe('GET /api/kibana/saved_objects/{type}/{id}', () => { + const savedObjectsClient = { get: sinon.stub() }; + let server; + + beforeEach(() => { + server = new MockServer(); + + const prereqs = { + getSavedObjectsClient: { + assign: 'savedObjectsClient', + method(request, reply) { + reply(savedObjectsClient); + } + }, + }; + + server.route(createReadRoute(prereqs)); + }); + + afterEach(() => { + savedObjectsClient.get.reset(); + }); + + it('formats successful response', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects/index-pattern/logstash-*' + }; + const clientResponse = { + id: 'logstash-*', + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true + }; + + savedObjectsClient.get.returns(Promise.resolve(clientResponse)); + + const { payload, statusCode } = await server.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).to.be(200); + expect(response).to.eql(clientResponse); + }); + + it('calls upon savedObjectClient.get', async () => { + const request = { + method: 'GET', + url: '/api/kibana/saved_objects/index-pattern/logstash-*' + }; + + await server.inject(request); + expect(savedObjectsClient.get.calledOnce).to.be(true); + + const args = savedObjectsClient.get.getCall(0).args; + expect(args).to.eql(['index-pattern', 'logstash-*']); + }); + +}); diff --git a/src/server/saved_objects/routes/__tests__/update.js b/src/server/saved_objects/routes/__tests__/update.js new file mode 100644 index 0000000000000..20f0952dc4bcd --- /dev/null +++ b/src/server/saved_objects/routes/__tests__/update.js @@ -0,0 +1,62 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { createUpdateRoute } from '../update'; +import { MockServer } from './mock_server'; + +describe('PUT /api/kibana/saved_objects/{type}/{id?}', () => { + const savedObjectsClient = { update: sinon.stub() }; + let server; + + beforeEach(() => { + server = new MockServer(); + + const prereqs = { + getSavedObjectsClient: { + assign: 'savedObjectsClient', + method(request, reply) { + reply(savedObjectsClient); + } + }, + }; + + server.route(createUpdateRoute(prereqs)); + }); + + afterEach(() => { + savedObjectsClient.update.reset(); + }); + + it('formats successful response', async () => { + const request = { + method: 'PUT', + url: '/api/kibana/saved_objects/index-pattern/logstash-*', + payload: { + title: 'Testing' + } + }; + + savedObjectsClient.update.returns(Promise.resolve(true)); + + const { payload, statusCode } = await server.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).to.be(200); + expect(response).to.eql(true); + }); + + it('calls upon savedObjectClient.update', async () => { + const request = { + method: 'PUT', + url: '/api/kibana/saved_objects/index-pattern/logstash-*', + payload: { + title: 'Testing' + } + }; + + await server.inject(request); + expect(savedObjectsClient.update.calledOnce).to.be(true); + + const args = savedObjectsClient.update.getCall(0).args; + expect(args).to.eql(['index-pattern', 'logstash-*', { title: 'Testing' }]); + }); +}); diff --git a/src/server/saved_objects/routes/create.js b/src/server/saved_objects/routes/create.js new file mode 100644 index 0000000000000..3c358045d8505 --- /dev/null +++ b/src/server/saved_objects/routes/create.js @@ -0,0 +1,30 @@ +import Joi from 'joi'; +import { has } from 'lodash'; + +export const createCreateRoute = (prereqs) => { + return { + path: '/api/kibana/saved_objects/{type}/{id?}', + method: 'POST', + config: { + pre: [prereqs.getSavedObjectsClient], + validate: { + params: Joi.object().keys({ + type: Joi.string().required(), + id: Joi.string() + }).required(), + payload: Joi.object().required() + }, + handler(request, reply) { + const { savedObjectsClient } = request.pre; + const { type } = request.params; + const body = Object.assign({}, request.payload); + + if (has(request.params, 'id')) { + body.id = request.params.id; + } + + reply(savedObjectsClient.create(type, body)); + } + } + }; +}; diff --git a/src/server/saved_objects/routes/delete.js b/src/server/saved_objects/routes/delete.js new file mode 100644 index 0000000000000..a67cd5809cd44 --- /dev/null +++ b/src/server/saved_objects/routes/delete.js @@ -0,0 +1,21 @@ +import Joi from 'joi'; + +export const createDeleteRoute = (prereqs) => ({ + path: '/api/kibana/saved_objects/{type}/{id}', + method: 'DELETE', + config: { + pre: [prereqs.getSavedObjectsClient], + validate: { + params: Joi.object().keys({ + type: Joi.string().required(), + id: Joi.string().required(), + }).required() + }, + handler(request, reply) { + const { savedObjectsClient } = request.pre; + const { type, id } = request.params; + + reply(savedObjectsClient.delete(type, id)); + } + } +}); diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js new file mode 100644 index 0000000000000..0581303592cfa --- /dev/null +++ b/src/server/saved_objects/routes/find.js @@ -0,0 +1,31 @@ +import Joi from 'joi'; + +export const createFindRoute = (prereqs) => ({ + path: '/api/kibana/saved_objects/{type?}', + method: 'GET', + config: { + pre: [prereqs.getSavedObjectsClient], + validate: { + params: Joi.object().keys({ + type: Joi.string() + }), + query: Joi.object().keys({ + per_page: Joi.number().min(1).default(20), + page: Joi.number().min(0).default(1), + type: Joi.string(), + search: Joi.string().allow('').optional(), + searchFields: [Joi.string(), Joi.array().items(Joi.string())], + fields: [Joi.string(), Joi.array().items(Joi.string())] + }) + }, + handler(request, reply) { + const options = Object.assign({}, request.query); + + if (request.params.type) { + options.type = request.params.type; + } + + reply(request.pre.savedObjectsClient.find(options)); + } + } +}); diff --git a/src/server/saved_objects/routes/index.js b/src/server/saved_objects/routes/index.js new file mode 100644 index 0000000000000..c80378e63e4be --- /dev/null +++ b/src/server/saved_objects/routes/index.js @@ -0,0 +1,5 @@ +export { createCreateRoute } from './create'; +export { createDeleteRoute } from './delete'; +export { createFindRoute } from './find'; +export { createReadRoute } from './read'; +export { createUpdateRoute } from './update'; diff --git a/src/server/saved_objects/routes/read.js b/src/server/saved_objects/routes/read.js new file mode 100644 index 0000000000000..31ef0428ec9ec --- /dev/null +++ b/src/server/saved_objects/routes/read.js @@ -0,0 +1,21 @@ +import Joi from 'joi'; + +export const createReadRoute = (prereqs) => ({ + path: '/api/kibana/saved_objects/{type}/{id}', + method: 'GET', + config: { + pre: [prereqs.getSavedObjectsClient], + validate: { + params: Joi.object().keys({ + type: Joi.string().required(), + id: Joi.string().required(), + }).required() + }, + handler(request, reply) { + const { savedObjectsClient } = request.pre; + const { type, id } = request.params; + + reply(savedObjectsClient.get(type, id)); + } + } +}); diff --git a/src/server/saved_objects/routes/update.js b/src/server/saved_objects/routes/update.js new file mode 100644 index 0000000000000..0f9fe7e669971 --- /dev/null +++ b/src/server/saved_objects/routes/update.js @@ -0,0 +1,24 @@ +import Joi from 'joi'; + +export const createUpdateRoute = (prereqs) => { + return { + path: '/api/kibana/saved_objects/{type}/{id}', + method: 'PUT', + config: { + pre: [prereqs.getSavedObjectsClient], + validate: { + params: Joi.object().keys({ + type: Joi.string().required(), + id: Joi.string().required(), + }).required(), + payload: Joi.object().required() + }, + handler(request, reply) { + const { savedObjectsClient } = request.pre; + const { type, id } = request.params; + + reply(savedObjectsClient.update(type, id, request.payload)); + } + } + }; +}; diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js new file mode 100644 index 0000000000000..151463493cb77 --- /dev/null +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -0,0 +1,30 @@ +import { SavedObjectsClient } from './client'; + +import { + createCreateRoute, + createDeleteRoute, + createFindRoute, + createReadRoute, + createUpdateRoute +} from './routes'; + +export function savedObjectsMixin(kbnServer, server) { + const prereqs = { + getSavedObjectsClient: { + assign: 'savedObjectsClient', + method(request, reply) { + reply(new SavedObjectsClient( + server.config().get('kibana.index'), + request, + server.plugins.elasticsearch.getCluster('admin').callWithRequest + )); + } + }, + }; + + server.route(createCreateRoute(prereqs)); + server.route(createDeleteRoute(prereqs)); + server.route(createFindRoute(prereqs)); + server.route(createReadRoute(prereqs)); + server.route(createUpdateRoute(prereqs)); +} diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js new file mode 100644 index 0000000000000..65033e4ef544f --- /dev/null +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -0,0 +1,324 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; +import { pick } from 'lodash'; +import { SavedObjectsClient } from '../saved_objects_client'; +import { SavedObject } from '../saved_object'; + + +describe('SavedObjectsClient', () => { + const basePath = Math.random().toString(36).substring(7); + const sandbox = sinon.sandbox.create(); + + let savedObjectsClient; + + beforeEach(() => { + savedObjectsClient = new SavedObjectsClient(sinon.stub, basePath); + sandbox.stub(savedObjectsClient, '_$http'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#_getUrl', () => { + it('returns without arguments', () => { + const url = savedObjectsClient._getUrl(); + const expected = `${basePath}/api/kibana/saved_objects/`; + + expect(url).to.be(expected); + }); + + it('appends path', () => { + const url = savedObjectsClient._getUrl(['some', 'path']); + const expected = `${basePath}/api/kibana/saved_objects/some/path`; + + expect(url).to.be(expected); + }); + + it('appends query', () => { + const url = savedObjectsClient._getUrl(['some', 'path'], { foo: 'Foo', bar: 'Bar' }); + const expected = `${basePath}/api/kibana/saved_objects/some/path?foo=Foo&bar=Bar`; + + expect(url).to.be(expected); + }); + }); + + describe('#_request', () => { + const params = { foo: 'Foo', bar: 'Bar' }; + + it('passes options to $http', () => { + savedObjectsClient._$http.withArgs({ + method: 'POST', + url: '/api/path', + data: params + }).returns(Promise.resolve({ data: '' })); + + savedObjectsClient._request('POST', '/api/path', params); + + expect(savedObjectsClient._$http.calledOnce).to.be(true); + }); + + it('sets params for GET request', () => { + savedObjectsClient._$http.withArgs({ + method: 'GET', + url: '/api/path', + params: params + }).returns(Promise.resolve({ data: '' })); + + savedObjectsClient._request('GET', '/api/path', params); + + expect(savedObjectsClient._$http.calledOnce).to.be(true); + }); + + it('catches API error', (done) => { + const message = 'Request failed'; + + savedObjectsClient._$http.returns(Promise.reject({ data: { error: message } })); + savedObjectsClient._request('GET', '/api/path', params).then(() => { + done('should have thrown'); + }).catch(e => { + expect(e.message).to.eql(message); + done(); + }); + }); + + it('catches API error message', (done) => { + const message = 'Request failed'; + + savedObjectsClient._$http.returns(Promise.reject({ data: { message: message } })); + savedObjectsClient._request('GET', '/api/path', params).then(() => { + done('should have thrown'); + }).catch(e => { + expect(e.message).to.eql(message); + done(); + }); + }); + + it('catches API error status', (done) => { + savedObjectsClient._$http.returns(Promise.reject({ status: 404 })); + savedObjectsClient._request('GET', '/api/path', params).then(() => { + done('should have thrown'); + }).catch(e => { + expect(e.message).to.eql('404 Response'); + done(); + }); + }); + }); + + describe('#get', () => { + const attributes = { type: 'index-pattern', foo: 'Foo' }; + + beforeEach(() => { + savedObjectsClient._$http.withArgs({ + method: 'GET', + url: `${basePath}/api/kibana/saved_objects/index-pattern/logstash-*` + }).returns(Promise.resolve(attributes)); + }); + + it('returns a promise', () => { + expect(savedObjectsClient.get('index-pattern', 'logstash-*')).to.be.a(Promise); + }); + + it('requires type', (done) => { + savedObjectsClient.get().then(() => { + done('should require type'); + }).catch((e) => { + expect(e.message).to.contain('requires type and id'); + done(); + }); + }); + + it('requires id', (done) => { + savedObjectsClient.get('index-pattern').then(() => { + done('should require id'); + }).catch((e) => { + expect(e.message).to.contain('requires type and id'); + done(); + }); + }); + + it('resolves with instantiated ObjectClass', async () => { + const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); + expect(response).to.be.a(SavedObject); + expect(response.attributes).to.eql(attributes); + expect(response.client).to.be.a(SavedObjectsClient); + }); + + it('makes HTTP call', () => { + savedObjectsClient.get('index-pattern', 'logstash-*'); + sinon.assert.calledOnce(savedObjectsClient._$http); + }); + }); + + describe('#delete', () => { + beforeEach(() => { + savedObjectsClient._$http.withArgs({ + method: 'DELETE', + url: `${basePath}/api/kibana/saved_objects/index-pattern/logstash-*` + }).returns(Promise.resolve({ data: 'api-response' })); + }); + + it('returns a promise', () => { + expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).to.be.a(Promise); + }); + + it('requires type', (done) => { + savedObjectsClient.delete().then(() => { + done('should require type'); + }).catch((e) => { + expect(e.message).to.contain('requires type and id'); + done(); + }); + }); + + it('requires id', (done) => { + savedObjectsClient.delete('index-pattern').then(() => { + done('should require id'); + }).catch((e) => { + expect(e.message).to.contain('requires type and id'); + done(); + }); + }); + + it('makes HTTP call', () => { + savedObjectsClient.delete('index-pattern', 'logstash-*'); + sinon.assert.calledOnce(savedObjectsClient._$http); + }); + }); + + describe('#update', () => { + const requireMessage = 'requires type, id and body'; + + beforeEach(() => { + savedObjectsClient._$http.withArgs({ + method: 'PUT', + url: `${basePath}/api/kibana/saved_objects/index-pattern/logstash-*`, + data: sinon.match.any + }).returns(Promise.resolve({ data: 'api-response' })); + }); + + it('returns a promise', () => { + expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).to.be.a(Promise); + }); + + it('requires type', (done) => { + savedObjectsClient.update().then(() => { + done('should require type'); + }).catch((e) => { + expect(e.message).to.contain(requireMessage); + done(); + }); + }); + + it('requires id', (done) => { + savedObjectsClient.update('index-pattern').then(() => { + done('should require id'); + }).catch((e) => { + expect(e.message).to.contain(requireMessage); + done(); + }); + }); + + it('requires body', (done) => { + savedObjectsClient.update('index-pattern', 'logstash-*').then(() => { + done('should require body'); + }).catch((e) => { + expect(e.message).to.contain(requireMessage); + done(); + }); + }); + + it('makes HTTP call', () => { + const body = { foo: 'Foo', bar: 'Bar' }; + + savedObjectsClient.update('index-pattern', 'logstash-*', body); + sinon.assert.calledOnce(savedObjectsClient._$http); + + expect(savedObjectsClient._$http.getCall(0).args[0].data).to.eql(body); + }); + }); + + describe('#create', () => { + const requireMessage = 'requires type and body'; + + beforeEach(() => { + savedObjectsClient._$http.withArgs({ + method: 'POST', + url: `${basePath}/api/kibana/saved_objects/index-pattern`, + data: sinon.match.any + }).returns(Promise.resolve({ data: 'api-response' })); + }); + + it('returns a promise', () => { + expect(savedObjectsClient.create('index-pattern', {})).to.be.a(Promise); + }); + + it('requires type', (done) => { + savedObjectsClient.create().then(() => { + done('should require type'); + }).catch((e) => { + expect(e.message).to.contain(requireMessage); + done(); + }); + }); + + it('requires body', (done) => { + savedObjectsClient.create('index-pattern').then(() => { + done('should require body'); + }).catch((e) => { + expect(e.message).to.contain(requireMessage); + done(); + }); + }); + + it('makes HTTP call', () => { + const body = { foo: 'Foo', bar: 'Bar', id: 'logstash-*' }; + savedObjectsClient.create('index-pattern', body); + + sinon.assert.calledOnce(savedObjectsClient._$http); + expect(savedObjectsClient._$http.getCall(0).args[0].data).to.eql(body); + }); + }); + + describe('#find', () => { + const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; + + beforeEach(() => { + savedObjectsClient._$http.returns(Promise.resolve({ data: [object] })); + }); + + it('returns a promise', () => { + expect(savedObjectsClient.find()).to.be.a(Promise); + }); + + it('accepts type', () => { + const body = { type: 'index-pattern', invalid: true }; + + savedObjectsClient.find(body); + expect(savedObjectsClient._$http.calledOnce).to.be(true); + + const options = savedObjectsClient._$http.getCall(0).args[0]; + expect(options.url).to.eql(`${basePath}/api/kibana/saved_objects/index-pattern`); + }); + + it('accepts fields', () => { + const body = { fields: ['title', 'description'], invalid: true }; + + savedObjectsClient.find(body); + expect(savedObjectsClient._$http.calledOnce).to.be(true); + + const options = savedObjectsClient._$http.getCall(0).args[0]; + expect(options.params).to.eql(pick(body, ['fields'])); + }); + + it('accepts from/size', () => { + const body = { from: 50, size: 10, invalid: true }; + + savedObjectsClient.find(body); + expect(savedObjectsClient._$http.calledOnce).to.be(true); + + const options = savedObjectsClient._$http.getCall(0).args[0]; + expect(options.params).to.eql(pick(body, ['from', 'size'])); + }); + }); +}); diff --git a/src/ui/public/saved_objects/index.js b/src/ui/public/saved_objects/index.js new file mode 100644 index 0000000000000..7b1d90d8452b0 --- /dev/null +++ b/src/ui/public/saved_objects/index.js @@ -0,0 +1,4 @@ +export { SavedObjectsClient } from './saved_objects_client'; +export { SavedObjectRegistryProvider } from './saved_object_registry'; +export { SavedObjectsClientProvider } from './saved_objects_client_provider'; +export { SavedObject } from './saved_object'; diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js new file mode 100644 index 0000000000000..e6ec6e8bda596 --- /dev/null +++ b/src/ui/public/saved_objects/saved_object.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; + +export class SavedObject { + constructor(client, attributes) { + this.client = client; + this.attributes = attributes; + } + + get(key) { + return _.get(this.attributes, key); + } + + set(key, value) { + return _.set(this.attributes, key, value); + } + + save() { + if (this.id) { + return this.client.update(this.type, this.id, this.attributes); + } else { + return this.client.create(this.type, this.attributes); + } + } + + delete() { + return this.cient.delete(this.type, this.id); + } + + get id() { + return this.get('id'); + } + + get type() { + return this.get('type'); + } +} diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js new file mode 100644 index 0000000000000..060b37e799ce2 --- /dev/null +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -0,0 +1,95 @@ +import { resolve as resolveUrl, format as formatUrl } from 'url'; +import { pick, partial } from 'lodash'; + +import { SavedObject } from './saved_object'; + +const join = (...uriComponents) => ( + uriComponents.filter(Boolean).map(encodeURIComponent).join('/') +); + +export class SavedObjectsClient { + constructor($http, basePath) { + this._$http = $http; + this._apiBaseUrl = `${basePath}/api/kibana/saved_objects/`; + } + + get(type, id) { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return this._request('GET', this._getUrl([type, id])).then(resp => { + return new this.ObjectClass(resp); + }); + } + + delete(type, id) { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return this._request('DELETE', this._getUrl([type, id])); + } + + update(type, id, body) { + if (!type || !id || !body) { + return Promise.reject(new Error('requires type, id and body')); + } + + return this._request('PUT', this._getUrl([type, id]), body); + } + + create(type, body) { + if (!type || !body) { + return Promise.reject(new Error('requires type and body')); + } + + const url = this._getUrl([type]); + + return this._request('POST', url, body); + } + + find(options = {}) { + const url = this._getUrl([options.type]); + const validOptions = pick(options, ['from', 'size', 'fields', 'filter']); + + return this._request('GET', url, validOptions).then(resp => { + resp.data = resp.data.map(d => new this.ObjectClass(d)); + return resp; + }); + } + + get ObjectClass() { + return partial(SavedObject, this); + } + + _getUrl(path, query) { + if (!path && !query) { + return this._apiBaseUrl; + } + + return resolveUrl(this._apiBaseUrl, formatUrl({ + pathname: join(...path), + query: pick(query, value => value != null) + })); + } + + _request(method, url, body) { + const options = { method, url }; + + if (method === 'GET' && body) { + options.params = body; + } else if (body) { + options.data = body; + } + + return this._$http(options) + .catch(resp => { + const respBody = resp.data || {}; + const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); + err.status = resp.status; + err.body = respBody; + throw err; + }); + } +} diff --git a/src/ui/public/saved_objects/saved_objects_client_provider.js b/src/ui/public/saved_objects/saved_objects_client_provider.js new file mode 100644 index 0000000000000..cb157bb480c97 --- /dev/null +++ b/src/ui/public/saved_objects/saved_objects_client_provider.js @@ -0,0 +1,7 @@ +import chrome from 'ui/chrome'; + +import { SavedObjectsClient } from './saved_objects_client'; + +export function SavedObjectsClientProvider($http) { + return new SavedObjectsClient($http, chrome.getBasePath()); +} From 621fd48f1170678cd4f030ec2f800415cce77739 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 15 May 2017 09:30:06 -0700 Subject: [PATCH 02/25] Fixes typo Signed-off-by: Tyler Smalley --- src/ui/public/saved_objects/saved_object.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js index e6ec6e8bda596..c48b5fb71a6e5 100644 --- a/src/ui/public/saved_objects/saved_object.js +++ b/src/ui/public/saved_objects/saved_object.js @@ -23,7 +23,7 @@ export class SavedObject { } delete() { - return this.cient.delete(this.type, this.id); + return this.client.delete(this.type, this.id); } get id() { From 197a5e4e39db700ecdd3e7d9b52293f64282d3be Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 15 May 2017 09:38:18 -0700 Subject: [PATCH 03/25] Remove kibana from saved object API path Signed-off-by: Tyler Smalley --- .../saved_objects/routes/__tests__/create.js | 6 +++--- .../saved_objects/routes/__tests__/delete.js | 6 +++--- .../saved_objects/routes/__tests__/find.js | 16 ++++++++-------- .../saved_objects/routes/__tests__/read.js | 6 +++--- .../saved_objects/routes/__tests__/update.js | 6 +++--- src/server/saved_objects/routes/create.js | 2 +- src/server/saved_objects/routes/delete.js | 2 +- src/server/saved_objects/routes/find.js | 2 +- src/server/saved_objects/routes/read.js | 2 +- src/server/saved_objects/routes/update.js | 2 +- .../__tests__/saved_objects_client.js | 16 ++++++++-------- .../public/saved_objects/saved_objects_client.js | 2 +- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/server/saved_objects/routes/__tests__/create.js b/src/server/saved_objects/routes/__tests__/create.js index 40bfd24a4fad8..356a636d1fcec 100644 --- a/src/server/saved_objects/routes/__tests__/create.js +++ b/src/server/saved_objects/routes/__tests__/create.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { createCreateRoute } from '../create'; import { MockServer } from './mock_server'; -describe('POST /api/kibana/saved_objects/{type}/{id?}', () => { +describe('POST /api/saved_objects/{type}/{id?}', () => { const savedObjectsClient = { create: sinon.stub() }; let server; @@ -29,7 +29,7 @@ describe('POST /api/kibana/saved_objects/{type}/{id?}', () => { it('formats successful response', async () => { const request = { method: 'POST', - url: '/api/kibana/saved_objects/index-pattern', + url: '/api/saved_objects/index-pattern', payload: { title: 'Testing' } @@ -52,7 +52,7 @@ describe('POST /api/kibana/saved_objects/{type}/{id?}', () => { it('calls upon savedObjectClient.create', async () => { const request = { method: 'POST', - url: '/api/kibana/saved_objects/index-pattern/logstash-*', + url: '/api/saved_objects/index-pattern/logstash-*', payload: { title: 'Testing' } diff --git a/src/server/saved_objects/routes/__tests__/delete.js b/src/server/saved_objects/routes/__tests__/delete.js index 64bc301ff926a..b020369210d73 100644 --- a/src/server/saved_objects/routes/__tests__/delete.js +++ b/src/server/saved_objects/routes/__tests__/delete.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { createDeleteRoute } from '../delete'; import { MockServer } from './mock_server'; -describe('DELETE /api/kibana/saved_objects/{type}/{id}', () => { +describe('DELETE /api/saved_objects/{type}/{id}', () => { const savedObjectsClient = { delete: sinon.stub() }; let server; @@ -29,7 +29,7 @@ describe('DELETE /api/kibana/saved_objects/{type}/{id}', () => { it('formats successful response', async () => { const request = { method: 'DELETE', - url: '/api/kibana/saved_objects/index-pattern/logstash-*' + url: '/api/saved_objects/index-pattern/logstash-*' }; const clientResponse = true; @@ -45,7 +45,7 @@ describe('DELETE /api/kibana/saved_objects/{type}/{id}', () => { it('calls upon savedObjectClient.delete', async () => { const request = { method: 'DELETE', - url: '/api/kibana/saved_objects/index-pattern/logstash-*' + url: '/api/saved_objects/index-pattern/logstash-*' }; await server.inject(request); diff --git a/src/server/saved_objects/routes/__tests__/find.js b/src/server/saved_objects/routes/__tests__/find.js index eecaecb4ce669..e93c5be51f78e 100644 --- a/src/server/saved_objects/routes/__tests__/find.js +++ b/src/server/saved_objects/routes/__tests__/find.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { createFindRoute } from '../find'; import { MockServer } from './mock_server'; -describe('GET /api/kibana/saved_objects/{type?}', () => { +describe('GET /api/saved_objects/{type?}', () => { const savedObjectsClient = { find: sinon.stub() }; let server; @@ -29,7 +29,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('formats successful response', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects' + url: '/api/saved_objects' }; const clientResponse = { @@ -63,7 +63,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('calls upon savedObjectClient.find with defaults', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects' + url: '/api/saved_objects' }; await server.inject(request); @@ -77,7 +77,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('accepts the query parameter page/per_page', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects?per_page=10&page=50' + url: '/api/saved_objects?per_page=10&page=50' }; await server.inject(request); @@ -91,7 +91,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('accepts the query parameter fields as a string', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects?fields=title' + url: '/api/saved_objects?fields=title' }; await server.inject(request); @@ -105,7 +105,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('accepts the query parameter fields as an array', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects?fields=title&fields=description' + url: '/api/saved_objects?fields=title&fields=description' }; await server.inject(request); @@ -121,7 +121,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('accepts the type as a query parameter', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects?type=index-pattern' + url: '/api/saved_objects?type=index-pattern' }; await server.inject(request); @@ -135,7 +135,7 @@ describe('GET /api/kibana/saved_objects/{type?}', () => { it('accepts the type as a URL parameter', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects/index-pattern' + url: '/api/saved_objects/index-pattern' }; await server.inject(request); diff --git a/src/server/saved_objects/routes/__tests__/read.js b/src/server/saved_objects/routes/__tests__/read.js index 574185942ce46..ea1205fdf345b 100644 --- a/src/server/saved_objects/routes/__tests__/read.js +++ b/src/server/saved_objects/routes/__tests__/read.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { createReadRoute } from '../read'; import { MockServer } from './mock_server'; -describe('GET /api/kibana/saved_objects/{type}/{id}', () => { +describe('GET /api/saved_objects/{type}/{id}', () => { const savedObjectsClient = { get: sinon.stub() }; let server; @@ -29,7 +29,7 @@ describe('GET /api/kibana/saved_objects/{type}/{id}', () => { it('formats successful response', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects/index-pattern/logstash-*' + url: '/api/saved_objects/index-pattern/logstash-*' }; const clientResponse = { id: 'logstash-*', @@ -50,7 +50,7 @@ describe('GET /api/kibana/saved_objects/{type}/{id}', () => { it('calls upon savedObjectClient.get', async () => { const request = { method: 'GET', - url: '/api/kibana/saved_objects/index-pattern/logstash-*' + url: '/api/saved_objects/index-pattern/logstash-*' }; await server.inject(request); diff --git a/src/server/saved_objects/routes/__tests__/update.js b/src/server/saved_objects/routes/__tests__/update.js index 20f0952dc4bcd..c55e90a7fc062 100644 --- a/src/server/saved_objects/routes/__tests__/update.js +++ b/src/server/saved_objects/routes/__tests__/update.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { createUpdateRoute } from '../update'; import { MockServer } from './mock_server'; -describe('PUT /api/kibana/saved_objects/{type}/{id?}', () => { +describe('PUT /api/saved_objects/{type}/{id?}', () => { const savedObjectsClient = { update: sinon.stub() }; let server; @@ -29,7 +29,7 @@ describe('PUT /api/kibana/saved_objects/{type}/{id?}', () => { it('formats successful response', async () => { const request = { method: 'PUT', - url: '/api/kibana/saved_objects/index-pattern/logstash-*', + url: '/api/saved_objects/index-pattern/logstash-*', payload: { title: 'Testing' } @@ -47,7 +47,7 @@ describe('PUT /api/kibana/saved_objects/{type}/{id?}', () => { it('calls upon savedObjectClient.update', async () => { const request = { method: 'PUT', - url: '/api/kibana/saved_objects/index-pattern/logstash-*', + url: '/api/saved_objects/index-pattern/logstash-*', payload: { title: 'Testing' } diff --git a/src/server/saved_objects/routes/create.js b/src/server/saved_objects/routes/create.js index 3c358045d8505..8e6436a4e549e 100644 --- a/src/server/saved_objects/routes/create.js +++ b/src/server/saved_objects/routes/create.js @@ -3,7 +3,7 @@ import { has } from 'lodash'; export const createCreateRoute = (prereqs) => { return { - path: '/api/kibana/saved_objects/{type}/{id?}', + path: '/api/saved_objects/{type}/{id?}', method: 'POST', config: { pre: [prereqs.getSavedObjectsClient], diff --git a/src/server/saved_objects/routes/delete.js b/src/server/saved_objects/routes/delete.js index a67cd5809cd44..a394530a0f655 100644 --- a/src/server/saved_objects/routes/delete.js +++ b/src/server/saved_objects/routes/delete.js @@ -1,7 +1,7 @@ import Joi from 'joi'; export const createDeleteRoute = (prereqs) => ({ - path: '/api/kibana/saved_objects/{type}/{id}', + path: '/api/saved_objects/{type}/{id}', method: 'DELETE', config: { pre: [prereqs.getSavedObjectsClient], diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js index 0581303592cfa..7c1991019ae9e 100644 --- a/src/server/saved_objects/routes/find.js +++ b/src/server/saved_objects/routes/find.js @@ -1,7 +1,7 @@ import Joi from 'joi'; export const createFindRoute = (prereqs) => ({ - path: '/api/kibana/saved_objects/{type?}', + path: '/api/saved_objects/{type?}', method: 'GET', config: { pre: [prereqs.getSavedObjectsClient], diff --git a/src/server/saved_objects/routes/read.js b/src/server/saved_objects/routes/read.js index 31ef0428ec9ec..f96b0cab51ad0 100644 --- a/src/server/saved_objects/routes/read.js +++ b/src/server/saved_objects/routes/read.js @@ -1,7 +1,7 @@ import Joi from 'joi'; export const createReadRoute = (prereqs) => ({ - path: '/api/kibana/saved_objects/{type}/{id}', + path: '/api/saved_objects/{type}/{id}', method: 'GET', config: { pre: [prereqs.getSavedObjectsClient], diff --git a/src/server/saved_objects/routes/update.js b/src/server/saved_objects/routes/update.js index 0f9fe7e669971..6443b5308edce 100644 --- a/src/server/saved_objects/routes/update.js +++ b/src/server/saved_objects/routes/update.js @@ -2,7 +2,7 @@ import Joi from 'joi'; export const createUpdateRoute = (prereqs) => { return { - path: '/api/kibana/saved_objects/{type}/{id}', + path: '/api/saved_objects/{type}/{id}', method: 'PUT', config: { pre: [prereqs.getSavedObjectsClient], diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index 65033e4ef544f..db2440c235242 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -23,21 +23,21 @@ describe('SavedObjectsClient', () => { describe('#_getUrl', () => { it('returns without arguments', () => { const url = savedObjectsClient._getUrl(); - const expected = `${basePath}/api/kibana/saved_objects/`; + const expected = `${basePath}/api/saved_objects/`; expect(url).to.be(expected); }); it('appends path', () => { const url = savedObjectsClient._getUrl(['some', 'path']); - const expected = `${basePath}/api/kibana/saved_objects/some/path`; + const expected = `${basePath}/api/saved_objects/some/path`; expect(url).to.be(expected); }); it('appends query', () => { const url = savedObjectsClient._getUrl(['some', 'path'], { foo: 'Foo', bar: 'Bar' }); - const expected = `${basePath}/api/kibana/saved_objects/some/path?foo=Foo&bar=Bar`; + const expected = `${basePath}/api/saved_objects/some/path?foo=Foo&bar=Bar`; expect(url).to.be(expected); }); @@ -111,7 +111,7 @@ describe('SavedObjectsClient', () => { beforeEach(() => { savedObjectsClient._$http.withArgs({ method: 'GET', - url: `${basePath}/api/kibana/saved_objects/index-pattern/logstash-*` + url: `${basePath}/api/saved_objects/index-pattern/logstash-*` }).returns(Promise.resolve(attributes)); }); @@ -154,7 +154,7 @@ describe('SavedObjectsClient', () => { beforeEach(() => { savedObjectsClient._$http.withArgs({ method: 'DELETE', - url: `${basePath}/api/kibana/saved_objects/index-pattern/logstash-*` + url: `${basePath}/api/saved_objects/index-pattern/logstash-*` }).returns(Promise.resolve({ data: 'api-response' })); }); @@ -192,7 +192,7 @@ describe('SavedObjectsClient', () => { beforeEach(() => { savedObjectsClient._$http.withArgs({ method: 'PUT', - url: `${basePath}/api/kibana/saved_objects/index-pattern/logstash-*`, + url: `${basePath}/api/saved_objects/index-pattern/logstash-*`, data: sinon.match.any }).returns(Promise.resolve({ data: 'api-response' })); }); @@ -244,7 +244,7 @@ describe('SavedObjectsClient', () => { beforeEach(() => { savedObjectsClient._$http.withArgs({ method: 'POST', - url: `${basePath}/api/kibana/saved_objects/index-pattern`, + url: `${basePath}/api/saved_objects/index-pattern`, data: sinon.match.any }).returns(Promise.resolve({ data: 'api-response' })); }); @@ -298,7 +298,7 @@ describe('SavedObjectsClient', () => { expect(savedObjectsClient._$http.calledOnce).to.be(true); const options = savedObjectsClient._$http.getCall(0).args[0]; - expect(options.url).to.eql(`${basePath}/api/kibana/saved_objects/index-pattern`); + expect(options.url).to.eql(`${basePath}/api/saved_objects/index-pattern`); }); it('accepts fields', () => { diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index 060b37e799ce2..773ff5c7634c7 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -10,7 +10,7 @@ const join = (...uriComponents) => ( export class SavedObjectsClient { constructor($http, basePath) { this._$http = $http; - this._apiBaseUrl = `${basePath}/api/kibana/saved_objects/`; + this._apiBaseUrl = `${basePath}/api/saved_objects/`; } get(type, id) { From 005b029bdd467482f93602f5d2bbddc6aae43ac4 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 15 May 2017 10:16:27 -0700 Subject: [PATCH 04/25] Nests document in doc element for ES Signed-off-by: Tyler Smalley --- src/server/saved_objects/client/saved_objects_client.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 2752967418266..c86ee47737115 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -39,7 +39,7 @@ export class SavedObjectsClient { throw Boom.notFound(); } - return get(response, 'deleted', false); + return get(response, 'result') === 'deleted'; } async find(options = {}) { @@ -82,8 +82,10 @@ export class SavedObjectsClient { const response = await this._withKibanaIndex('update', { type, id, - body, - refresh: true, + body: { + doc: body + }, + refresh: 'wait_for' }); return get(response, 'result') === 'updated'; From 8b0c32deef7f5dd29f6cdb3d1ab363590609b694 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 15 May 2017 10:22:46 -0700 Subject: [PATCH 05/25] Resolves tests for update API Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 4acc8be47fed7..544c51c9918c9 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -94,10 +94,10 @@ describe('SavedObjectsClient', () => { describe('#delete', () => { it('returns based on ES success', async () => { - callWithRequest.returns(Promise.resolve({ deleted: 'testing' })); + callWithRequest.returns(Promise.resolve({ result: 'deleted' })); const response = await savedObjectsClient.delete('index-pattern', 'logstash-*'); - expect(response).to.be('testing'); + expect(response).to.be(true); }); it('throws notFound when ES is unable to find the document', (done) => { @@ -251,8 +251,8 @@ describe('SavedObjectsClient', () => { expect(args[2]).to.eql({ type: 'index-pattern', id: 'logstash-*', - body: { title: 'Testing' }, - refresh: true, + body: { doc: { title: 'Testing' } }, + refresh: 'wait_for', index: '.kibana-test' }); }); From f6dd70520170127d91170251e4fc6b259df866aa Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 15 May 2017 12:45:03 -0700 Subject: [PATCH 06/25] Prevent leaking of ES query to API Signed-off-by: Tyler Smalley --- src/server/saved_objects/client/lib/handle_es_error.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/saved_objects/client/lib/handle_es_error.js b/src/server/saved_objects/client/lib/handle_es_error.js index 0bea7eac7922d..877c1a6804bf9 100644 --- a/src/server/saved_objects/client/lib/handle_es_error.js +++ b/src/server/saved_objects/client/lib/handle_es_error.js @@ -23,7 +23,7 @@ export function handleEsError(error) { error instanceof NoConnections || error instanceof RequestTimeout ) { - throw Boom.serverTimeout(error); + throw Boom.serverTimeout(); } if (error instanceof Conflict || error.message.includes('index_template_already_exists')) { @@ -31,15 +31,15 @@ export function handleEsError(error) { } if (error instanceof Forbidden) { - throw Boom.forbidden(error); + throw Boom.forbidden(); } if (error instanceof NotFound) { - throw Boom.notFound(error); + throw Boom.notFound(); } if (error instanceof BadRequest) { - throw Boom.badRequest(error); + throw Boom.badRequest(); } throw error; From af59a5c3d5de3c7c5189ccb5ebd7658af47ce1f4 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 15 May 2017 23:06:42 -0700 Subject: [PATCH 07/25] Adds version to saved objects API Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 15 ++++++++++----- .../client/lib/__tests__/create_filter_path.js | 4 +++- .../client/lib/create_filter_path.js | 2 +- .../saved_objects/client/lib/handle_es_error.js | 13 ++++++++----- .../saved_objects/client/saved_objects_client.js | 14 +++++++++++--- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 544c51c9918c9..263a855b0ca83 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -53,7 +53,7 @@ describe('SavedObjectsClient', () => { describe('#create', () => { it('formats Elasticsearch response', async () => { - callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*' }); + callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); const response = await savedObjectsClient.create('index-pattern', { id: 'logstash-*', @@ -68,7 +68,7 @@ describe('SavedObjectsClient', () => { }); it('should use ES create action with specifying an id', async () => { - callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*' }); + callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); await savedObjectsClient.create('index-pattern', { id: 'logstash-*', @@ -138,7 +138,7 @@ describe('SavedObjectsClient', () => { expect(response.data).to.have.length(count); docs.hits.hits.forEach((doc, i) => { expect(response.data[i]).to.eql(Object.assign( - { id: doc._id, type: doc._type }, + { id: doc._id, type: doc._type, version: doc._version }, doc._source) ); }); @@ -190,6 +190,7 @@ describe('SavedObjectsClient', () => { 'hits.total', 'hits.hits._id', 'hits.hits._type', + 'hits.hits._version', 'hits.hits._source.title' ]); }); @@ -205,7 +206,8 @@ describe('SavedObjectsClient', () => { 'hits.hits._source.description', 'hits.total', 'hits.hits._id', - 'hits.hits._type' + 'hits.hits._type', + 'hits.hits._version' ]); }); }); @@ -215,6 +217,7 @@ describe('SavedObjectsClient', () => { callWithRequest.returns(Promise.resolve({ _id: 'logstash-*', _type: 'index-pattern', + _version: 2, _source: { title: 'Testing' } @@ -224,7 +227,8 @@ describe('SavedObjectsClient', () => { expect(response).to.eql({ id: 'logstash-*', type: 'index-pattern', - title: 'Testing' + title: 'Testing', + version: 2 }); }); }); @@ -251,6 +255,7 @@ describe('SavedObjectsClient', () => { expect(args[2]).to.eql({ type: 'index-pattern', id: 'logstash-*', + version: undefined, body: { doc: { title: 'Testing' } }, refresh: 'wait_for', index: '.kibana-test' diff --git a/src/server/saved_objects/client/lib/__tests__/create_filter_path.js b/src/server/saved_objects/client/lib/__tests__/create_filter_path.js index 391ade7c9e2b8..e115b242a28ea 100644 --- a/src/server/saved_objects/client/lib/__tests__/create_filter_path.js +++ b/src/server/saved_objects/client/lib/__tests__/create_filter_path.js @@ -8,6 +8,7 @@ describe('createFilterPath', () => { 'hits.total', 'hits.hits._id', 'hits.hits._type', + 'hits.hits._version', 'hits.hits._source.foo' ]); }); @@ -19,7 +20,8 @@ describe('createFilterPath', () => { 'hits.hits._source.bar', 'hits.total', 'hits.hits._id', - 'hits.hits._type' + 'hits.hits._type', + 'hits.hits._version', ]); }); }); diff --git a/src/server/saved_objects/client/lib/create_filter_path.js b/src/server/saved_objects/client/lib/create_filter_path.js index 39c33b5717460..2ffc1842ddbaa 100644 --- a/src/server/saved_objects/client/lib/create_filter_path.js +++ b/src/server/saved_objects/client/lib/create_filter_path.js @@ -1,5 +1,5 @@ export function createFilterPath(fields) { - const baseKeys = ['hits.total', 'hits.hits._id', 'hits.hits._type']; + const baseKeys = ['hits.total', 'hits.hits._id', 'hits.hits._type', 'hits.hits._version']; if (Array.isArray(fields)) { return fields.map(f => `hits.hits._source.${f}`).concat(baseKeys); diff --git a/src/server/saved_objects/client/lib/handle_es_error.js b/src/server/saved_objects/client/lib/handle_es_error.js index 877c1a6804bf9..a6a1b695a79db 100644 --- a/src/server/saved_objects/client/lib/handle_es_error.js +++ b/src/server/saved_objects/client/lib/handle_es_error.js @@ -1,5 +1,6 @@ import elasticsearch from 'elasticsearch'; import Boom from 'boom'; +import { get } from 'lodash'; const { ConnectionFault, @@ -17,6 +18,8 @@ export function handleEsError(error) { throw new Error('Expected an instance of Error'); } + const reason = get(error, 'body.error.reason'); + if ( error instanceof ConnectionFault || error instanceof ServiceUnavailable || @@ -26,20 +29,20 @@ export function handleEsError(error) { throw Boom.serverTimeout(); } - if (error instanceof Conflict || error.message.includes('index_template_already_exists')) { - throw Boom.conflict(error); + if (error instanceof Conflict) { + throw Boom.conflict(reason); } if (error instanceof Forbidden) { - throw Boom.forbidden(); + throw Boom.forbidden(reason); } if (error instanceof NotFound) { - throw Boom.notFound(); + throw Boom.notFound(reason); } if (error instanceof BadRequest) { - throw Boom.badRequest(); + throw Boom.badRequest(reason); } throw error; diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index c86ee47737115..57192117b93d5 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -60,7 +60,7 @@ export class SavedObjectsClient { return { data: get(response, 'hits.hits', []).map(r => { - return Object.assign({ id: r._id, type: r._type }, r._source); + return Object.assign({ id: r._id, type: r._type, version: r._version }, r._source); }), total: get(response, 'hits.total', 0), per_page: perPage, @@ -75,15 +75,23 @@ export class SavedObjectsClient { id, }); - return Object.assign({ id: response._id, type: response._type }, response._source); + return Object.assign({ + id: response._id, + type: response._type, + version: response._version + }, response._source); } async update(type, id, body) { + const version = get(body, 'version'); + const doc = omit(body, ['version']); + const response = await this._withKibanaIndex('update', { type, id, + version, body: { - doc: body + doc: doc }, refresh: 'wait_for' }); From a8e3279f159ce8ffac09b9ae7e6091b2e8df5043 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 09:21:03 -0700 Subject: [PATCH 08/25] Return version for searches Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 4 ++-- .../client/lib/__tests__/create_find_query.js | 14 +++++++++----- .../saved_objects/client/lib/create_find_query.js | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 263a855b0ca83..7cb72005cb31b 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -152,7 +152,7 @@ describe('SavedObjectsClient', () => { const options = callWithRequest.getCall(0).args[2]; expect(options).to.eql({ index: '.kibana-test', - body: { query: { match_all: {} } }, + body: { query: { match_all: {} }, version: true }, from: 50, size: 10 }); @@ -175,7 +175,7 @@ describe('SavedObjectsClient', () => { from: 0, index: '.kibana-test', size: 20, - body: { query: expectedQuery }, + body: { query: expectedQuery, version: true }, type: 'index-pattern', }); }); diff --git a/src/server/saved_objects/client/lib/__tests__/create_find_query.js b/src/server/saved_objects/client/lib/__tests__/create_find_query.js index 5e6dd84451093..e682c5ddbda6f 100644 --- a/src/server/saved_objects/client/lib/__tests__/create_find_query.js +++ b/src/server/saved_objects/client/lib/__tests__/create_find_query.js @@ -4,7 +4,7 @@ import { createFindQuery } from '../create_find_query'; describe('createFindQuery', () => { it('matches all when there is no type or filter', () => { const query = createFindQuery(); - expect(query).to.eql({ query: { match_all: {} } }); + expect(query).to.eql({ query: { match_all: {} }, version: true }); }); it('adds bool filter for type', () => { @@ -21,7 +21,8 @@ describe('createFindQuery', () => { match_all: {} }] } - } + }, + version: true }); }); @@ -38,7 +39,8 @@ describe('createFindQuery', () => { } }] } - } + }, + version: true }); }); @@ -55,7 +57,8 @@ describe('createFindQuery', () => { } }] } - } + }, + version: true }); }); @@ -72,7 +75,8 @@ describe('createFindQuery', () => { } }] } - } + }, + version: true }); }); }); diff --git a/src/server/saved_objects/client/lib/create_find_query.js b/src/server/saved_objects/client/lib/create_find_query.js index 67ee22a0ef36f..fc5993e21b8a5 100644 --- a/src/server/saved_objects/client/lib/create_find_query.js +++ b/src/server/saved_objects/client/lib/create_find_query.js @@ -2,7 +2,7 @@ export function createFindQuery(options = {}) { const { type, search, searchFields } = options; if (!type && !search) { - return { query: { match_all: {} } }; + return { version: true, query: { match_all: {} } }; } const bool = { must: [], filter: [] }; @@ -35,5 +35,5 @@ export function createFindQuery(options = {}) { }); } - return { query: { bool } }; + return { version: true, query: { bool } }; } From 915c7dfa6143b6ce2a57a86bb690f1b6908bcfee Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 10:50:53 -0700 Subject: [PATCH 09/25] Removes ability to specify id on object creation Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 12 +----------- .../saved_objects/client/saved_objects_client.js | 5 +---- src/server/saved_objects/routes/__tests__/create.js | 6 +++--- src/server/saved_objects/routes/create.js | 13 +++---------- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 7cb72005cb31b..d6906d5176448 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -67,7 +67,7 @@ describe('SavedObjectsClient', () => { }); }); - it('should use ES create action with specifying an id', async () => { + it('should use ES create action', async () => { callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); await savedObjectsClient.create('index-pattern', { @@ -80,16 +80,6 @@ describe('SavedObjectsClient', () => { const args = callWithRequest.getCall(0).args; expect(args[1]).to.be('create'); }); - - it('should use ES index action with specifying an id', async () => { - callWithRequest.returns({ _type: 'index-pattern', _id: 'abc123' }); - - await savedObjectsClient.create('index-pattern', { title: 'Logstash' }); - expect(callWithRequest.calledOnce).to.be(true); - - const args = callWithRequest.getCall(0).args; - expect(args[1]).to.be('index'); - }); }); describe('#delete', () => { diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 57192117b93d5..ead6eeee61217 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -16,12 +16,9 @@ export class SavedObjectsClient { async create(type, options = {}) { const body = omit(options, 'id'); - const id = get(options, 'id'); - const method = id ? 'create' : 'index'; - const response = await this._withKibanaIndex(method, { + const response = await this._withKibanaIndex('create', { type, - id, body }); diff --git a/src/server/saved_objects/routes/__tests__/create.js b/src/server/saved_objects/routes/__tests__/create.js index 356a636d1fcec..174e787485209 100644 --- a/src/server/saved_objects/routes/__tests__/create.js +++ b/src/server/saved_objects/routes/__tests__/create.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { createCreateRoute } from '../create'; import { MockServer } from './mock_server'; -describe('POST /api/saved_objects/{type}/{id?}', () => { +describe('POST /api/saved_objects/{type}', () => { const savedObjectsClient = { create: sinon.stub() }; let server; @@ -52,7 +52,7 @@ describe('POST /api/saved_objects/{type}/{id?}', () => { it('calls upon savedObjectClient.create', async () => { const request = { method: 'POST', - url: '/api/saved_objects/index-pattern/logstash-*', + url: '/api/saved_objects/index-pattern', payload: { title: 'Testing' } @@ -62,6 +62,6 @@ describe('POST /api/saved_objects/{type}/{id?}', () => { expect(savedObjectsClient.create.calledOnce).to.be(true); const args = savedObjectsClient.create.getCall(0).args; - expect(args).to.eql(['index-pattern', { title: 'Testing', id: 'logstash-*' }]); + expect(args).to.eql(['index-pattern', { title: 'Testing' }]); }); }); diff --git a/src/server/saved_objects/routes/create.js b/src/server/saved_objects/routes/create.js index 8e6436a4e549e..17abf78e1ef31 100644 --- a/src/server/saved_objects/routes/create.js +++ b/src/server/saved_objects/routes/create.js @@ -1,29 +1,22 @@ import Joi from 'joi'; -import { has } from 'lodash'; export const createCreateRoute = (prereqs) => { return { - path: '/api/saved_objects/{type}/{id?}', + path: '/api/saved_objects/{type}', method: 'POST', config: { pre: [prereqs.getSavedObjectsClient], validate: { params: Joi.object().keys({ - type: Joi.string().required(), - id: Joi.string() + type: Joi.string().required() }).required(), payload: Joi.object().required() }, handler(request, reply) { const { savedObjectsClient } = request.pre; const { type } = request.params; - const body = Object.assign({}, request.payload); - if (has(request.params, 'id')) { - body.id = request.params.id; - } - - reply(savedObjectsClient.create(type, body)); + reply(savedObjectsClient.create(type, request.payload)); } } }; From 6c0cfb6151617b5a4d8f150020aa19afeb079b8e Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 12:40:10 -0700 Subject: [PATCH 10/25] Adds version to update response Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 6 ++++-- src/server/saved_objects/client/saved_objects_client.js | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index d6906d5176448..7d03a4c6b5d7b 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -63,7 +63,8 @@ describe('SavedObjectsClient', () => { expect(response).to.eql({ type: 'index-pattern', id: 'logstash-*', - title: 'Logstash' + title: 'Logstash', + version: 2 }); }); @@ -228,11 +229,12 @@ describe('SavedObjectsClient', () => { callWithRequest.returns(Promise.resolve({ _id: 'logstash-*', _type: 'index-pattern', + _version: 2, result: 'updated' })); const response = await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' }); - expect(response).to.be(true); + expect(response).to.eql({ version: 2 }); }); it('passes the parameters to callWithRequest', async () => { diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index ead6eeee61217..eddfba1452579 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -22,7 +22,11 @@ export class SavedObjectsClient { body }); - return Object.assign({ type: response._type, id: response._id }, body); + return Object.assign({ + type: response._type, + id: response._id, + version: response._version + }, body); } async delete(type, id) { @@ -93,7 +97,7 @@ export class SavedObjectsClient { refresh: 'wait_for' }); - return get(response, 'result') === 'updated'; + return { version: get(response, '_version') }; } async _withKibanaIndex(method, params) { From 183516368ae5cbd9046dc2e87fee467da3fc24f9 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 12:40:35 -0700 Subject: [PATCH 11/25] Create uses ES index action Signed-off-by: Tyler Smalley --- .../saved_objects/client/__tests__/saved_objects_client.js | 2 +- src/server/saved_objects/client/saved_objects_client.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 7d03a4c6b5d7b..e937206c068d3 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -79,7 +79,7 @@ describe('SavedObjectsClient', () => { expect(callWithRequest.calledOnce).to.be(true); const args = callWithRequest.getCall(0).args; - expect(args[1]).to.be('create'); + expect(args[1]).to.be('index'); }); }); diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index eddfba1452579..0ec4e78bd81f2 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -17,7 +17,7 @@ export class SavedObjectsClient { async create(type, options = {}) { const body = omit(options, 'id'); - const response = await this._withKibanaIndex('create', { + const response = await this._withKibanaIndex('index', { type, body }); From a2d98772b2b7051d3eb9fa107dc2a1ff1c3a6dc7 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 13:02:47 -0700 Subject: [PATCH 12/25] Allow per_page of 0 for total Signed-off-by: Tyler Smalley --- src/server/saved_objects/routes/find.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js index 7c1991019ae9e..e601f9ab24a64 100644 --- a/src/server/saved_objects/routes/find.js +++ b/src/server/saved_objects/routes/find.js @@ -10,7 +10,7 @@ export const createFindRoute = (prereqs) => ({ type: Joi.string() }), query: Joi.object().keys({ - per_page: Joi.number().min(1).default(20), + per_page: Joi.number().min(0).default(20), page: Joi.number().min(0).default(1), type: Joi.string(), search: Joi.string().allow('').optional(), From cbbeff78a3ed83cb045f4ff5bd23faa8d85acfb3 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 14:41:03 -0700 Subject: [PATCH 13/25] Cleans up SavedObjectClient.find and uses camelCases Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 7 +++-- .../client/saved_objects_client.js | 28 +++++++++++-------- .../saved_objects/routes/__tests__/find.js | 12 ++++---- src/server/saved_objects/routes/find.js | 3 +- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index e937206c068d3..5937d4b5029ad 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -136,7 +136,7 @@ describe('SavedObjectsClient', () => { }); it('accepts per_page/page', async () => { - await savedObjectsClient.find({ per_page: 10, page: 6 }); + await savedObjectsClient.find({ perPage: 10, page: 6 }); expect(callWithRequest.calledOnce).to.be(true); @@ -144,8 +144,10 @@ describe('SavedObjectsClient', () => { expect(options).to.eql({ index: '.kibana-test', body: { query: { match_all: {} }, version: true }, + filterPath: undefined, from: 50, - size: 10 + size: 10, + type: undefined }); }); @@ -163,6 +165,7 @@ describe('SavedObjectsClient', () => { }; expect(options).to.eql({ + filterPath: undefined, from: 0, index: '.kibana-test', size: 20, diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 0ec4e78bd81f2..2ab01188bf3f4 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -1,5 +1,5 @@ import Boom from 'boom'; -import { get, omit, pick } from 'lodash'; +import { get, omit } from 'lodash'; import { createFindQuery, @@ -44,18 +44,22 @@ export class SavedObjectsClient { } async find(options = {}) { - const { search, searchFields, type, fields } = options; - const esOptions = pick(options, ['type']); - const perPage = get(options, 'per_page', 20); - const page = get(options, 'page', 1); - - if (fields) { - esOptions.filterPath = createFilterPath(fields); - } + const { + search, + searchFields, + type, + fields, + perPage = 20, + page = 1, + } = options; - esOptions.size = perPage; - esOptions.from = esOptions.size * (page - 1); - esOptions.body = createFindQuery({ search, searchFields, type }); + const esOptions = { + type, + filterPath: fields ? createFilterPath(fields) : undefined, + size: perPage, + from: perPage * (page - 1), + body: createFindQuery({ search, searchFields, type }) + }; const response = await this._withKibanaIndex('search', esOptions); diff --git a/src/server/saved_objects/routes/__tests__/find.js b/src/server/saved_objects/routes/__tests__/find.js index e93c5be51f78e..7a3d2aa0caacd 100644 --- a/src/server/saved_objects/routes/__tests__/find.js +++ b/src/server/saved_objects/routes/__tests__/find.js @@ -71,7 +71,7 @@ describe('GET /api/saved_objects/{type?}', () => { expect(savedObjectsClient.find.calledOnce).to.be(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).to.eql({ per_page: 20, page: 1 }); + expect(options).to.eql({ perPage: 20, page: 1 }); }); it('accepts the query parameter page/per_page', async () => { @@ -85,7 +85,7 @@ describe('GET /api/saved_objects/{type?}', () => { expect(savedObjectsClient.find.calledOnce).to.be(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).to.eql({ per_page: 10, page: 50 }); + expect(options).to.eql({ perPage: 10, page: 50 }); }); it('accepts the query parameter fields as a string', async () => { @@ -99,7 +99,7 @@ describe('GET /api/saved_objects/{type?}', () => { expect(savedObjectsClient.find.calledOnce).to.be(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).to.eql({ per_page: 20, page: 1, fields: 'title' }); + expect(options).to.eql({ perPage: 20, page: 1, fields: 'title' }); }); it('accepts the query parameter fields as an array', async () => { @@ -114,7 +114,7 @@ describe('GET /api/saved_objects/{type?}', () => { const options = savedObjectsClient.find.getCall(0).args[0]; expect(options).to.eql({ - per_page: 20, page: 1, fields: ['title', 'description'] + perPage: 20, page: 1, fields: ['title', 'description'] }); }); @@ -129,7 +129,7 @@ describe('GET /api/saved_objects/{type?}', () => { expect(savedObjectsClient.find.calledOnce).to.be(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).to.eql({ per_page: 20, page: 1, type: 'index-pattern' }); + expect(options).to.eql({ perPage: 20, page: 1, type: 'index-pattern' }); }); it('accepts the type as a URL parameter', async () => { @@ -143,6 +143,6 @@ describe('GET /api/saved_objects/{type?}', () => { expect(savedObjectsClient.find.calledOnce).to.be(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).to.eql({ per_page: 20, page: 1, type: 'index-pattern' }); + expect(options).to.eql({ perPage: 20, page: 1, type: 'index-pattern' }); }); }); diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js index e601f9ab24a64..d69c30dd182d3 100644 --- a/src/server/saved_objects/routes/find.js +++ b/src/server/saved_objects/routes/find.js @@ -1,4 +1,5 @@ import Joi from 'joi'; +import { keysToCamelCaseShallow } from '../../../utils/case_conversion'; export const createFindRoute = (prereqs) => ({ path: '/api/saved_objects/{type?}', @@ -19,7 +20,7 @@ export const createFindRoute = (prereqs) => ({ }) }, handler(request, reply) { - const options = Object.assign({}, request.query); + const options = keysToCamelCaseShallow(request.query); if (request.params.type) { options.type = request.params.type; From 986a73aa0068def36e958c425c9b223215b7b5ae Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 14:41:37 -0700 Subject: [PATCH 14/25] Underscores private variables Signed-off-by: Tyler Smalley --- src/ui/public/saved_objects/saved_object.js | 22 +++++++-------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js index c48b5fb71a6e5..4b027e7adea4c 100644 --- a/src/ui/public/saved_objects/saved_object.js +++ b/src/ui/public/saved_objects/saved_object.js @@ -2,35 +2,27 @@ import _ from 'lodash'; export class SavedObject { constructor(client, attributes) { - this.client = client; - this.attributes = attributes; + this._client = client; + this._attributes = attributes; } get(key) { - return _.get(this.attributes, key); + return _.get(this._attributes, key); } set(key, value) { - return _.set(this.attributes, key, value); + return _.set(this._attributes, key, value); } save() { if (this.id) { - return this.client.update(this.type, this.id, this.attributes); + return this._client.update(this.type, this.id, this._attributes); } else { - return this.client.create(this.type, this.attributes); + return this._client.create(this.type, this._attributes); } } delete() { - return this.client.delete(this.type, this.id); - } - - get id() { - return this.get('id'); - } - - get type() { - return this.get('type'); + return this._client.delete(this.type, this.id); } } From b916a7a64a7eb83c12bc5542c63b4c2f8a48b587 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 14:51:01 -0700 Subject: [PATCH 15/25] Removes request dependency on SavedObjectClient Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 64 +++++++++---------- .../client/saved_objects_client.js | 7 +- .../saved_objects/saved_objects_mixin.js | 8 ++- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 5937d4b5029ad..ec2add76cb2e0 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { SavedObjectsClient } from '../saved_objects_client'; describe('SavedObjectsClient', () => { - let callWithRequest; + let callAdminCluster; let savedObjectsClient; const docs = { hits: { @@ -42,18 +42,18 @@ describe('SavedObjectsClient', () => { }; beforeEach(() => { - callWithRequest = sinon.mock(); - savedObjectsClient = new SavedObjectsClient('.kibana-test', {}, callWithRequest); + callAdminCluster = sinon.mock(); + savedObjectsClient = new SavedObjectsClient('.kibana-test', callAdminCluster); }); afterEach(() => { - callWithRequest.reset(); + callAdminCluster.reset(); }); describe('#create', () => { it('formats Elasticsearch response', async () => { - callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); + callAdminCluster.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); const response = await savedObjectsClient.create('index-pattern', { id: 'logstash-*', @@ -69,30 +69,30 @@ describe('SavedObjectsClient', () => { }); it('should use ES create action', async () => { - callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); + callAdminCluster.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); await savedObjectsClient.create('index-pattern', { id: 'logstash-*', title: 'Logstash' }); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const args = callWithRequest.getCall(0).args; - expect(args[1]).to.be('index'); + const args = callAdminCluster.getCall(0).args; + expect(args[0]).to.be('index'); }); }); describe('#delete', () => { it('returns based on ES success', async () => { - callWithRequest.returns(Promise.resolve({ result: 'deleted' })); + callAdminCluster.returns(Promise.resolve({ result: 'deleted' })); const response = await savedObjectsClient.delete('index-pattern', 'logstash-*'); expect(response).to.be(true); }); it('throws notFound when ES is unable to find the document', (done) => { - callWithRequest.returns(Promise.resolve({ found: false })); + callAdminCluster.returns(Promise.resolve({ found: false })); savedObjectsClient.delete('index-pattern', 'logstash-*').then(() => { done('failed'); @@ -102,14 +102,14 @@ describe('SavedObjectsClient', () => { }); }); - it('passes the parameters to callWithRequest', async () => { + it('passes the parameters to callAdminCluster', async () => { await savedObjectsClient.delete('index-pattern', 'logstash-*'); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const args = callWithRequest.getCall(0).args; - expect(args[1]).to.be('delete'); - expect(args[2]).to.eql({ + const args = callAdminCluster.getCall(0).args; + expect(args[0]).to.be('delete'); + expect(args[1]).to.eql({ type: 'index-pattern', id: 'logstash-*', refresh: 'wait_for', @@ -122,7 +122,7 @@ describe('SavedObjectsClient', () => { it('formats Elasticsearch response', async () => { const count = docs.hits.hits.length; - callWithRequest.returns(Promise.resolve(docs)); + callAdminCluster.returns(Promise.resolve(docs)); const response = await savedObjectsClient.find(); expect(response.total).to.be(count); @@ -138,9 +138,9 @@ describe('SavedObjectsClient', () => { it('accepts per_page/page', async () => { await savedObjectsClient.find({ perPage: 10, page: 6 }); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const options = callWithRequest.getCall(0).args[2]; + const options = callAdminCluster.getCall(0).args[1]; expect(options).to.eql({ index: '.kibana-test', body: { query: { match_all: {} }, version: true }, @@ -154,9 +154,9 @@ describe('SavedObjectsClient', () => { it('accepts type', async () => { await savedObjectsClient.find({ type: 'index-pattern' }); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const options = callWithRequest.getCall(0).args[2]; + const options = callAdminCluster.getCall(0).args[1]; const expectedQuery = { bool: { must: [{ match_all: {} }], @@ -177,9 +177,9 @@ describe('SavedObjectsClient', () => { it('accepts fields as a string', async () => { await savedObjectsClient.find({ fields: 'title' }); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const options = callWithRequest.getCall(0).args[2]; + const options = callAdminCluster.getCall(0).args[1]; expect(options.filterPath).to.eql([ 'hits.total', 'hits.hits._id', @@ -192,9 +192,9 @@ describe('SavedObjectsClient', () => { it('accepts fields as an array', async () => { await savedObjectsClient.find({ fields: ['title', 'description'] }); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const options = callWithRequest.getCall(0).args[2]; + const options = callAdminCluster.getCall(0).args[1]; expect(options.filterPath).to.eql([ 'hits.hits._source.title', 'hits.hits._source.description', @@ -208,7 +208,7 @@ describe('SavedObjectsClient', () => { describe('#get', () => { it('formats Elasticsearch response', async () => { - callWithRequest.returns(Promise.resolve({ + callAdminCluster.returns(Promise.resolve({ _id: 'logstash-*', _type: 'index-pattern', _version: 2, @@ -229,7 +229,7 @@ describe('SavedObjectsClient', () => { describe('#update', () => { it('returns based on ES success', async () => { - callWithRequest.returns(Promise.resolve({ + callAdminCluster.returns(Promise.resolve({ _id: 'logstash-*', _type: 'index-pattern', _version: 2, @@ -240,14 +240,14 @@ describe('SavedObjectsClient', () => { expect(response).to.eql({ version: 2 }); }); - it('passes the parameters to callWithRequest', async () => { + it('passes the parameters to callAdminCluster', async () => { await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' }); - expect(callWithRequest.calledOnce).to.be(true); + expect(callAdminCluster.calledOnce).to.be(true); - const args = callWithRequest.getCall(0).args; - expect(args[1]).to.be('update'); - expect(args[2]).to.eql({ + const args = callAdminCluster.getCall(0).args; + expect(args[0]).to.be('update'); + expect(args[1]).to.eql({ type: 'index-pattern', id: 'logstash-*', version: undefined, diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 2ab01188bf3f4..fb2174f0c0704 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -8,10 +8,9 @@ import { } from './lib'; export class SavedObjectsClient { - constructor(kibanaIndex, request, callWithRequest) { + constructor(kibanaIndex, callAdminCluster) { this._kibanaIndex = kibanaIndex; - this._request = request; - this._callWithRequest = callWithRequest; + this._callAdminCluster = callAdminCluster; } async create(type, options = {}) { @@ -106,7 +105,7 @@ export class SavedObjectsClient { async _withKibanaIndex(method, params) { try { - return await this._callWithRequest(this._request, method, { + return await this._callAdminCluster(method, { ...params, index: this._kibanaIndex, }); diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index 151463493cb77..64046edc57bd3 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -12,11 +12,13 @@ export function savedObjectsMixin(kbnServer, server) { const prereqs = { getSavedObjectsClient: { assign: 'savedObjectsClient', - method(request, reply) { + method(req, reply) { + const adminCluster = req.server.plugins.elasticsearch.getCluster('admin'); + const callAdminCluster = (...args) => adminCluster.callWithRequest(req, ...args); + reply(new SavedObjectsClient( server.config().get('kibana.index'), - request, - server.plugins.elasticsearch.getCluster('admin').callWithRequest + callAdminCluster )); } }, From 5fa37cc8dcc050f70d8068109f7e2fd5e3f80a4f Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 May 2017 14:58:45 -0700 Subject: [PATCH 16/25] Allows for replacement of Promise library for Ng Signed-off-by: Tyler Smalley --- .../saved_objects/__tests__/saved_objects_client.js | 6 ++++-- src/ui/public/saved_objects/saved_objects_client.js | 11 ++++++----- .../saved_objects/saved_objects_client_provider.js | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index db2440c235242..a484eaad8e670 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -140,8 +140,10 @@ describe('SavedObjectsClient', () => { it('resolves with instantiated ObjectClass', async () => { const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); expect(response).to.be.a(SavedObject); - expect(response.attributes).to.eql(attributes); - expect(response.client).to.be.a(SavedObjectsClient); + expect(response.get('type')).to.eql('index-pattern'); + expect(response.get('foo')).to.eql('Foo'); + expect(response._attributes).to.eql(attributes); + expect(response._client).to.be.a(SavedObjectsClient); }); it('makes HTTP call', () => { diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index 773ff5c7634c7..d610507520c34 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -8,14 +8,15 @@ const join = (...uriComponents) => ( ); export class SavedObjectsClient { - constructor($http, basePath) { + constructor($http, basePath, PromiseCtor = Promise) { this._$http = $http; this._apiBaseUrl = `${basePath}/api/saved_objects/`; + this._PromiseCtor = PromiseCtor; } get(type, id) { if (!type || !id) { - return Promise.reject(new Error('requires type and id')); + return this._PromiseCtor.reject(new Error('requires type and id')); } return this._request('GET', this._getUrl([type, id])).then(resp => { @@ -25,7 +26,7 @@ export class SavedObjectsClient { delete(type, id) { if (!type || !id) { - return Promise.reject(new Error('requires type and id')); + return this._PromiseCtor.reject(new Error('requires type and id')); } return this._request('DELETE', this._getUrl([type, id])); @@ -33,7 +34,7 @@ export class SavedObjectsClient { update(type, id, body) { if (!type || !id || !body) { - return Promise.reject(new Error('requires type, id and body')); + return this._PromiseCtor.reject(new Error('requires type, id and body')); } return this._request('PUT', this._getUrl([type, id]), body); @@ -41,7 +42,7 @@ export class SavedObjectsClient { create(type, body) { if (!type || !body) { - return Promise.reject(new Error('requires type and body')); + return this._PromiseCtor.reject(new Error('requires type and body')); } const url = this._getUrl([type]); diff --git a/src/ui/public/saved_objects/saved_objects_client_provider.js b/src/ui/public/saved_objects/saved_objects_client_provider.js index cb157bb480c97..2d335e5b2353d 100644 --- a/src/ui/public/saved_objects/saved_objects_client_provider.js +++ b/src/ui/public/saved_objects/saved_objects_client_provider.js @@ -2,6 +2,6 @@ import chrome from 'ui/chrome'; import { SavedObjectsClient } from './saved_objects_client'; -export function SavedObjectsClientProvider($http) { - return new SavedObjectsClient($http, chrome.getBasePath()); +export function SavedObjectsClientProvider($http, $q) { + return new SavedObjectsClient($http, chrome.getBasePath(), $q); } From 280003d49160ca3b8161be15f2ec599af41bf561 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 17 May 2017 09:06:34 -0700 Subject: [PATCH 17/25] Use ES source filtering Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 30 +++---------------- .../lib/__tests__/create_filter_path.js | 27 ----------------- .../client/lib/create_filter_path.js | 9 ------ src/server/saved_objects/client/lib/index.js | 1 - .../client/saved_objects_client.js | 3 +- 5 files changed, 5 insertions(+), 65 deletions(-) delete mode 100644 src/server/saved_objects/client/lib/__tests__/create_filter_path.js delete mode 100644 src/server/saved_objects/client/lib/create_filter_path.js diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index ec2add76cb2e0..3f7adb50346e9 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -144,7 +144,7 @@ describe('SavedObjectsClient', () => { expect(options).to.eql({ index: '.kibana-test', body: { query: { match_all: {} }, version: true }, - filterPath: undefined, + _source: undefined, from: 50, size: 10, type: undefined @@ -165,7 +165,7 @@ describe('SavedObjectsClient', () => { }; expect(options).to.eql({ - filterPath: undefined, + _source: undefined, from: 0, index: '.kibana-test', size: 20, @@ -174,35 +174,13 @@ describe('SavedObjectsClient', () => { }); }); - it('accepts fields as a string', async () => { + it('can filter by fields', async () => { await savedObjectsClient.find({ fields: 'title' }); expect(callAdminCluster.calledOnce).to.be(true); const options = callAdminCluster.getCall(0).args[1]; - expect(options.filterPath).to.eql([ - 'hits.total', - 'hits.hits._id', - 'hits.hits._type', - 'hits.hits._version', - 'hits.hits._source.title' - ]); - }); - - it('accepts fields as an array', async () => { - await savedObjectsClient.find({ fields: ['title', 'description'] }); - - expect(callAdminCluster.calledOnce).to.be(true); - - const options = callAdminCluster.getCall(0).args[1]; - expect(options.filterPath).to.eql([ - 'hits.hits._source.title', - 'hits.hits._source.description', - 'hits.total', - 'hits.hits._id', - 'hits.hits._type', - 'hits.hits._version' - ]); + expect(options._source).to.eql('title'); }); }); diff --git a/src/server/saved_objects/client/lib/__tests__/create_filter_path.js b/src/server/saved_objects/client/lib/__tests__/create_filter_path.js deleted file mode 100644 index e115b242a28ea..0000000000000 --- a/src/server/saved_objects/client/lib/__tests__/create_filter_path.js +++ /dev/null @@ -1,27 +0,0 @@ -import expect from 'expect.js'; -import { createFilterPath } from '../create_filter_path'; - -describe('createFilterPath', () => { - it('handles a string', () => { - const fields = createFilterPath('foo'); - expect(fields).to.eql([ - 'hits.total', - 'hits.hits._id', - 'hits.hits._type', - 'hits.hits._version', - 'hits.hits._source.foo' - ]); - }); - - it('handles an array', () => { - const fields = createFilterPath(['foo', 'bar']); - expect(fields).to.eql([ - 'hits.hits._source.foo', - 'hits.hits._source.bar', - 'hits.total', - 'hits.hits._id', - 'hits.hits._type', - 'hits.hits._version', - ]); - }); -}); diff --git a/src/server/saved_objects/client/lib/create_filter_path.js b/src/server/saved_objects/client/lib/create_filter_path.js deleted file mode 100644 index 2ffc1842ddbaa..0000000000000 --- a/src/server/saved_objects/client/lib/create_filter_path.js +++ /dev/null @@ -1,9 +0,0 @@ -export function createFilterPath(fields) { - const baseKeys = ['hits.total', 'hits.hits._id', 'hits.hits._type', 'hits.hits._version']; - - if (Array.isArray(fields)) { - return fields.map(f => `hits.hits._source.${f}`).concat(baseKeys); - } else if (fields) { - return baseKeys.concat([`hits.hits._source.${fields}`]); - } -} diff --git a/src/server/saved_objects/client/lib/index.js b/src/server/saved_objects/client/lib/index.js index f7d3299bc0259..b0e3483b16b31 100644 --- a/src/server/saved_objects/client/lib/index.js +++ b/src/server/saved_objects/client/lib/index.js @@ -1,3 +1,2 @@ export { createFindQuery } from './create_find_query'; -export { createFilterPath } from './create_filter_path'; export { handleEsError } from './handle_es_error'; diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index fb2174f0c0704..b7c7856b61639 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -3,7 +3,6 @@ import { get, omit } from 'lodash'; import { createFindQuery, - createFilterPath, handleEsError, } from './lib'; @@ -54,7 +53,7 @@ export class SavedObjectsClient { const esOptions = { type, - filterPath: fields ? createFilterPath(fields) : undefined, + _source: fields, size: perPage, from: perPage * (page - 1), body: createFindQuery({ search, searchFields, type }) From 110628ebedbbea259e044c79e0f949ce00c6ba79 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 18 May 2017 09:54:21 -0700 Subject: [PATCH 18/25] Seperate attributes Signed-off-by: Tyler Smalley --- .../client/__tests__/saved_objects_client.js | 77 +++--- .../client/saved_objects_client.js | 43 ++-- .../saved_objects/routes/__tests__/create.js | 24 +- .../saved_objects/routes/__tests__/update.js | 11 +- src/server/saved_objects/routes/create.js | 7 +- src/server/saved_objects/routes/update.js | 9 +- .../saved_objects/__tests__/saved_object.js | 34 +++ .../__tests__/saved_objects_client.js | 239 +++++++++--------- src/ui/public/saved_objects/saved_object.js | 13 +- .../saved_objects/saved_objects_client.js | 16 +- 10 files changed, 291 insertions(+), 182 deletions(-) create mode 100644 src/ui/public/saved_objects/__tests__/saved_object.js diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 3f7adb50346e9..2112dbaf5a684 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -56,15 +56,16 @@ describe('SavedObjectsClient', () => { callAdminCluster.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); const response = await savedObjectsClient.create('index-pattern', { - id: 'logstash-*', title: 'Logstash' }); expect(response).to.eql({ type: 'index-pattern', id: 'logstash-*', - title: 'Logstash', - version: 2 + version: 2, + attributes: { + title: 'Logstash', + } }); }); @@ -128,10 +129,12 @@ describe('SavedObjectsClient', () => { expect(response.total).to.be(count); expect(response.data).to.have.length(count); docs.hits.hits.forEach((doc, i) => { - expect(response.data[i]).to.eql(Object.assign( - { id: doc._id, type: doc._type, version: doc._version }, - doc._source) - ); + expect(response.data[i]).to.eql({ + id: doc._id, + type: doc._type, + version: doc._version, + attributes: doc._source + }); }); }); @@ -141,14 +144,8 @@ describe('SavedObjectsClient', () => { expect(callAdminCluster.calledOnce).to.be(true); const options = callAdminCluster.getCall(0).args[1]; - expect(options).to.eql({ - index: '.kibana-test', - body: { query: { match_all: {} }, version: true }, - _source: undefined, - from: 50, - size: 10, - type: undefined - }); + expect(options.size).to.be(10); + expect(options.from).to.be(50); }); it('accepts type', async () => { @@ -164,13 +161,8 @@ describe('SavedObjectsClient', () => { } }; - expect(options).to.eql({ - _source: undefined, - from: 0, - index: '.kibana-test', - size: 20, - body: { query: expectedQuery, version: true }, - type: 'index-pattern', + expect(options.body).to.eql({ + query: expectedQuery, version: true }); }); @@ -199,23 +191,47 @@ describe('SavedObjectsClient', () => { expect(response).to.eql({ id: 'logstash-*', type: 'index-pattern', - title: 'Testing', - version: 2 + version: 2, + attributes: { + title: 'Testing' + } }); }); }); describe('#update', () => { - it('returns based on ES success', async () => { + it('returns current ES document version', async () => { + const id = 'logstash-*'; + const type = 'index-pattern'; + const version = 2; + const attributes = { title: 'Testing' }; + callAdminCluster.returns(Promise.resolve({ - _id: 'logstash-*', - _type: 'index-pattern', - _version: 2, + _id: id, + _type: type, + _version: version, result: 'updated' })); - const response = await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' }); - expect(response).to.eql({ version: 2 }); + const response = await savedObjectsClient.update('index-pattern', 'logstash-*', attributes); + expect(response).to.eql({ + id, + type, + version, + attributes + }); + }); + + it('accepts version', async () => { + await savedObjectsClient.update( + 'index-pattern', + 'logstash-*', + { title: 'Testing' }, + { version: 1 } + ); + + const esParams = callAdminCluster.getCall(0).args[1]; + expect(esParams.version).to.be(1); }); it('passes the parameters to callAdminCluster', async () => { @@ -224,6 +240,7 @@ describe('SavedObjectsClient', () => { expect(callAdminCluster.calledOnce).to.be(true); const args = callAdminCluster.getCall(0).args; + expect(args[0]).to.be('update'); expect(args[1]).to.eql({ type: 'index-pattern', diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index b7c7856b61639..2d40da1df99cb 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -1,5 +1,5 @@ import Boom from 'boom'; -import { get, omit } from 'lodash'; +import { get } from 'lodash'; import { createFindQuery, @@ -12,19 +12,18 @@ export class SavedObjectsClient { this._callAdminCluster = callAdminCluster; } - async create(type, options = {}) { - const body = omit(options, 'id'); - + async create(type, body = {}) { const response = await this._withKibanaIndex('index', { type, body }); - return Object.assign({ + return { type: response._type, id: response._id, - version: response._version - }, body); + version: response._version, + attributes: body + }; } async delete(type, id) { @@ -63,7 +62,12 @@ export class SavedObjectsClient { return { data: get(response, 'hits.hits', []).map(r => { - return Object.assign({ id: r._id, type: r._type, version: r._version }, r._source); + return { + id: r._id, + type: r._type, + version: r._version, + attributes: r._source + }; }), total: get(response, 'hits.total', 0), per_page: perPage, @@ -78,28 +82,31 @@ export class SavedObjectsClient { id, }); - return Object.assign({ + return { id: response._id, type: response._type, - version: response._version - }, response._source); + version: response._version, + attributes: response._source + }; } - async update(type, id, body) { - const version = get(body, 'version'); - const doc = omit(body, ['version']); - + async update(type, id, attributes, options = {}) { const response = await this._withKibanaIndex('update', { type, id, - version, + version: get(options, 'version'), body: { - doc: doc + doc: attributes }, refresh: 'wait_for' }); - return { version: get(response, '_version') }; + return { + id: id, + type: type, + version: get(response, '_version'), + attributes: attributes + }; } async _withKibanaIndex(method, params) { diff --git a/src/server/saved_objects/routes/__tests__/create.js b/src/server/saved_objects/routes/__tests__/create.js index 174e787485209..384c4b32cdfce 100644 --- a/src/server/saved_objects/routes/__tests__/create.js +++ b/src/server/saved_objects/routes/__tests__/create.js @@ -31,7 +31,9 @@ describe('POST /api/saved_objects/{type}', () => { method: 'POST', url: '/api/saved_objects/index-pattern', payload: { - title: 'Testing' + attributes: { + title: 'Testing' + } } }; const clientResponse = { @@ -49,12 +51,30 @@ describe('POST /api/saved_objects/{type}', () => { expect(response).to.eql(clientResponse); }); + it('requires attributes', async () => { + const request = { + method: 'POST', + url: '/api/saved_objects/index-pattern', + payload: {} + }; + + const { statusCode, payload } = await server.inject(request); + const response = JSON.parse(payload); + + expect(response.validation.keys).to.contain('attributes'); + expect(response.message).to.match(/is required/); + expect(response.statusCode).to.be(400); + expect(statusCode).to.be(400); + }); + it('calls upon savedObjectClient.create', async () => { const request = { method: 'POST', url: '/api/saved_objects/index-pattern', payload: { - title: 'Testing' + attributes: { + title: 'Testing' + } } }; diff --git a/src/server/saved_objects/routes/__tests__/update.js b/src/server/saved_objects/routes/__tests__/update.js index c55e90a7fc062..92a7494aca339 100644 --- a/src/server/saved_objects/routes/__tests__/update.js +++ b/src/server/saved_objects/routes/__tests__/update.js @@ -31,7 +31,9 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { method: 'PUT', url: '/api/saved_objects/index-pattern/logstash-*', payload: { - title: 'Testing' + attributes: { + title: 'Testing' + } } }; @@ -45,11 +47,14 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { }); it('calls upon savedObjectClient.update', async () => { + const attributes = { title: 'Testing' }; + const options = { version: 2 }; const request = { method: 'PUT', url: '/api/saved_objects/index-pattern/logstash-*', payload: { - title: 'Testing' + attributes, + version: options.version } }; @@ -57,6 +62,6 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { expect(savedObjectsClient.update.calledOnce).to.be(true); const args = savedObjectsClient.update.getCall(0).args; - expect(args).to.eql(['index-pattern', 'logstash-*', { title: 'Testing' }]); + expect(args).to.eql(['index-pattern', 'logstash-*', attributes, options]); }); }); diff --git a/src/server/saved_objects/routes/create.js b/src/server/saved_objects/routes/create.js index 17abf78e1ef31..76e6e6d975b78 100644 --- a/src/server/saved_objects/routes/create.js +++ b/src/server/saved_objects/routes/create.js @@ -10,13 +10,16 @@ export const createCreateRoute = (prereqs) => { params: Joi.object().keys({ type: Joi.string().required() }).required(), - payload: Joi.object().required() + payload: Joi.object({ + attributes: Joi.object().required() + }).required() }, handler(request, reply) { const { savedObjectsClient } = request.pre; const { type } = request.params; + const { attributes } = request.payload; - reply(savedObjectsClient.create(type, request.payload)); + reply(savedObjectsClient.create(type, attributes)); } } }; diff --git a/src/server/saved_objects/routes/update.js b/src/server/saved_objects/routes/update.js index 6443b5308edce..771f3352d372b 100644 --- a/src/server/saved_objects/routes/update.js +++ b/src/server/saved_objects/routes/update.js @@ -11,13 +11,18 @@ export const createUpdateRoute = (prereqs) => { type: Joi.string().required(), id: Joi.string().required(), }).required(), - payload: Joi.object().required() + payload: Joi.object({ + attributes: Joi.object().required(), + version: Joi.number().min(1) + }).required() }, handler(request, reply) { const { savedObjectsClient } = request.pre; const { type, id } = request.params; + const { attributes, version } = request.payload; + const options = { version }; - reply(savedObjectsClient.update(type, id, request.payload)); + reply(savedObjectsClient.update(type, id, attributes, options)); } } }; diff --git a/src/ui/public/saved_objects/__tests__/saved_object.js b/src/ui/public/saved_objects/__tests__/saved_object.js new file mode 100644 index 0000000000000..d4d383dd69de1 --- /dev/null +++ b/src/ui/public/saved_objects/__tests__/saved_object.js @@ -0,0 +1,34 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; +import { SavedObject } from '../saved_object'; + +describe('SavedObject', () => { + it('persists type and id', () => { + const id = 'logstash-*'; + const type = 'index-pattern'; + + const client = sinon.stub(); + const savedObject = new SavedObject(client, { id, type }); + + expect(savedObject._id).to.be(id); + expect(savedObject._type).to.be(type); + }); + + it('persists attributes', () => { + const attributes = { title: 'My title' }; + + const client = sinon.stub(); + const savedObject = new SavedObject(client, { attributes }); + + expect(savedObject._attributes).to.be(attributes); + }); + + it('persists version', () => { + const version = 2; + + const client = sinon.stub(); + const savedObject = new SavedObject(client, { version }); + + expect(savedObject._version).to.be(version); + }); +}); diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index a484eaad8e670..6ef23bdda6412 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -4,16 +4,21 @@ import { pick } from 'lodash'; import { SavedObjectsClient } from '../saved_objects_client'; import { SavedObject } from '../saved_object'; - describe('SavedObjectsClient', () => { const basePath = Math.random().toString(36).substring(7); const sandbox = sinon.sandbox.create(); + const doc = { + id: 'AVwSwFxtcMV38qjDZoQg', + type: 'config', + attributes: { title: 'Example title' } + }; let savedObjectsClient; + let $http; beforeEach(() => { - savedObjectsClient = new SavedObjectsClient(sinon.stub, basePath); - sandbox.stub(savedObjectsClient, '_$http'); + $http = sandbox.stub().returns(Promise.resolve({})); + savedObjectsClient = new SavedObjectsClient($http, basePath); }); afterEach(() => { @@ -47,7 +52,7 @@ describe('SavedObjectsClient', () => { const params = { foo: 'Foo', bar: 'Bar' }; it('passes options to $http', () => { - savedObjectsClient._$http.withArgs({ + $http.withArgs({ method: 'POST', url: '/api/path', data: params @@ -55,11 +60,11 @@ describe('SavedObjectsClient', () => { savedObjectsClient._request('POST', '/api/path', params); - expect(savedObjectsClient._$http.calledOnce).to.be(true); + expect($http.calledOnce).to.be(true); }); it('sets params for GET request', () => { - savedObjectsClient._$http.withArgs({ + $http.withArgs({ method: 'GET', url: '/api/path', params: params @@ -67,94 +72,92 @@ describe('SavedObjectsClient', () => { savedObjectsClient._request('GET', '/api/path', params); - expect(savedObjectsClient._$http.calledOnce).to.be(true); + expect($http.calledOnce).to.be(true); }); - it('catches API error', (done) => { + it('catches API error', async () => { const message = 'Request failed'; + $http.returns(Promise.reject({ data: { error: message } })); - savedObjectsClient._$http.returns(Promise.reject({ data: { error: message } })); - savedObjectsClient._request('GET', '/api/path', params).then(() => { - done('should have thrown'); - }).catch(e => { + try { + await savedObjectsClient._request('GET', '/api/path', params); + expect().fail('should have error'); + } catch (e) { expect(e.message).to.eql(message); - done(); - }); + } }); - it('catches API error message', (done) => { + it('catches API error message', async () => { const message = 'Request failed'; + $http.returns(Promise.reject({ data: { message: message } })); - savedObjectsClient._$http.returns(Promise.reject({ data: { message: message } })); - savedObjectsClient._request('GET', '/api/path', params).then(() => { - done('should have thrown'); - }).catch(e => { + try { + await savedObjectsClient._request('GET', '/api/path', params); + expect().fail('should have error'); + } catch (e) { expect(e.message).to.eql(message); - done(); - }); + } }); - it('catches API error status', (done) => { - savedObjectsClient._$http.returns(Promise.reject({ status: 404 })); - savedObjectsClient._request('GET', '/api/path', params).then(() => { - done('should have thrown'); - }).catch(e => { + it('catches API error status', async () => { + $http.returns(Promise.reject({ status: 404 })); + + try { + await savedObjectsClient._request('GET', '/api/path', params); + expect().fail('should have error'); + } catch (e) { expect(e.message).to.eql('404 Response'); - done(); - }); + } }); }); describe('#get', () => { - const attributes = { type: 'index-pattern', foo: 'Foo' }; - beforeEach(() => { - savedObjectsClient._$http.withArgs({ + $http.withArgs({ method: 'GET', url: `${basePath}/api/saved_objects/index-pattern/logstash-*` - }).returns(Promise.resolve(attributes)); + }).returns(Promise.resolve(doc)); }); it('returns a promise', () => { expect(savedObjectsClient.get('index-pattern', 'logstash-*')).to.be.a(Promise); }); - it('requires type', (done) => { - savedObjectsClient.get().then(() => { - done('should require type'); - }).catch((e) => { - expect(e.message).to.contain('requires type and id'); - done(); - }); + it('requires type', async () => { + try { + await savedObjectsClient.get(); + expect().fail('should have error'); + } catch (e) { + expect(e.message).to.be('requires type and id'); + } }); - it('requires id', (done) => { - savedObjectsClient.get('index-pattern').then(() => { - done('should require id'); - }).catch((e) => { - expect(e.message).to.contain('requires type and id'); - done(); - }); + it('requires id', async () => { + try { + await savedObjectsClient.get('index-pattern'); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be('requires type and id'); + } }); it('resolves with instantiated ObjectClass', async () => { const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); expect(response).to.be.a(SavedObject); - expect(response.get('type')).to.eql('index-pattern'); - expect(response.get('foo')).to.eql('Foo'); - expect(response._attributes).to.eql(attributes); + expect(response._type).to.eql('config'); + expect(response.get('title')).to.eql('Example title'); expect(response._client).to.be.a(SavedObjectsClient); }); it('makes HTTP call', () => { savedObjectsClient.get('index-pattern', 'logstash-*'); - sinon.assert.calledOnce(savedObjectsClient._$http); + sinon.assert.calledOnce($http); }); }); describe('#delete', () => { beforeEach(() => { - savedObjectsClient._$http.withArgs({ + $http.withArgs({ method: 'DELETE', url: `${basePath}/api/saved_objects/index-pattern/logstash-*` }).returns(Promise.resolve({ data: 'api-response' })); @@ -164,35 +167,35 @@ describe('SavedObjectsClient', () => { expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).to.be.a(Promise); }); - it('requires type', (done) => { - savedObjectsClient.delete().then(() => { - done('should require type'); - }).catch((e) => { - expect(e.message).to.contain('requires type and id'); - done(); - }); + it('requires type', async () => { + try { + await savedObjectsClient.delete(); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be('requires type and id'); + } }); - it('requires id', (done) => { - savedObjectsClient.delete('index-pattern').then(() => { - done('should require id'); - }).catch((e) => { - expect(e.message).to.contain('requires type and id'); - done(); - }); + it('requires id', async () => { + try { + await savedObjectsClient.delete('index-pattern'); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be('requires type and id'); + } }); it('makes HTTP call', () => { savedObjectsClient.delete('index-pattern', 'logstash-*'); - sinon.assert.calledOnce(savedObjectsClient._$http); + sinon.assert.calledOnce($http); }); }); describe('#update', () => { - const requireMessage = 'requires type, id and body'; + const requireMessage = 'requires type, id and attributes'; beforeEach(() => { - savedObjectsClient._$http.withArgs({ + $http.withArgs({ method: 'PUT', url: `${basePath}/api/saved_objects/index-pattern/logstash-*`, data: sinon.match.any @@ -203,40 +206,42 @@ describe('SavedObjectsClient', () => { expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).to.be.a(Promise); }); - it('requires type', (done) => { - savedObjectsClient.update().then(() => { - done('should require type'); - }).catch((e) => { - expect(e.message).to.contain(requireMessage); - done(); - }); + it('requires type', async () => { + try { + await savedObjectsClient.update(); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be(requireMessage); + } }); - it('requires id', (done) => { - savedObjectsClient.update('index-pattern').then(() => { - done('should require id'); - }).catch((e) => { - expect(e.message).to.contain(requireMessage); - done(); - }); + it('requires id', async () => { + try { + await savedObjectsClient.update('index-pattern'); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be(requireMessage); + } }); - it('requires body', (done) => { - savedObjectsClient.update('index-pattern', 'logstash-*').then(() => { - done('should require body'); - }).catch((e) => { - expect(e.message).to.contain(requireMessage); - done(); - }); + it('requires attributes', async () => { + try { + await savedObjectsClient.update('index-pattern', 'logstash-*'); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be(requireMessage); + } }); it('makes HTTP call', () => { - const body = { foo: 'Foo', bar: 'Bar' }; + const attributes = { foo: 'Foo', bar: 'Bar' }; + const body = { attributes, version: 2 }; + const options = { version: 2 }; - savedObjectsClient.update('index-pattern', 'logstash-*', body); - sinon.assert.calledOnce(savedObjectsClient._$http); + savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); + sinon.assert.calledOnce($http); - expect(savedObjectsClient._$http.getCall(0).args[0].data).to.eql(body); + expect($http.getCall(0).args[0].data).to.eql(body); }); }); @@ -244,7 +249,7 @@ describe('SavedObjectsClient', () => { const requireMessage = 'requires type and body'; beforeEach(() => { - savedObjectsClient._$http.withArgs({ + $http.withArgs({ method: 'POST', url: `${basePath}/api/saved_objects/index-pattern`, data: sinon.match.any @@ -255,30 +260,30 @@ describe('SavedObjectsClient', () => { expect(savedObjectsClient.create('index-pattern', {})).to.be.a(Promise); }); - it('requires type', (done) => { - savedObjectsClient.create().then(() => { - done('should require type'); - }).catch((e) => { - expect(e.message).to.contain(requireMessage); - done(); - }); + it('requires type', async () => { + try { + await savedObjectsClient.create(); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be(requireMessage); + } }); - it('requires body', (done) => { - savedObjectsClient.create('index-pattern').then(() => { - done('should require body'); - }).catch((e) => { - expect(e.message).to.contain(requireMessage); - done(); - }); + it('requires body', async () => { + try { + await savedObjectsClient.create('index-pattern'); + expect().throw('should have error'); + } catch (e) { + expect(e.message).to.be(requireMessage); + } }); it('makes HTTP call', () => { const body = { foo: 'Foo', bar: 'Bar', id: 'logstash-*' }; savedObjectsClient.create('index-pattern', body); - sinon.assert.calledOnce(savedObjectsClient._$http); - expect(savedObjectsClient._$http.getCall(0).args[0].data).to.eql(body); + sinon.assert.calledOnce($http); + expect($http.getCall(0).args[0].data).to.eql(body); }); }); @@ -286,7 +291,7 @@ describe('SavedObjectsClient', () => { const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; beforeEach(() => { - savedObjectsClient._$http.returns(Promise.resolve({ data: [object] })); + $http.returns(Promise.resolve({ data: [object] })); }); it('returns a promise', () => { @@ -297,9 +302,9 @@ describe('SavedObjectsClient', () => { const body = { type: 'index-pattern', invalid: true }; savedObjectsClient.find(body); - expect(savedObjectsClient._$http.calledOnce).to.be(true); + expect($http.calledOnce).to.be(true); - const options = savedObjectsClient._$http.getCall(0).args[0]; + const options = $http.getCall(0).args[0]; expect(options.url).to.eql(`${basePath}/api/saved_objects/index-pattern`); }); @@ -307,9 +312,9 @@ describe('SavedObjectsClient', () => { const body = { fields: ['title', 'description'], invalid: true }; savedObjectsClient.find(body); - expect(savedObjectsClient._$http.calledOnce).to.be(true); + expect($http.calledOnce).to.be(true); - const options = savedObjectsClient._$http.getCall(0).args[0]; + const options = $http.getCall(0).args[0]; expect(options.params).to.eql(pick(body, ['fields'])); }); @@ -317,9 +322,9 @@ describe('SavedObjectsClient', () => { const body = { from: 50, size: 10, invalid: true }; savedObjectsClient.find(body); - expect(savedObjectsClient._$http.calledOnce).to.be(true); + expect($http.calledOnce).to.be(true); - const options = savedObjectsClient._$http.getCall(0).args[0]; + const options = $http.getCall(0).args[0]; expect(options.params).to.eql(pick(body, ['from', 'size'])); }); }); diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js index 4b027e7adea4c..0512d2ffe757e 100644 --- a/src/ui/public/saved_objects/saved_object.js +++ b/src/ui/public/saved_objects/saved_object.js @@ -1,9 +1,12 @@ import _ from 'lodash'; export class SavedObject { - constructor(client, attributes) { + constructor(client, { id, type, version, attributes }) { this._client = client; - this._attributes = attributes; + this._id = id; + this._type = type; + this._attributes = attributes || {}; + this._version = version; } get(key) { @@ -14,8 +17,12 @@ export class SavedObject { return _.set(this._attributes, key, value); } + has(key) { + return _.has(this._attributes, key); + } + save() { - if (this.id) { + if (this._id) { return this._client.update(this.type, this.id, this._attributes); } else { return this._client.create(this.type, this._attributes); diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index d610507520c34..c4d492b6fd31d 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -32,11 +32,16 @@ export class SavedObjectsClient { return this._request('DELETE', this._getUrl([type, id])); } - update(type, id, body) { - if (!type || !id || !body) { - return this._PromiseCtor.reject(new Error('requires type, id and body')); + update(type, id, attributes, { version } = {}) { + if (!type || !id || !attributes) { + return this._PromiseCtor.reject(new Error('requires type, id and attributes')); } + const body = { + attributes, + version + }; + return this._request('PUT', this._getUrl([type, id]), body); } @@ -84,12 +89,13 @@ export class SavedObjectsClient { options.data = body; } - return this._$http(options) - .catch(resp => { + return this._$http(options).catch(resp => { const respBody = resp.data || {}; const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); + err.status = resp.status; err.body = respBody; + throw err; }); } From 6148eaca6d5ff1879029c16c0d12da845d80a53a Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 18 May 2017 10:10:45 -0700 Subject: [PATCH 19/25] search_fields should be snake case Signed-off-by: Tyler Smalley --- src/server/saved_objects/routes/__tests__/find.js | 14 ++++++++++++++ src/server/saved_objects/routes/find.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/server/saved_objects/routes/__tests__/find.js b/src/server/saved_objects/routes/__tests__/find.js index 7a3d2aa0caacd..8b3e4808d10bf 100644 --- a/src/server/saved_objects/routes/__tests__/find.js +++ b/src/server/saved_objects/routes/__tests__/find.js @@ -88,6 +88,20 @@ describe('GET /api/saved_objects/{type?}', () => { expect(options).to.eql({ perPage: 10, page: 50 }); }); + it('accepts the query parameter search_fields', async() => { + const request = { + method: 'GET', + url: '/api/saved_objects?search_fields=title' + }; + + await server.inject(request); + + expect(savedObjectsClient.find.calledOnce).to.be(true); + + const options = savedObjectsClient.find.getCall(0).args[0]; + expect(options).to.eql({ perPage: 20, page: 1, searchFields: 'title' }); + }); + it('accepts the query parameter fields as a string', async () => { const request = { method: 'GET', diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js index d69c30dd182d3..1d9720fb0302f 100644 --- a/src/server/saved_objects/routes/find.js +++ b/src/server/saved_objects/routes/find.js @@ -15,7 +15,7 @@ export const createFindRoute = (prereqs) => ({ page: Joi.number().min(0).default(1), type: Joi.string(), search: Joi.string().allow('').optional(), - searchFields: [Joi.string(), Joi.array().items(Joi.string())], + search_fields: [Joi.string(), Joi.array().items(Joi.string())], fields: [Joi.string(), Joi.array().items(Joi.string())] }) }, From 9eb2efc54c2391725396279ba22a04c95eab9ec2 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 22 May 2017 01:05:03 -0700 Subject: [PATCH 20/25] $http Response returns data attribute Signed-off-by: Tyler Smalley --- .../saved_dashboard_register.js | 7 +++++- .../saved_objects/__tests__/saved_object.js | 4 ++-- .../__tests__/saved_objects_client.js | 10 ++++----- src/ui/public/saved_objects/saved_object.js | 8 +++---- .../saved_objects/saved_objects_client.js | 22 ++++++++++--------- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js index 0c1b26f1e4605..9b83367b1c8ac 100644 --- a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js +++ b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js @@ -1,3 +1,8 @@ export function savedDashboardRegister(savedDashboards) { - return savedDashboards; + return { + client: savedDashboards, + name: 'dashboards', + singular: 'Saved Dashboard', + plural: 'Saved Dashboards' + }; } diff --git a/src/ui/public/saved_objects/__tests__/saved_object.js b/src/ui/public/saved_objects/__tests__/saved_object.js index d4d383dd69de1..f519b3760de27 100644 --- a/src/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/ui/public/saved_objects/__tests__/saved_object.js @@ -10,8 +10,8 @@ describe('SavedObject', () => { const client = sinon.stub(); const savedObject = new SavedObject(client, { id, type }); - expect(savedObject._id).to.be(id); - expect(savedObject._type).to.be(type); + expect(savedObject.id).to.be(id); + expect(savedObject.type).to.be(type); }); it('persists attributes', () => { diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index 6ef23bdda6412..85557d8dfc499 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -116,7 +116,7 @@ describe('SavedObjectsClient', () => { $http.withArgs({ method: 'GET', url: `${basePath}/api/saved_objects/index-pattern/logstash-*` - }).returns(Promise.resolve(doc)); + }).returns(Promise.resolve({ data: doc })); }); it('returns a promise', () => { @@ -144,7 +144,7 @@ describe('SavedObjectsClient', () => { it('resolves with instantiated ObjectClass', async () => { const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); expect(response).to.be.a(SavedObject); - expect(response._type).to.eql('config'); + expect(response.type).to.eql('config'); expect(response.get('title')).to.eql('Example title'); expect(response._client).to.be.a(SavedObjectsClient); }); @@ -291,7 +291,7 @@ describe('SavedObjectsClient', () => { const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; beforeEach(() => { - $http.returns(Promise.resolve({ data: [object] })); + $http.returns(Promise.resolve({ data: { data: [object] } })); }); it('returns a promise', () => { @@ -309,7 +309,7 @@ describe('SavedObjectsClient', () => { }); it('accepts fields', () => { - const body = { fields: ['title', 'description'], invalid: true }; + const body = { fields: ['title', 'description'] }; savedObjectsClient.find(body); expect($http.calledOnce).to.be(true); @@ -319,7 +319,7 @@ describe('SavedObjectsClient', () => { }); it('accepts from/size', () => { - const body = { from: 50, size: 10, invalid: true }; + const body = { from: 50, size: 10 }; savedObjectsClient.find(body); expect($http.calledOnce).to.be(true); diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js index 0512d2ffe757e..e9763c09affaf 100644 --- a/src/ui/public/saved_objects/saved_object.js +++ b/src/ui/public/saved_objects/saved_object.js @@ -1,10 +1,10 @@ import _ from 'lodash'; export class SavedObject { - constructor(client, { id, type, version, attributes }) { + constructor(client, { id, type, version, attributes } = {}) { this._client = client; - this._id = id; - this._type = type; + this.id = id; + this.type = type; this._attributes = attributes || {}; this._version = version; } @@ -22,7 +22,7 @@ export class SavedObject { } save() { - if (this._id) { + if (this.id) { return this._client.update(this.type, this.id, this._attributes); } else { return this._client.create(this.type, this._attributes); diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index c4d492b6fd31d..d8ee83099c9bc 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -1,5 +1,6 @@ import { resolve as resolveUrl, format as formatUrl } from 'url'; import { pick, partial } from 'lodash'; +import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../../../utils/case_conversion'; import { SavedObject } from './saved_object'; @@ -57,11 +58,10 @@ export class SavedObjectsClient { find(options = {}) { const url = this._getUrl([options.type]); - const validOptions = pick(options, ['from', 'size', 'fields', 'filter']); - return this._request('GET', url, validOptions).then(resp => { + return this._request('GET', url, keysToSnakeCaseShallow(options)).then(resp => { resp.data = resp.data.map(d => new this.ObjectClass(d)); - return resp; + return keysToCamelCaseShallow(resp); }); } @@ -89,14 +89,16 @@ export class SavedObjectsClient { options.data = body; } - return this._$http(options).catch(resp => { - const respBody = resp.data || {}; - const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); + return this._$http(options) + .then(resp => resp.data) + .catch(resp => { + const respBody = resp.data || {}; + const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); - err.status = resp.status; - err.body = respBody; + err.status = resp.status; + err.body = respBody; - throw err; - }); + throw err; + }); } } From 639feb9e6b0caddee9f13c6f095e6e057c5c340c Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 22 May 2017 11:48:20 -0700 Subject: [PATCH 21/25] Embed object array as saved_objects instead of data Signed-off-by: Tyler Smalley --- .../saved_objects/client/__tests__/saved_objects_client.js | 4 ++-- src/server/saved_objects/client/saved_objects_client.js | 2 +- src/ui/public/saved_objects/__tests__/saved_objects_client.js | 2 +- src/ui/public/saved_objects/saved_objects_client.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 2112dbaf5a684..1cb651b868444 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -127,9 +127,9 @@ describe('SavedObjectsClient', () => { const response = await savedObjectsClient.find(); expect(response.total).to.be(count); - expect(response.data).to.have.length(count); + expect(response.saved_objects).to.have.length(count); docs.hits.hits.forEach((doc, i) => { - expect(response.data[i]).to.eql({ + expect(response.saved_objects[i]).to.eql({ id: doc._id, type: doc._type, version: doc._version, diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 2d40da1df99cb..3da369d700000 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -61,7 +61,7 @@ export class SavedObjectsClient { const response = await this._withKibanaIndex('search', esOptions); return { - data: get(response, 'hits.hits', []).map(r => { + saved_objects: get(response, 'hits.hits', []).map(r => { return { id: r._id, type: r._type, diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index 85557d8dfc499..5f54f74cacbee 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -291,7 +291,7 @@ describe('SavedObjectsClient', () => { const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; beforeEach(() => { - $http.returns(Promise.resolve({ data: { data: [object] } })); + $http.returns(Promise.resolve({ data: { saved_objects: [object] } })); }); it('returns a promise', () => { diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index d8ee83099c9bc..1eac671712786 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -60,7 +60,7 @@ export class SavedObjectsClient { const url = this._getUrl([options.type]); return this._request('GET', url, keysToSnakeCaseShallow(options)).then(resp => { - resp.data = resp.data.map(d => new this.ObjectClass(d)); + resp.saved_objects = resp.saved_objects.map(d => new this.ObjectClass(d)); return keysToCamelCaseShallow(resp); }); } From bfadd34f1d6cd68baaebe4c4dede83ceb8b69a7b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 22 May 2017 12:43:30 -0700 Subject: [PATCH 22/25] Review feedback Signed-off-by: Tyler Smalley --- .../__tests__/saved_objects_client.js | 40 ++++++------------- .../saved_objects/saved_objects_client.js | 26 ++++++------ 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index 5f54f74cacbee..c0eda17196de6 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -1,6 +1,5 @@ import sinon from 'sinon'; import expect from 'expect.js'; -import { pick } from 'lodash'; import { SavedObjectsClient } from '../saved_objects_client'; import { SavedObject } from '../saved_object'; @@ -63,36 +62,21 @@ describe('SavedObjectsClient', () => { expect($http.calledOnce).to.be(true); }); - it('sets params for GET request', () => { - $http.withArgs({ - method: 'GET', - url: '/api/path', - params: params - }).returns(Promise.resolve({ data: '' })); - - savedObjectsClient._request('GET', '/api/path', params); - - expect($http.calledOnce).to.be(true); - }); - - it('catches API error', async () => { - const message = 'Request failed'; - $http.returns(Promise.reject({ data: { error: message } })); - + it('throws error when body is provided for GET', async () => { try { await savedObjectsClient._request('GET', '/api/path', params); expect().fail('should have error'); } catch (e) { - expect(e.message).to.eql(message); + expect(e.message).to.eql('data not permitted for GET requests'); } }); - it('catches API error message', async () => { + it('catches API error', async () => { const message = 'Request failed'; - $http.returns(Promise.reject({ data: { message: message } })); + $http.returns(Promise.reject({ data: { error: message } })); try { - await savedObjectsClient._request('GET', '/api/path', params); + await savedObjectsClient._request('POST', '/api/path', params); expect().fail('should have error'); } catch (e) { expect(e.message).to.eql(message); @@ -103,7 +87,7 @@ describe('SavedObjectsClient', () => { $http.returns(Promise.reject({ status: 404 })); try { - await savedObjectsClient._request('GET', '/api/path', params); + await savedObjectsClient._request('POST', '/api/path', params); expect().fail('should have error'); } catch (e) { expect(e.message).to.eql('404 Response'); @@ -115,7 +99,8 @@ describe('SavedObjectsClient', () => { beforeEach(() => { $http.withArgs({ method: 'GET', - url: `${basePath}/api/saved_objects/index-pattern/logstash-*` + url: `${basePath}/api/saved_objects/index-pattern/logstash-*`, + data: undefined }).returns(Promise.resolve({ data: doc })); }); @@ -141,7 +126,7 @@ describe('SavedObjectsClient', () => { } }); - it('resolves with instantiated ObjectClass', async () => { + it('resolves with instantiated SavedObject', async () => { const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); expect(response).to.be.a(SavedObject); expect(response.type).to.eql('config'); @@ -305,7 +290,7 @@ describe('SavedObjectsClient', () => { expect($http.calledOnce).to.be(true); const options = $http.getCall(0).args[0]; - expect(options.url).to.eql(`${basePath}/api/saved_objects/index-pattern`); + expect(options.url).to.eql(`${basePath}/api/saved_objects/index-pattern?type=index-pattern&invalid=true`); }); it('accepts fields', () => { @@ -315,7 +300,7 @@ describe('SavedObjectsClient', () => { expect($http.calledOnce).to.be(true); const options = $http.getCall(0).args[0]; - expect(options.params).to.eql(pick(body, ['fields'])); + expect(options.url).to.eql(`${basePath}/api/saved_objects/?fields=title&fields=description`); }); it('accepts from/size', () => { @@ -325,7 +310,8 @@ describe('SavedObjectsClient', () => { expect($http.calledOnce).to.be(true); const options = $http.getCall(0).args[0]; - expect(options.params).to.eql(pick(body, ['from', 'size'])); + expect(options.url).to.eql(`${basePath}/api/saved_objects/?from=50&size=10`); + }); }); }); diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index 1eac671712786..b21ff8fd0527a 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -1,5 +1,5 @@ import { resolve as resolveUrl, format as formatUrl } from 'url'; -import { pick, partial } from 'lodash'; +import { pick, get } from 'lodash'; import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../../../utils/case_conversion'; import { SavedObject } from './saved_object'; @@ -21,7 +21,7 @@ export class SavedObjectsClient { } return this._request('GET', this._getUrl([type, id])).then(resp => { - return new this.ObjectClass(resp); + return this.createSavedObject(resp); }); } @@ -57,16 +57,16 @@ export class SavedObjectsClient { } find(options = {}) { - const url = this._getUrl([options.type]); + const url = this._getUrl([options.type], keysToSnakeCaseShallow(options)); - return this._request('GET', url, keysToSnakeCaseShallow(options)).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => new this.ObjectClass(d)); + return this._request('GET', url).then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); return keysToCamelCaseShallow(resp); }); } - get ObjectClass() { - return partial(SavedObject, this); + createSavedObject(options) { + return new SavedObject(this, options); } _getUrl(path, query) { @@ -80,17 +80,15 @@ export class SavedObjectsClient { })); } - _request(method, url, body) { - const options = { method, url }; + _request(method, url, data) { + const options = { method, url, data }; - if (method === 'GET' && body) { - options.params = body; - } else if (body) { - options.data = body; + if (method === 'GET' && data) { + return this._PromiseCtor.reject(new Error('data not permitted for GET requests')); } return this._$http(options) - .then(resp => resp.data) + .then(resp => get(resp, 'data')) .catch(resp => { const respBody = resp.data || {}; const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); From ef193a2f26328e73ad74c9ebe46aa821dd713c9c Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 22 May 2017 12:54:43 -0700 Subject: [PATCH 23/25] Removes delete boolean response Signed-off-by: Tyler Smalley --- .../saved_objects/client/__tests__/saved_objects_client.js | 7 ------- src/server/saved_objects/client/saved_objects_client.js | 2 -- 2 files changed, 9 deletions(-) diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index 1cb651b868444..2498327e1e401 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -85,13 +85,6 @@ describe('SavedObjectsClient', () => { }); describe('#delete', () => { - it('returns based on ES success', async () => { - callAdminCluster.returns(Promise.resolve({ result: 'deleted' })); - const response = await savedObjectsClient.delete('index-pattern', 'logstash-*'); - - expect(response).to.be(true); - }); - it('throws notFound when ES is unable to find the document', (done) => { callAdminCluster.returns(Promise.resolve({ found: false })); diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 3da369d700000..2d7f06efcb199 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -36,8 +36,6 @@ export class SavedObjectsClient { if (get(response, 'found') === false) { throw Boom.notFound(); } - - return get(response, 'result') === 'deleted'; } async find(options = {}) { From d44aae1e3de36ea1202e31634d36fe55d0e8fc82 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 23 May 2017 09:54:32 -0700 Subject: [PATCH 24/25] Renames data attribute to body Signed-off-by: Tyler Smalley --- .../saved_objects/__tests__/saved_objects_client.js | 2 +- src/ui/public/saved_objects/saved_objects_client.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index c0eda17196de6..38d5fa5b5c59b 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -67,7 +67,7 @@ describe('SavedObjectsClient', () => { await savedObjectsClient._request('GET', '/api/path', params); expect().fail('should have error'); } catch (e) { - expect(e.message).to.eql('data not permitted for GET requests'); + expect(e.message).to.eql('body not permitted for GET requests'); } }); diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index b21ff8fd0527a..04aef92a0b678 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -80,11 +80,11 @@ export class SavedObjectsClient { })); } - _request(method, url, data) { - const options = { method, url, data }; + _request(method, url, body) { + const options = { method, url, data: body }; - if (method === 'GET' && data) { - return this._PromiseCtor.reject(new Error('data not permitted for GET requests')); + if (method === 'GET' && body) { + return this._PromiseCtor.reject(new Error('body not permitted for GET requests')); } return this._$http(options) From 4454f85cec0f86f9f18df10592ae8c0d7ed92c76 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 23 May 2017 14:21:34 -0700 Subject: [PATCH 25/25] Restores savedDashboardRegister Signed-off-by: Tyler Smalley --- .../dashboard/saved_dashboard/saved_dashboard_register.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js index 9b83367b1c8ac..0c1b26f1e4605 100644 --- a/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js +++ b/src/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_register.js @@ -1,8 +1,3 @@ export function savedDashboardRegister(savedDashboards) { - return { - client: savedDashboards, - name: 'dashboards', - singular: 'Saved Dashboard', - plural: 'Saved Dashboards' - }; + return savedDashboards; }