Skip to content

Commit

Permalink
Adds an API for managing saved objects (#11632)
Browse files Browse the repository at this point in the history
POST /api/saved_objects/{type}
GET /api/saved_objects/{type}/{id}
PUT /api/saved_objects/{type}/{id}
DELETE /api/saved_objects/{type}/{id}
GET /api/saved_objects/{type?}
  • Loading branch information
tylersmalley authored May 23, 2017
1 parent 5fc5a70 commit d4be917
Show file tree
Hide file tree
Showing 28 changed files with 1,669 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/server/kbn_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('.');

Expand All @@ -38,8 +39,10 @@ module.exports = class KbnServer {
loggingMixin,
warningsMixin,
statusMixin,

// writes pid file
pidMixin,

// find plugins and set this.plugins
pluginsScanMixin,

Expand All @@ -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,
() => {
Expand Down
248 changes: 248 additions & 0 deletions src/server/saved_objects/client/__tests__/saved_objects_client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import expect from 'expect.js';
import sinon from 'sinon';
import { SavedObjectsClient } from '../saved_objects_client';

describe('SavedObjectsClient', () => {
let callAdminCluster;
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(() => {
callAdminCluster = sinon.mock();
savedObjectsClient = new SavedObjectsClient('.kibana-test', callAdminCluster);
});

afterEach(() => {
callAdminCluster.reset();
});


describe('#create', () => {
it('formats Elasticsearch response', async () => {
callAdminCluster.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 });

const response = await savedObjectsClient.create('index-pattern', {
title: 'Logstash'
});

expect(response).to.eql({
type: 'index-pattern',
id: 'logstash-*',
version: 2,
attributes: {
title: 'Logstash',
}
});
});

it('should use ES create action', async () => {
callAdminCluster.returns({ _type: 'index-pattern', _id: 'logstash-*', _version: 2 });

await savedObjectsClient.create('index-pattern', {
id: 'logstash-*',
title: 'Logstash'
});

expect(callAdminCluster.calledOnce).to.be(true);

const args = callAdminCluster.getCall(0).args;
expect(args[0]).to.be('index');
});
});

describe('#delete', () => {
it('throws notFound when ES is unable to find the document', (done) => {
callAdminCluster.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 callAdminCluster', async () => {
await savedObjectsClient.delete('index-pattern', 'logstash-*');

expect(callAdminCluster.calledOnce).to.be(true);

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',
index: '.kibana-test'
});
});
});

describe('#find', () => {
it('formats Elasticsearch response', async () => {
const count = docs.hits.hits.length;

callAdminCluster.returns(Promise.resolve(docs));
const response = await savedObjectsClient.find();

expect(response.total).to.be(count);
expect(response.saved_objects).to.have.length(count);
docs.hits.hits.forEach((doc, i) => {
expect(response.saved_objects[i]).to.eql({
id: doc._id,
type: doc._type,
version: doc._version,
attributes: doc._source
});
});
});

it('accepts per_page/page', async () => {
await savedObjectsClient.find({ perPage: 10, page: 6 });

expect(callAdminCluster.calledOnce).to.be(true);

const options = callAdminCluster.getCall(0).args[1];
expect(options.size).to.be(10);
expect(options.from).to.be(50);
});

it('accepts type', async () => {
await savedObjectsClient.find({ type: 'index-pattern' });

expect(callAdminCluster.calledOnce).to.be(true);

const options = callAdminCluster.getCall(0).args[1];
const expectedQuery = {
bool: {
must: [{ match_all: {} }],
filter: [{ term: { _type: 'index-pattern' } }]
}
};

expect(options.body).to.eql({
query: expectedQuery, version: true
});
});

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._source).to.eql('title');
});
});

describe('#get', () => {
it('formats Elasticsearch response', async () => {
callAdminCluster.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',
version: 2,
attributes: {
title: 'Testing'
}
});
});
});

describe('#update', () => {
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: id,
_type: type,
_version: version,
result: 'updated'
}));

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 () => {
await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' });

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',
id: 'logstash-*',
version: undefined,
body: { doc: { title: 'Testing' } },
refresh: 'wait_for',
index: '.kibana-test'
});
});
});
});
1 change: 1 addition & 0 deletions src/server/saved_objects/client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SavedObjectsClient } from './saved_objects_client';
82 changes: 82 additions & 0 deletions src/server/saved_objects/client/lib/__tests__/create_find_query.js
Original file line number Diff line number Diff line change
@@ -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
});
});
});
Loading

0 comments on commit d4be917

Please sign in to comment.