Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds an API for managing saved objects #11632

Merged
merged 27 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b250ad9
Adds saved object API
May 15, 2017
621fd48
Fixes typo
May 15, 2017
197a5e4
Remove kibana from saved object API path
May 15, 2017
005b029
Nests document in doc element for ES
May 15, 2017
8b0c32d
Resolves tests for update API
May 15, 2017
f6dd705
Prevent leaking of ES query to API
May 15, 2017
af59a5c
Adds version to saved objects API
May 16, 2017
a8e3279
Return version for searches
May 16, 2017
915c7df
Removes ability to specify id on object creation
May 16, 2017
6c0cfb6
Adds version to update response
May 16, 2017
1835163
Create uses ES index action
May 16, 2017
a2d9877
Allow per_page of 0 for total
May 16, 2017
cbbeff7
Cleans up SavedObjectClient.find and uses camelCases
May 16, 2017
986a73a
Underscores private variables
May 16, 2017
b916a7a
Removes request dependency on SavedObjectClient
May 16, 2017
5fa37cc
Allows for replacement of Promise library for Ng
May 16, 2017
280003d
Use ES source filtering
May 17, 2017
110628e
Seperate attributes
May 18, 2017
6148eac
search_fields should be snake case
May 18, 2017
9eb2efc
$http Response returns data attribute
May 22, 2017
639feb9
Embed object array as saved_objects instead of data
May 22, 2017
bfadd34
Review feedback
May 22, 2017
ef193a2
Removes delete boolean response
May 22, 2017
a397738
Merge remote-tracking branch 'upstream/master' into saved-objects-api
May 22, 2017
d44aae1
Renames data attribute to body
May 23, 2017
5a152da
Merge remote-tracking branch 'upstream/master' into saved-objects-api
May 23, 2017
4454f85
Restores savedDashboardRegister
May 23, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
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
255 changes: 255 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,255 @@
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('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 }));

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.data).to.have.length(count);
docs.hits.hits.forEach((doc, i) => {
expect(response.data[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