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..d6906d5176448 --- /dev/null +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -0,0 +1,255 @@ +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-*', _version: 2 }); + + 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', async () => { + callWithRequest.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 }); + + 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'); + }); + }); + + describe('#delete', () => { + it('returns based on ES success', async () => { + callWithRequest.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 })); + + 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, version: doc._version }, + 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: {} }, version: true }, + 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, version: true }, + 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._version', + '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', + 'hits.hits._version' + ]); + }); + }); + + describe('#get', () => { + it('formats Elasticsearch response', async () => { + callWithRequest.returns(Promise.resolve({ + _id: 'logstash-*', + _type: 'index-pattern', + _version: 2, + _source: { + title: 'Testing' + } + })); + + const response = await savedObjectsClient.get('index-pattern', 'logstash-*'); + expect(response).to.eql({ + id: 'logstash-*', + type: 'index-pattern', + title: 'Testing', + version: 2 + }); + }); + }); + + 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-*', + version: undefined, + body: { doc: { title: 'Testing' } }, + refresh: 'wait_for', + 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..e115b242a28ea --- /dev/null +++ b/src/server/saved_objects/client/lib/__tests__/create_filter_path.js @@ -0,0 +1,27 @@ +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/__tests__/create_find_query.js b/src/server/saved_objects/client/lib/__tests__/create_find_query.js new file mode 100644 index 0000000000000..e682c5ddbda6f --- /dev/null +++ b/src/server/saved_objects/client/lib/__tests__/create_find_query.js @@ -0,0 +1,82 @@ +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: {} }, version: true }); + }); + + 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: {} + }] + } + }, + version: true + }); + }); + + 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 + } + }] + } + }, + version: 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'] + } + }] + } + }, + version: true + }); + }); + + 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'] + } + }] + } + }, + version: true + }); + }); +}); 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..2ffc1842ddbaa --- /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', '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/create_find_query.js b/src/server/saved_objects/client/lib/create_find_query.js new file mode 100644 index 0000000000000..fc5993e21b8a5 --- /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 { version: true, 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 { version: true, 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..a6a1b695a79db --- /dev/null +++ b/src/server/saved_objects/client/lib/handle_es_error.js @@ -0,0 +1,49 @@ +import elasticsearch from 'elasticsearch'; +import Boom from 'boom'; +import { get } from 'lodash'; + +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'); + } + + const reason = get(error, 'body.error.reason'); + + if ( + error instanceof ConnectionFault || + error instanceof ServiceUnavailable || + error instanceof NoConnections || + error instanceof RequestTimeout + ) { + throw Boom.serverTimeout(); + } + + if (error instanceof Conflict) { + throw Boom.conflict(reason); + } + + if (error instanceof Forbidden) { + throw Boom.forbidden(reason); + } + + if (error instanceof NotFound) { + throw Boom.notFound(reason); + } + + if (error instanceof BadRequest) { + throw Boom.badRequest(reason); + } + + 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..ead6eeee61217 --- /dev/null +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -0,0 +1,109 @@ +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 response = await this._withKibanaIndex('create', { + type, + 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, 'result') === 'deleted'; + } + + 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, version: r._version }, 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, + 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: doc + }, + refresh: 'wait_for' + }); + + 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..174e787485209 --- /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/saved_objects/{type}', () => { + 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/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/saved_objects/index-pattern', + 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' }]); + }); +}); 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..b020369210d73 --- /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/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/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/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..e93c5be51f78e --- /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/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/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/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/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/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/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/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/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..ea1205fdf345b --- /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/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/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/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..c55e90a7fc062 --- /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/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/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/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..17abf78e1ef31 --- /dev/null +++ b/src/server/saved_objects/routes/create.js @@ -0,0 +1,23 @@ +import Joi from 'joi'; + +export const createCreateRoute = (prereqs) => { + return { + path: '/api/saved_objects/{type}', + method: 'POST', + config: { + pre: [prereqs.getSavedObjectsClient], + validate: { + params: Joi.object().keys({ + type: Joi.string().required() + }).required(), + payload: Joi.object().required() + }, + handler(request, reply) { + const { savedObjectsClient } = request.pre; + const { type } = request.params; + + reply(savedObjectsClient.create(type, request.payload)); + } + } + }; +}; diff --git a/src/server/saved_objects/routes/delete.js b/src/server/saved_objects/routes/delete.js new file mode 100644 index 0000000000000..a394530a0f655 --- /dev/null +++ b/src/server/saved_objects/routes/delete.js @@ -0,0 +1,21 @@ +import Joi from 'joi'; + +export const createDeleteRoute = (prereqs) => ({ + path: '/api/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..7c1991019ae9e --- /dev/null +++ b/src/server/saved_objects/routes/find.js @@ -0,0 +1,31 @@ +import Joi from 'joi'; + +export const createFindRoute = (prereqs) => ({ + path: '/api/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..f96b0cab51ad0 --- /dev/null +++ b/src/server/saved_objects/routes/read.js @@ -0,0 +1,21 @@ +import Joi from 'joi'; + +export const createReadRoute = (prereqs) => ({ + path: '/api/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..6443b5308edce --- /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/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/server/status/index.js b/src/server/status/index.js index 58031184e9bba..3156e472a598f 100644 --- a/src/server/status/index.js +++ b/src/server/status/index.js @@ -1,6 +1,7 @@ import ServerStatus from './server_status'; import wrapAuthConfig from './wrap_auth_config'; import { Metrics } from './metrics'; +import { getStats } from './stats'; export default function (kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); @@ -18,7 +19,11 @@ export default function (kbnServer, server, config) { server.route(wrapAuth({ method: 'GET', path: '/api/status', - handler: function (request, reply) { + handler: async function (request, reply) { + const stats = getStats( + server.config().get('kibana.index'), + request, + server.plugins.elasticsearch.getCluster('admin').callWithRequest); const status = { name: config.get('server.name'), uuid: config.get('server.uuid'), @@ -29,7 +34,8 @@ export default function (kbnServer, server, config) { build_snapshot: matchSnapshot.test(config.get('pkg.version')) }, status: kbnServer.status.toJSON(), - metrics: kbnServer.metrics + metrics: kbnServer.metrics, + stats, }; return reply(status); diff --git a/src/server/status/stats.js b/src/server/status/stats.js new file mode 100644 index 0000000000000..28958567b090c --- /dev/null +++ b/src/server/status/stats.js @@ -0,0 +1,12 @@ +import { SavedObjectsClient } from '../saved_objects/client'; + +export async function getStats(kibanaIndex, request, callWithRequest) { + const savedObjectsClient = new SavedObjectsClient(kibanaIndex, request, callWithRequest); + const dashResponse = await savedObjectsClient.find({ type: 'dashboard', perPage: 0 }); + const visResponse = await savedObjectsClient.find({ type: 'visualization', perPage: 0 }); + + return { + dashboardCount: dashResponse.total, + visualizationCount: visResponse.total, + }; +} 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..db2440c235242 --- /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/saved_objects/`; + + expect(url).to.be(expected); + }); + + it('appends path', () => { + const url = savedObjectsClient._getUrl(['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/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/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/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/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/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/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..c48b5fb71a6e5 --- /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.client.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..773ff5c7634c7 --- /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/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()); +}