diff --git a/src/server/mappings/kibana_index_mappings_mixin.js b/src/server/mappings/kibana_index_mappings_mixin.js index 5b7d8fb683c58..be0202888d56e 100644 --- a/src/server/mappings/kibana_index_mappings_mixin.js +++ b/src/server/mappings/kibana_index_mappings_mixin.js @@ -29,6 +29,9 @@ const BASE_SAVED_OBJECT_MAPPINGS = { doc: { dynamic: 'strict', properties: { + namespace: { + type: 'keyword' + }, type: { type: 'keyword' }, diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index 086107953cf37..61ee6d2a5e762 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -54,7 +54,7 @@ export function savedObjectsMixin(kbnServer, server) { server.route(createGetRoute(prereqs)); server.route(createUpdateRoute(prereqs)); - server.decorate('server', 'savedObjects', createSavedObjectsService(server)); + server.decorate('server', 'savedObjects', createSavedObjectsService(server, kbnServer.uiExports.savedObjectsSchema)); const savedObjectsClientCache = new WeakMap(); server.decorate('request', 'getSavedObjectsClient', function () { diff --git a/src/server/saved_objects/service/create_saved_objects_service.js b/src/server/saved_objects/service/create_saved_objects_service.js index edb6b0093b0ba..35e2d385d435a 100644 --- a/src/server/saved_objects/service/create_saved_objects_service.js +++ b/src/server/saved_objects/service/create_saved_objects_service.js @@ -17,11 +17,11 @@ * under the License. */ -import { getRootPropertiesObjects } from '../..//mappings'; -import { SavedObjectsRepository, ScopedSavedObjectsClientProvider, SavedObjectsRepositoryProvider } from './lib'; +import { getRootPropertiesObjects } from '../../mappings'; +import { SavedObjectsRepository, ScopedSavedObjectsClientProvider, SavedObjectsRepositoryProvider, SavedObjectsSchema } from './lib'; import { SavedObjectsClient } from './saved_objects_client'; -export function createSavedObjectsService(server) { +export function createSavedObjectsService(server, uiExportsSchema) { const onBeforeWrite = async () => { const adminCluster = server.plugins.elasticsearch.getCluster('admin'); @@ -59,10 +59,13 @@ export function createSavedObjectsService(server) { } }; + const schema = new SavedObjectsSchema(uiExportsSchema); + const mappings = server.getKibanaIndexMappingsDsl(); const repositoryProvider = new SavedObjectsRepositoryProvider({ index: server.config().get('kibana.index'), mappings, + schema, onBeforeWrite, }); @@ -86,6 +89,7 @@ export function createSavedObjectsService(server) { types: Object.keys(getRootPropertiesObjects(mappings)), SavedObjectsClient, SavedObjectsRepository, + schema, getSavedObjectsRepository: (...args) => repositoryProvider.getRepository(...args), getScopedSavedObjectsClient: (...args) => diff --git a/src/server/saved_objects/service/lib/index.js b/src/server/saved_objects/service/lib/index.js index 30adefe7e47c1..e773275b9dfa1 100644 --- a/src/server/saved_objects/service/lib/index.js +++ b/src/server/saved_objects/service/lib/index.js @@ -20,6 +20,7 @@ export { SavedObjectsRepository } from './repository'; export { ScopedSavedObjectsClientProvider } from './scoped_client_provider'; export { SavedObjectsRepositoryProvider } from './repository_provider'; +export { SavedObjectsSchema } from './schema'; import * as errors from './errors'; export { errors }; diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 6ede707c1819a..d7fa6ac9e5188 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -35,12 +35,14 @@ export class SavedObjectsRepository { const { index, mappings, + schema, callCluster, onBeforeWrite = () => { }, } = options; this._index = index; this._mappings = mappings; + this._schema = schema; this._type = getRootType(this._mappings); this._onBeforeWrite = onBeforeWrite; this._unwrappedCallCluster = callCluster; @@ -54,14 +56,14 @@ export class SavedObjectsRepository { * @param {object} [options={}] * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] - * @property {object} [options.extraDocumentProperties={}] - extra properties to append to the document body, outside of the object's type property + * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { const { id, - extraDocumentProperties = {}, - overwrite = false + overwrite = false, + namespace, } = options; const method = id && !overwrite ? 'create' : 'index'; @@ -69,12 +71,12 @@ export class SavedObjectsRepository { try { const response = await this._writeToCluster(method, { - id: this._generateEsId(type, id), + id: this._generateEsId(namespace, type, id), type: this._type, index: this._index, refresh: 'wait_for', body: { - ...extraDocumentProperties, + ...namespace && !this._schema.isNamespaceAgnostic(type) && { namespace }, type, updated_at: time, [type]: attributes, @@ -82,7 +84,7 @@ export class SavedObjectsRepository { }); return { - id: trimIdPrefix(response._id, type), + id: trimIdPrefix(this._schema, response._id, namespace, type), type, updated_at: time, version: response._version, @@ -101,14 +103,16 @@ export class SavedObjectsRepository { /** * Creates multiple documents at once * - * @param {array} objects - [{ type, id, attributes, extraDocumentProperties }] + * @param {array} objects - [{ type, id, attributes }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - overwrites existing documents + * @property {string} [options.namespace] * @returns {promise} - {saved_objects: [[{ id, type, version, attributes, error: { message } }]} */ async bulkCreate(objects, options = {}) { const { - overwrite = false + overwrite = false, + namespace } = options; const time = this._getCurrentTime(); const objectToBulkRequest = (object) => { @@ -117,12 +121,12 @@ export class SavedObjectsRepository { return [ { [method]: { - _id: this._generateEsId(object.type, object.id), + _id: this._generateEsId(namespace, object.type, object.id), _type: this._type, } }, { - ...object.extraDocumentProperties, + ... namespace && !this._schema.isNamespaceAgnostic(object.type) && { namespace }, type: object.type, updated_at: time, [object.type]: object.attributes, @@ -186,11 +190,17 @@ export class SavedObjectsRepository { * * @param {string} type * @param {string} id + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} */ - async delete(type, id) { + async delete(type, id, options = {}) { + const { + namespace + } = options; + const response = await this._writeToCluster('delete', { - id: this._generateEsId(type, id), + id: this._generateEsId(namespace, type, id), type: this._type, index: this._index, refresh: 'wait_for', @@ -220,12 +230,12 @@ export class SavedObjectsRepository { * @property {string} [options.search] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information - * @property {object} [options.filters] - ES Query filters to append * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] * @property {string} [options.sortField] * @property {string} [options.sortOrder] * @property {Array} [options.fields] + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ async find(options = {}) { @@ -238,7 +248,7 @@ export class SavedObjectsRepository { sortField, sortOrder, fields, - filters, + namespace, } = options; if (searchFields && !Array.isArray(searchFields)) { @@ -249,10 +259,6 @@ export class SavedObjectsRepository { throw new TypeError('options.searchFields must be an array'); } - if (filters && !Array.isArray(filters)) { - throw new TypeError('options.filters must be an array'); - } - const esOptions = { index: this._index, size: perPage, @@ -261,13 +267,13 @@ export class SavedObjectsRepository { ignore: [404], body: { version: true, - ...getSearchDsl(this._mappings, { + ...getSearchDsl(this._mappings, this._schema, { + namespace, search, searchFields, type, sortField, sortOrder, - filters }) } }; @@ -292,7 +298,7 @@ export class SavedObjectsRepository { saved_objects: response.hits.hits.map(hit => { const { type, updated_at: updatedAt } = hit._source; return { - id: trimIdPrefix(hit._id, type), + id: trimIdPrefix(this._schema, hit._id, namespace, type), type, ...updatedAt && { updated_at: updatedAt }, version: hit._version, @@ -306,8 +312,8 @@ export class SavedObjectsRepository { * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @param {object} [options = {}] - * @param {array} [options.extraDocumentProperties = []] - an array of extra properties to return from the underlying document + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -317,6 +323,10 @@ export class SavedObjectsRepository { * ]) */ async bulkGet(objects = [], options = {}) { + const { + namespace + } = options; + if (objects.length === 0) { return { saved_objects: [] }; } @@ -325,7 +335,7 @@ export class SavedObjectsRepository { index: this._index, body: { docs: objects.map(object => ({ - _id: this._generateEsId(object.type, object.id), + _id: this._generateEsId(namespace, object.type, object.id), _type: this._type, })) } @@ -333,8 +343,6 @@ export class SavedObjectsRepository { const { docs } = response; - const { extraDocumentProperties = [] } = options; - return { saved_objects: docs.map((doc, i) => { const { id, type } = objects[i]; @@ -353,9 +361,6 @@ export class SavedObjectsRepository { type, ...time && { updated_at: time }, version: doc._version, - ...extraDocumentProperties - .map(s => ({ [s]: doc._source[s] })) - .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { ...doc._source[type], } @@ -371,13 +376,17 @@ export class SavedObjectsRepository { * * @param {string} type * @param {string} id - * @param {object} [options = {}] - * @param {array} [options.extraDocumentProperties = []] - an array of extra properties to return from the underlying document + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { + const { + namespace + } = options; + const response = await this._callCluster('get', { - id: this._generateEsId(type, id), + id: this._generateEsId(namespace, type, id), type: this._type, index: this._index, ignore: [404] @@ -390,8 +399,6 @@ export class SavedObjectsRepository { throw errors.createGenericNotFoundError(type, id); } - const { extraDocumentProperties = [] } = options; - const { updated_at: updatedAt } = response._source; return { @@ -399,9 +406,6 @@ export class SavedObjectsRepository { type, ...updatedAt && { updated_at: updatedAt }, version: response._version, - ...extraDocumentProperties - .map(s => ({ [s]: response._source[s] })) - .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { ...response._source[type], } @@ -415,21 +419,26 @@ export class SavedObjectsRepository { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object - * @param {array} [options.extraDocumentProperties = {}] - an object of extra properties to write into the underlying document + * @property {string} [options.namespace] * @returns {promise} */ async update(type, id, attributes, options = {}) { + const { + version, + namespace + } = options; + const time = this._getCurrentTime(); const response = await this._writeToCluster('update', { - id: this._generateEsId(type, id), + id: this._generateEsId(namespace, type, id), type: this._type, index: this._index, - version: options.version, + version, refresh: 'wait_for', ignore: [404], body: { doc: { - ...options.extraDocumentProperties, + ...namespace && !this._schema.isNamespaceAgnostic(type) && { namespace }, updated_at: time, [type]: attributes, } @@ -467,8 +476,9 @@ export class SavedObjectsRepository { } } - _generateEsId(type, id) { - return `${type}:${id || uuid.v1()}`; + _generateEsId(namespace, type, id) { + const namespacePrefix = namespace && !this._schema.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + return `${namespacePrefix}${type}:${id || uuid.v1()}`; } _getCurrentTime() { diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index 2b84e773e0276..8ed28ffa1b6b8 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -36,9 +36,9 @@ describe('SavedObjectsRepository', () => { let savedObjectsRepository; const mockTimestamp = '2017-08-14T15:49:14.886Z'; const mockTimestampFields = { updated_at: mockTimestamp }; - const searchResults = { + const noNamespaceSearchResults = { hits: { - total: 3, + total: 4, hits: [{ _index: '.kibana', _type: 'doc', @@ -80,6 +80,81 @@ describe('SavedObjectsRepository', () => { notExpandable: true } } + }, { + _index: '.kibana', + _type: 'doc', + _id: 'no-ns-type:something', + _score: 1, + _source: { + type: 'no-ns-type', + ...mockTimestampFields, + 'no-ns-type': { + name: 'bar', + } + } + }] + } + }; + + const namespacedSearchResults = { + hits: { + total: 4, + hits: [{ + _index: '.kibana', + _type: 'doc', + _id: 'foo-namespace:index-pattern:logstash-*', + _score: 1, + _source: { + namespace: 'foo-namespace', + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true + } + } + }, { + _index: '.kibana', + _type: 'doc', + _id: 'foo-namespace:config:6.0.0-alpha1', + _score: 1, + _source: { + namespace: 'foo-namespace', + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8467, + defaultIndex: 'logstash-*' + } + } + }, { + _index: '.kibana', + _type: 'doc', + _id: 'foo-namespace:index-pattern:stocks-*', + _score: 1, + _source: { + namespace: 'foo-namespace', + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'stocks-*', + timeFieldName: '@timestamp', + notExpandable: true + } + } + }, { + _index: '.kibana', + _type: 'doc', + _id: 'no-ns-type:something', + _score: 1, + _source: { + type: 'no-ns-type', + ...mockTimestampFields, + 'no-ns-type': { + name: 'bar', + } + } }] } }; @@ -98,6 +173,10 @@ describe('SavedObjectsRepository', () => { } }; + const schema = { + isNamespaceAgnostic: type => type === 'no-ns-type', + }; + beforeEach(() => { callAdminCluster = sandbox.stub(); onBeforeWrite = sandbox.stub(); @@ -105,6 +184,7 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', mappings, + schema, callCluster: callAdminCluster, onBeforeWrite }); @@ -120,17 +200,20 @@ describe('SavedObjectsRepository', () => { describe('#create', () => { beforeEach(() => { - callAdminCluster.returns(Promise.resolve({ + callAdminCluster.callsFake((method, params) => ({ _type: 'doc', - _id: 'index-pattern:logstash-*', + _id: params.id, _version: 2 })); }); it('formats Elasticsearch response', async () => { - const response = await savedObjectsRepository.create('index-pattern', { - title: 'Logstash' - }); + const response = await savedObjectsRepository.create('index-pattern', + { + title: 'Logstash' + }, { + id: 'logstash-*' + }); expect(response).toEqual({ type: 'index-pattern', @@ -193,25 +276,23 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('appends extraDocumentProperties to the document', async () => { + it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { await savedObjectsRepository.create('index-pattern', { title: 'Logstash' }, { - extraDocumentProperties: { - myExtraProp: 'myExtraValue', - myOtherExtraProp: true, - } - } + id: 'foo-id', + namespace: 'foo-namespace', + }, ); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + id: `foo-namespace:index-pattern:foo-id`, body: { [`index-pattern`]: { title: 'Logstash' }, - myExtraProp: 'myExtraValue', - myOtherExtraProp: true, + namespace: 'foo-namespace', type: 'index-pattern', updated_at: '2017-08-14T15:49:14.886Z' } @@ -220,29 +301,21 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('does not allow extraDocumentProperties to overwrite existing properties', async () => { + it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { await savedObjectsRepository.create('index-pattern', { title: 'Logstash' }, { - extraDocumentProperties: { - myExtraProp: 'myExtraValue', - myOtherExtraProp: true, - updated_at: 'should_not_be_used', - 'index-pattern': { - title: 'should_not_be_used' - } - } + id: 'foo-id' } ); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + id: `index-pattern:foo-id`, body: { [`index-pattern`]: { title: 'Logstash' }, - myExtraProp: 'myExtraValue', - myOtherExtraProp: true, type: 'index-pattern', updated_at: '2017-08-14T15:49:14.886Z' } @@ -250,6 +323,30 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); + + it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { + await savedObjectsRepository.create('no-ns-type', + { + title: 'Logstash' + }, + { + id: 'foo-id', + namespace: 'foo-namespace', + } + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + id: `no-ns-type:foo-id`, + body: { + [`no-ns-type`]: { title: 'Logstash' }, + type: 'no-ns-type', + updated_at: '2017-08-14T15:49:14.886Z' + } + })); + + sinon.assert.calledOnce(onBeforeWrite); + }); }); describe('#bulkCreate', () => { @@ -389,67 +486,70 @@ describe('SavedObjectsRepository', () => { }); }); - it('appends extraDocumentProperties to each created object', async () => { + it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { callAdminCluster.returns({ items: [] }); await savedObjectsRepository.bulkCreate( [ - { type: 'config', id: 'one', attributes: { title: 'Test One' }, extraDocumentProperties: { extraConfigValue: true } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, extraDocumentProperties: { extraIndexValue: true } } - ]); + { type: 'config', id: 'one', attributes: { title: 'Test One' } }, + { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } } + ], + { + namespace: 'foo-namespace', + }, + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({ + body: [ + { create: { _type: 'doc', _id: 'foo-namespace:config:one' } }, + { namespace: 'foo-namespace', type: 'config', ...mockTimestampFields, config: { title: 'Test One' } }, + { create: { _type: 'doc', _id: 'foo-namespace:index-pattern:two' } }, + { namespace: 'foo-namespace', type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } } + ] + })); + + sinon.assert.calledOnce(onBeforeWrite); + }); + + it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { + callAdminCluster.returns({ items: [] }); + + await savedObjectsRepository.bulkCreate([ + { type: 'config', id: 'one', attributes: { title: 'Test One' } }, + { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } } + ]); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({ body: [ { create: { _type: 'doc', _id: 'config:one' } }, - { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, extraConfigValue: true }, + { type: 'config', ...mockTimestampFields, config: { title: 'Test One' } }, { create: { _type: 'doc', _id: 'index-pattern:two' } }, - { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' }, extraIndexValue: true } + { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } } ] })); sinon.assert.calledOnce(onBeforeWrite); }); - it('does not allow extraDocumentProperties to overwrite existing properties', async () => { - + it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { callAdminCluster.returns({ items: [] }); - const extraDocumentProperties = { - extraProp: 'extraVal', - updated_at: 'should_not_be_used', - }; - const configExtraDocumentProperties = { - ...extraDocumentProperties, - 'config': { newIgnoredProp: 'should_not_be_used' } - }; - const indexPatternExtraDocumentProperties = { - ...extraDocumentProperties, - 'index-pattern': { title: 'should_not_be_used', newIgnoredProp: 'should_not_be_used' } - }; - await savedObjectsRepository.bulkCreate( - [{ - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - extraDocumentProperties: configExtraDocumentProperties - }, + [ + { type: 'no-ns-type', id: 'one', attributes: { title: 'Test One' } }, + ], { - type: 'index-pattern', - id: 'two', - attributes: { title: 'Test Two' }, - extraDocumentProperties: indexPatternExtraDocumentProperties - }] + namespace: 'foo-namespace', + }, ); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({ body: [ - { create: { _type: 'doc', _id: 'config:one' } }, - { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, extraProp: 'extraVal' }, - { create: { _type: 'doc', _id: 'index-pattern:two' } }, - { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' }, extraProp: 'extraVal' } + { create: { _type: 'doc', _id: 'no-ns-type:one' } }, + { type: 'no-ns-type', ...mockTimestampFields, 'no-ns-type': { title: 'Test One' } }, ] })); @@ -472,7 +572,27 @@ describe('SavedObjectsRepository', () => { } }); - it('passes the parameters to callAdminCluster', async () => { + it(`prepends namespace to the id when providing namespace for namespaced type`, async () => { + callAdminCluster.returns({ + result: 'deleted' + }); + await savedObjectsRepository.delete('index-pattern', 'logstash-*', { + namespace: 'foo-namespace', + }); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'delete', { + type: 'doc', + id: 'foo-namespace:index-pattern:logstash-*', + refresh: 'wait_for', + index: '.kibana-test', + ignore: [404], + }); + + sinon.assert.calledOnce(onBeforeWrite); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for namespaced type`, async () => { callAdminCluster.returns({ result: 'deleted' }); @@ -489,14 +609,31 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - }); - describe('#find', () => { - beforeEach(() => { - callAdminCluster.returns(searchResults); + it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { + callAdminCluster.returns({ + result: 'deleted' + }); + await savedObjectsRepository.delete('no-ns-type', 'logstash-*', { + namespace: 'foo-namespace', + }); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'delete', { + type: 'doc', + id: 'no-ns-type:logstash-*', + refresh: 'wait_for', + index: '.kibana-test', + ignore: [404], + }); + + sinon.assert.calledOnce(onBeforeWrite); }); + }); + describe('#find', () => { it('requires searchFields be an array if defined', async () => { + callAdminCluster.returns(noNamespaceSearchResults); try { await savedObjectsRepository.find({ searchFields: 'string' }); throw new Error('expected find() to reject'); @@ -508,6 +645,7 @@ describe('SavedObjectsRepository', () => { }); it('requires fields be an array if defined', async () => { + callAdminCluster.returns(noNamespaceSearchResults); try { await savedObjectsRepository.find({ fields: 'string' }); throw new Error('expected find() to reject'); @@ -518,33 +656,24 @@ describe('SavedObjectsRepository', () => { } }); - it('requires filters to be an array if defined', async () => { - try { - await savedObjectsRepository.find({ filters: 'string' }); - throw new Error('expected find() to reject'); - } catch (error) { - sinon.assert.notCalled(callAdminCluster); - sinon.assert.notCalled(onBeforeWrite); - expect(error.message).toMatch('must be an array'); - } - }); - - it('passes mappings, search, searchFields, type, sortField, filters, and sortOrder to getSearchDsl', async () => { + it('passes mappings, schema, namespace, search, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => { + callAdminCluster.returns(namespacedSearchResults); const relevantOpts = { + namespace: 'foo-namespace', search: 'foo*', searchFields: ['foo'], type: 'bar', sortField: 'name', sortOrder: 'desc', - filters: [{ bool: {} }], }; await savedObjectsRepository.find(relevantOpts); sinon.assert.calledOnce(getSearchDsl); - sinon.assert.calledWithExactly(getSearchDsl, mappings, relevantOpts); + sinon.assert.calledWithExactly(getSearchDsl, mappings, schema, relevantOpts); }); it('merges output of getSearchDsl into es request body', async () => { + callAdminCluster.returns(noNamespaceSearchResults); getSearchDsl.returns({ query: 1, aggregations: 2 }); await savedObjectsRepository.find(); sinon.assert.calledOnce(callAdminCluster); @@ -557,17 +686,40 @@ describe('SavedObjectsRepository', () => { })); }); - it('formats Elasticsearch response', async () => { - const count = searchResults.hits.hits.length; + it('formats Elasticsearch response when there is no namespace', async () => { + callAdminCluster.returns(noNamespaceSearchResults); + const count = noNamespaceSearchResults.hits.hits.length; const response = await savedObjectsRepository.find(); expect(response.total).toBe(count); expect(response.saved_objects).toHaveLength(count); - searchResults.hits.hits.forEach((doc, i) => { + noNamespaceSearchResults.hits.hits.forEach((doc, i) => { expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(index-pattern|config)\:/, ''), + id: doc._id.replace(/(index-pattern|config|no-ns-type)\:/, ''), + type: doc._source.type, + ...mockTimestampFields, + version: doc._version, + attributes: doc._source[doc._source.type] + }); + }); + }); + + it('formats Elasticsearch response when there is a namespace', async () => { + callAdminCluster.returns(namespacedSearchResults); + const count = namespacedSearchResults.hits.hits.length; + + const response = await savedObjectsRepository.find({ + namespace: 'foo-namespace', + }); + + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); + + namespacedSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|no-ns-type)\:/, ''), type: doc._source.type, ...mockTimestampFields, version: doc._version, @@ -577,6 +729,7 @@ describe('SavedObjectsRepository', () => { }); it('accepts per_page/page', async () => { + callAdminCluster.returns(noNamespaceSearchResults); await savedObjectsRepository.find({ perPage: 10, page: 6 }); sinon.assert.calledOnce(callAdminCluster); @@ -589,6 +742,7 @@ describe('SavedObjectsRepository', () => { }); it('can filter by fields', async () => { + callAdminCluster.returns(noNamespaceSearchResults); await savedObjectsRepository.find({ fields: ['title'] }); sinon.assert.calledOnce(callAdminCluster); @@ -603,24 +757,37 @@ describe('SavedObjectsRepository', () => { }); describe('#get', () => { - beforeEach(() => { - callAdminCluster.returns(Promise.resolve({ - _id: 'index-pattern:logstash-*', - _type: 'doc', - _version: 2, - _source: { - type: 'index-pattern', - specialProperty: 'specialValue', - ...mockTimestampFields, - 'index-pattern': { - title: 'Testing' - } + const noNamespaceResult = { + _id: 'index-pattern:logstash-*', + _type: 'doc', + _version: 2, + _source: { + type: 'index-pattern', + specialProperty: 'specialValue', + ...mockTimestampFields, + 'index-pattern': { + title: 'Testing' } - })); - }); + } + }; + const namespacedResult = { + _id: 'foo-namespace:index-pattern:logstash-*', + _type: 'doc', + _version: 2, + _source: { + namespace: 'foo-namespace', + type: 'index-pattern', + specialProperty: 'specialValue', + ...mockTimestampFields, + 'index-pattern': { + title: 'Testing' + } + } + }; it('formats Elasticsearch response', async () => { - const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); + callAdminCluster.returns(Promise.resolve(namespacedResult)); + const response = await savedObjectsRepository.get('index-pattern', 'logstash-*', 'foo-namespace'); sinon.assert.notCalled(onBeforeWrite); expect(response).toEqual({ id: 'logstash-*', @@ -633,7 +800,22 @@ describe('SavedObjectsRepository', () => { }); }); - it('prepends type to the id', async () => { + it('prepends namespace and type to the id when providing namespace for namespaced type', async () => { + callAdminCluster.returns(Promise.resolve(namespacedResult)); + await savedObjectsRepository.get('index-pattern', 'logstash-*', { + namespace: 'foo-namespace', + }); + + sinon.assert.notCalled(onBeforeWrite); + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + id: 'foo-namespace:index-pattern:logstash-*', + type: 'doc' + })); + }); + + it(`only prepends type to the id when providing no namespace for namespaced type`, async () => { + callAdminCluster.returns(Promise.resolve(noNamespaceResult)); await savedObjectsRepository.get('index-pattern', 'logstash-*'); sinon.assert.notCalled(onBeforeWrite); @@ -644,32 +826,29 @@ describe('SavedObjectsRepository', () => { })); }); - it('includes the requested extraDocumentProperties in the response for the requested object', async () => { - const response = await savedObjectsRepository.get('index-pattern', 'logstash-*', { - extraDocumentProperties: ['specialProperty', 'undefinedProperty'] + it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { + callAdminCluster.returns(Promise.resolve(namespacedResult)); + await savedObjectsRepository.get('no-ns-type', 'logstash-*', { + namespace: 'foo-namespace', }); - expect(response).toEqual({ - id: 'logstash-*', - type: 'index-pattern', - updated_at: mockTimestamp, - version: 2, - specialProperty: 'specialValue', - undefinedProperty: undefined, - attributes: { - title: 'Testing' - } - }); + sinon.assert.notCalled(onBeforeWrite); + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + id: 'no-ns-type:logstash-*', + type: 'doc' + })); }); }); describe('#bulkGet', () => { - it('accepts a array of mixed type and ids', async () => { + it('prepends type to id when getting objects when there is no namespace', async () => { callAdminCluster.returns({ docs: [] }); await savedObjectsRepository.bulkGet([ { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' } + { id: 'two', type: 'index-pattern' }, + { id: 'three', type: 'no-ns-type' }, ]); sinon.assert.calledOnce(callAdminCluster); @@ -677,7 +856,35 @@ describe('SavedObjectsRepository', () => { body: { docs: [ { _type: 'doc', _id: 'config:one' }, - { _type: 'doc', _id: 'index-pattern:two' } + { _type: 'doc', _id: 'index-pattern:two' }, + { _type: 'doc', _id: 'no-ns-type:three' }, + ] + } + })); + + sinon.assert.notCalled(onBeforeWrite); + }); + + it('prepends namespace and type appropriately to id when getting objects when there is a namespace', async () => { + callAdminCluster.returns({ docs: [] }); + + await savedObjectsRepository.bulkGet( + [ + { id: 'one', type: 'config' }, + { id: 'two', type: 'index-pattern' }, + { id: 'three', type: 'no-ns-type' }, + ], { + namespace: 'foo-namespace', + } + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + body: { + docs: [ + { _type: 'doc', _id: 'foo-namespace:config:one' }, + { _type: 'doc', _id: 'foo-namespace:index-pattern:two' }, + { _type: 'doc', _id: 'no-ns-type:three' }, ] } })); @@ -731,80 +938,6 @@ describe('SavedObjectsRepository', () => { error: { statusCode: 404, message: 'Not found' } }); }); - - it('includes the requested extraDocumentProperties in the response for each requested object', async () => { - callAdminCluster.returns(Promise.resolve({ - docs: [{ - _type: 'doc', - _id: 'config:good', - found: true, - _version: 2, - _source: { - ...mockTimestampFields, - type: 'config', - specialProperty: 'specialValue', - config: { title: 'Test' } - } - }, { - _type: 'doc', - _id: 'config:bad', - found: false - }, { - _id: 'index-pattern:logstash-*', - _type: 'doc', - found: true, - _version: 2, - _source: { - type: 'index-pattern', - specialProperty: 'anotherSpecialValue', - ...mockTimestampFields, - 'index-pattern': { - title: 'Testing' - } - } - }] - })); - - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet( - [ - { id: 'good', type: 'config' }, - { id: 'bad', type: 'config' }, - { id: 'logstash-*', type: 'index-pattern' } - ], { - extraDocumentProperties: ['specialProperty', 'undefinedProperty'] - } - ); - - expect(savedObjects).toHaveLength(3); - - expect(savedObjects[0]).toEqual({ - id: 'good', - type: 'config', - ...mockTimestampFields, - version: 2, - specialProperty: 'specialValue', - undefinedProperty: undefined, - attributes: { title: 'Test' } - }); - - expect(savedObjects[1]).toEqual({ - id: 'bad', - type: 'config', - error: { statusCode: 404, message: 'Not found' } - }); - - expect(savedObjects[2]).toEqual({ - id: 'logstash-*', - type: 'index-pattern', - ...mockTimestampFields, - version: 2, - specialProperty: 'anotherSpecialValue', - undefinedProperty: undefined, - attributes: { - title: 'Testing' - } - }); - }); }); describe('#update', () => { @@ -847,16 +980,20 @@ describe('SavedObjectsRepository', () => { })); }); - it('passes the parameters to callAdminCluster', async () => { - await savedObjectsRepository.update('index-pattern', 'logstash-*', { title: 'Testing' }); + it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { + await savedObjectsRepository.update('index-pattern', 'logstash-*', { + title: 'Testing', + }, { + namespace: 'foo-namespace', + }); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, 'update', { type: 'doc', - id: 'index-pattern:logstash-*', + id: 'foo-namespace:index-pattern:logstash-*', version: undefined, body: { - doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } } + doc: { namespace: 'foo-namespace', updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } } }, ignore: [404], refresh: 'wait_for', @@ -866,13 +1003,8 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('updates the document including all provided extraDocumentProperties', async () => { - await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - { title: 'Testing' }, - { extraDocumentProperties: { extraProp: 'extraVal' } } - ); + it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { + await savedObjectsRepository.update('index-pattern', 'logstash-*', { title: 'Testing' }); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, 'update', { @@ -880,7 +1012,7 @@ describe('SavedObjectsRepository', () => { id: 'index-pattern:logstash-*', version: undefined, body: { - doc: { updated_at: mockTimestamp, extraProp: 'extraVal', 'index-pattern': { title: 'Testing' } } + doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } } }, ignore: [404], refresh: 'wait_for', @@ -890,27 +1022,20 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('does not allow extraDocumentProperties to overwrite existing properties', async () => { - await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - { title: 'Testing' }, - { - extraDocumentProperties: { - extraProp: 'extraVal', - updated_at: 'should_not_be_used', - 'index-pattern': { title: 'should_not_be_used', newIgnoredProp: 'should_not_be_used' } - } - } - ); + it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { + await savedObjectsRepository.update('no-ns-type', 'foo', { + name: 'bar', + }, { + namespace: 'foo-namespace', + }); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, 'update', { type: 'doc', - id: 'index-pattern:logstash-*', + id: 'no-ns-type:foo', version: undefined, body: { - doc: { updated_at: mockTimestamp, extraProp: 'extraVal', 'index-pattern': { title: 'Testing' } } + doc: { updated_at: mockTimestamp, 'no-ns-type': { name: 'bar' } } }, ignore: [404], refresh: 'wait_for', diff --git a/src/server/saved_objects/service/lib/repository_provider.js b/src/server/saved_objects/service/lib/repository_provider.js index 131eefd619a25..eabe1be09fed6 100644 --- a/src/server/saved_objects/service/lib/repository_provider.js +++ b/src/server/saved_objects/service/lib/repository_provider.js @@ -27,10 +27,12 @@ export class SavedObjectsRepositoryProvider { constructor({ index, mappings, + schema, onBeforeWrite }) { this._index = index; this._mappings = mappings; + this._schema = schema; this._onBeforeWrite = onBeforeWrite; } @@ -43,6 +45,7 @@ export class SavedObjectsRepositoryProvider { return new SavedObjectsRepository({ index: this._index, mappings: this._mappings, + schema: this._schema, onBeforeWrite: this._onBeforeWrite, callCluster }); diff --git a/src/server/saved_objects/service/lib/repository_provider.test.js b/src/server/saved_objects/service/lib/repository_provider.test.js index bc39fcecf9384..41a8f72f2e643 100644 --- a/src/server/saved_objects/service/lib/repository_provider.test.js +++ b/src/server/saved_objects/service/lib/repository_provider.test.js @@ -41,22 +41,26 @@ test('creates a valid Repository', async () => { } } }, + schema: { + isNamespaceAgnostic: jest.fn(), + }, onBeforeWrite: jest.fn() }; const provider = new SavedObjectsRepositoryProvider(properties); const callCluster = jest.fn().mockReturnValue({ - _id: 'new' + _id: 'ns:foo:new' }); const repository = provider.getRepository(callCluster); - await repository.create('foo', {}); + await repository.create('foo', {}, { namespace: 'ns' }); expect(callCluster).toHaveBeenCalledTimes(1); + expect(properties.schema.isNamespaceAgnostic).toHaveBeenCalled(); expect(properties.onBeforeWrite).toHaveBeenCalledTimes(1); expect(callCluster).toHaveBeenCalledWith('index', expect.objectContaining({ index: properties.index })); -}); \ No newline at end of file +}); diff --git a/src/server/saved_objects/service/lib/schema.js b/src/server/saved_objects/service/lib/schema.js new file mode 100644 index 0000000000000..02cbbc4271c6c --- /dev/null +++ b/src/server/saved_objects/service/lib/schema.js @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class SavedObjectsSchema { + constructor(uiExportsSchema) { + this._schema = uiExportsSchema; + } + + isNamespaceAgnostic(type) { + const typeSchema = this._schema[type]; + if (!typeSchema) { + return false; + } + return Boolean(typeSchema.isNamespaceAgnostic); + } +} diff --git a/src/server/saved_objects/service/lib/schema.test.js b/src/server/saved_objects/service/lib/schema.test.js new file mode 100644 index 0000000000000..a524c4bb6bd84 --- /dev/null +++ b/src/server/saved_objects/service/lib/schema.test.js @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsSchema } from './schema'; + +describe('#isNamespaceAgnostic', () => { + it(`returns false for unknown types`, () => { + const schema = new SavedObjectsSchema({}); + const result = schema.isNamespaceAgnostic('bar'); + expect(result).toBe(false); + }); + + it(`returns true for explicitly namespace agnostic type`, () => { + const schema = new SavedObjectsSchema({ + foo: { + isNamespaceAgnostic: true, + } + }); + const result = schema.isNamespaceAgnostic('foo'); + expect(result).toBe(true); + }); + + it(`returns false for explicitly namespaced type`, () => { + const schema = new SavedObjectsSchema({ + foo: { + isNamespaceAgnostic: false, + } + }); + const result = schema.isNamespaceAgnostic('foo'); + expect(result).toBe(false); + }); +}); + diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js index c5ca55f985bdd..5bcff743c1082 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js @@ -59,25 +59,58 @@ function getFieldsForTypes(searchFields, types) { }; } +/** + * Gets the clause that will filter for the type in the namespace. + * Some types are namespace agnostic, so they must be treated differently. + * @param {SavedObjectsSchema} schema + * @param {string} namespace + * @param {string} type + * @return {Object} + */ +function getClauseForType(schema, namespace, type) { + if (!type) { + throw new Error(`type is required to build filter clause`); + } + + if (namespace && !schema.isNamespaceAgnostic(type)) { + return { + bool: { + must: [ + { term: { type } }, + { term: { namespace } }, + ] + } + }; + } + + return { + bool: { + must: [{ term: { type } }], + must_not: [{ exists: { field: 'namespace' } }] + } + }; +} + /** * Get the "query" related keys for the search body * @param {EsMapping} mapping mappings from Ui + * *@param {SavedObjectsSchema} schema * @param {(string|Array)} type * @param {String} search * @param {Array} searchFields - * @param {Array} filters additional query filters * @return {Object} */ -export function getQueryParams(mappings, type, search, searchFields, filters = []) { - +export function getQueryParams(mappings, schema, namespace, type, search, searchFields) { + const types = getTypes(mappings, type); const bool = { - filter: [...filters], + filter: [{ + bool: { + should: types.map(type => getClauseForType(schema, namespace, type)), + minimum_should_match: 1 + } + }], }; - if (type) { - bool.filter.push({ [Array.isArray(type) ? 'terms' : 'term']: { type } }); - } - if (search) { bool.must = [ { @@ -85,19 +118,12 @@ export function getQueryParams(mappings, type, search, searchFields, filters = [ query: search, ...getFieldsForTypes( searchFields, - getTypes(mappings, type) + types ) } } ]; } - // Don't construct a query if there is nothing to search on. - if (bool.filter.length === 0 && !search) { - return {}; - } - return { query: { bool } }; } - - diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js index e3e261db1e77e..45f241ba41883 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js @@ -50,61 +50,189 @@ const MAPPINGS = { } } } + }, + global: { + properties: { + name: { + type: 'keyword', + } + } } } } }; +const SCHEMA = { + isNamespaceAgnostic: type => type === 'global', +}; + + +// create a type clause to be used within the "should", if a namespace is specified +// the clause will ensure the namespace matches; otherwise, the clause will ensure +// that there isn't a namespace field. +const createTypeClause = (type, namespace) => { + if (namespace) { + return { + bool: { + must: [ + { term: { type } }, + { term: { namespace } }, + ] + } + }; + } + + return { + bool: { + must: [{ term: { type } }], + must_not: [{ exists: { field: 'namespace' } }] + } + }; +}; + describe('searchDsl/queryParams', () => { - describe('{}', () => { - it('searches for everything', () => { - expect(getQueryParams(MAPPINGS)) - .toEqual({}); + describe('no parameters', () => { + it('searches for all known types without a namespace specified', () => { + expect(getQueryParams(MAPPINGS, SCHEMA)) + .toEqual({ + query: { + bool: { + filter: [{ + bool: { + should: [ + createTypeClause('pending'), + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }] + } + } + }); }); }); - describe('{type}', () => { - it('includes just a terms filter', () => { - expect(getQueryParams(MAPPINGS, 'saved')) + describe('namespace', () => { + it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace')) .toEqual({ query: { bool: { - filter: [ - { - term: { type: 'saved' } + filter: [{ + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 } - ] + }] } } }); }); }); - describe('{type,filters}', () => { - it('includes filters and a term filter for type', () => { - expect(getQueryParams(MAPPINGS, 'saved', null, null, [{ terms: { foo: ['bar', 'baz'] } }])) + describe('type (singular, namespaced)', () => { + it('includes a terms filter for type and namespace not being specified', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, 'saved')) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - { - term: { type: 'saved' } + filter: [{ + bool: { + should: [ + createTypeClause('saved'), + ], + minimum_should_match: 1 } - ] + }] + } + } + }); + }); + }); + + describe('type (singular, global)', () => { + it('includes a terms filter for type and namespace not being specified', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, 'global')) + .toEqual({ + query: { + bool: { + filter: [{ + bool: { + should: [ + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }] } } }); }); }); - describe('{search}', () => { - it('includes just a sqs query', () => { - expect(getQueryParams(MAPPINGS, null, 'us*')) + describe('type (plural, namespaced and global)', () => { + it('includes term filters for types and namespace not being specified', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, ['saved', 'global'])) .toEqual({ query: { bool: { - filter: [], + filter: [{ + bool: { + should: [ + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }] + } + } + }); + }); + }); + + describe('namespace, type (plural, namespaced and global)', () => { + it('includes a terms filter for type and namespace not being specified', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'])) + .toEqual({ + query: { + bool: { + filter: [{ + bool: { + should: [ + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }] + } + } + }); + }); + }); + + describe('search', () => { + it('includes a sqs query and all known types without a namespace specified', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, null, 'us*')) + .toEqual({ + query: { + bool: { + filter: [{ + bool: { + should: [ + createTypeClause('pending'), + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { @@ -119,15 +247,22 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{search,filters}', () => { - it('includes filters and a sqs query', () => { - expect(getQueryParams(MAPPINGS, null, 'us*', null, [{ terms: { foo: ['bar', 'baz'] } }])) + describe('namespace, search', () => { + it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', null, 'us*')) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { @@ -142,19 +277,25 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{type,search}', () => { - it('includes bool with sqs query and term filter for type', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*')) + describe('type (plural, namespaced and global), search', () => { + it('includes a sqs query and types without a namespace', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, ['saved', 'global'], 'us*')) .toEqual({ query: { bool: { - filter: [ - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { - query: 'y*', + query: 'us*', all_fields: true } } @@ -165,20 +306,25 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{type,search,filters}', () => { - it('includes bool with sqs query, filters and term filter for type', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', null, [{ terms: { foo: ['bar', 'baz'] } }])) + describe('namespace, type (plural, namespaced and global), search', () => { + it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'us*')) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { - query: 'y*', + query: 'us*', all_fields: true } } @@ -189,20 +335,30 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{search,searchFields}', () => { + describe('search, searchFields', () => { it('includes all types for field', () => { - expect(getQueryParams(MAPPINGS, null, 'y*', ['title'])) + expect(getQueryParams(MAPPINGS, SCHEMA, null, null, 'y*', ['title'])) .toEqual({ query: { bool: { - filter: [], + filter: [{ + bool: { + should: [ + createTypeClause('pending'), + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ 'pending.title', - 'saved.title' + 'saved.title', + 'global.title', ] } } @@ -212,18 +368,28 @@ describe('searchDsl/queryParams', () => { }); }); it('supports field boosting', () => { - expect(getQueryParams(MAPPINGS, null, 'y*', ['title^3'])) + expect(getQueryParams(MAPPINGS, SCHEMA, null, null, 'y*', ['title^3'])) .toEqual({ query: { bool: { - filter: [], + filter: [{ + bool: { + should: [ + createTypeClause('pending'), + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ 'pending.title^3', - 'saved.title^3' + 'saved.title^3', + 'global.title^3', ] } } @@ -233,11 +399,20 @@ describe('searchDsl/queryParams', () => { }); }); it('supports field and multi-field', () => { - expect(getQueryParams(MAPPINGS, null, 'y*', ['title', 'title.raw'])) + expect(getQueryParams(MAPPINGS, SCHEMA, null, null, 'y*', ['title', 'title.raw'])) .toEqual({ query: { bool: { - filter: [], + filter: [{ + bool: { + should: [ + createTypeClause('pending'), + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { @@ -245,8 +420,10 @@ describe('searchDsl/queryParams', () => { fields: [ 'pending.title', 'saved.title', + 'global.title', 'pending.title.raw', 'saved.title.raw', + 'global.title.raw', ] } } @@ -257,22 +434,30 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{search,searchFields,filters}', () => { - it('specifies filters and includes all types for field', () => { - expect(getQueryParams(MAPPINGS, null, 'y*', ['title'], [{ terms: { foo: ['bar', 'baz'] } }])) + describe('namespace, search, searchFields', () => { + it('includes all types for field', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', null, 'y*', ['title'])) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - ], + filter: [{ + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ 'pending.title', - 'saved.title' + 'saved.title', + 'global.title', ] } } @@ -281,21 +466,29 @@ describe('searchDsl/queryParams', () => { } }); }); - it('specifies filters and supports field boosting', () => { - expect(getQueryParams(MAPPINGS, null, 'y*', ['title^3'], [{ terms: { foo: ['bar', 'baz'] } }])) + it('supports field boosting', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', null, 'y*', ['title^3'])) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - ], + filter: [{ + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ 'pending.title^3', - 'saved.title^3' + 'saved.title^3', + 'global.title^3', ] } } @@ -304,14 +497,21 @@ describe('searchDsl/queryParams', () => { } }); }); - it('specifies filters and supports field and multi-field', () => { - expect(getQueryParams(MAPPINGS, null, 'y*', ['title', 'title.raw'], [{ terms: { foo: ['bar', 'baz'] } }])) + it('supports field and multi-field', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', null, 'y*', ['title', 'title.raw'])) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - ], + filter: [{ + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { @@ -319,8 +519,10 @@ describe('searchDsl/queryParams', () => { fields: [ 'pending.title', 'saved.title', + 'global.title', 'pending.title.raw', 'saved.title.raw', + 'global.title.raw', ] } } @@ -331,21 +533,28 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{type,search,searchFields}', () => { - it('includes bool, and sqs with field list', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title'])) + describe('type (plural, namespaced and global), search, searchFields', () => { + it('includes all types for field', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, ['saved', 'global'], 'y*', ['title'])) .toEqual({ query: { bool: { - filter: [ - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ - 'saved.title' + 'saved.title', + 'global.title', ] } } @@ -354,20 +563,27 @@ describe('searchDsl/queryParams', () => { } }); }); - it('supports fields pointing to multi-fields', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title.raw'])) + it('supports field boosting', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, ['saved', 'global'], 'y*', ['title^3'])) .toEqual({ query: { bool: { - filter: [ - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ - 'saved.title.raw' + 'saved.title^3', + 'global.title^3', ] } } @@ -376,21 +592,29 @@ describe('searchDsl/queryParams', () => { } }); }); - it('supports multiple search fields', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title', 'title.raw'])) + it('supports field and multi-field', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, null, ['saved', 'global'], 'y*', ['title', 'title.raw'])) .toEqual({ query: { bool: { - filter: [ - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ 'saved.title', - 'saved.title.raw' + 'global.title', + 'saved.title.raw', + 'global.title.raw', ] } } @@ -401,22 +625,28 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{type,search,searchFields,filters}', () => { - it('includes specified filters, type filter and sqs with field list', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title'], [{ terms: { foo: ['bar', 'baz'] } }])) + describe('namespace, type (plural, namespaced and global), search, searchFields', () => { + it('includes all types for field', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title'])) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ - 'saved.title' + 'saved.title', + 'global.title', ] } } @@ -425,21 +655,27 @@ describe('searchDsl/queryParams', () => { } }); }); - it('supports fields pointing to multi-fields', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title.raw'], [{ terms: { foo: ['bar', 'baz'] } }])) + it('supports field boosting', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title^3'])) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ - 'saved.title.raw' + 'saved.title^3', + 'global.title^3', ] } } @@ -448,22 +684,29 @@ describe('searchDsl/queryParams', () => { } }); }); - it('supports multiple search fields', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title', 'title.raw'], [{ terms: { foo: ['bar', 'baz'] } }])) + it('supports field and multi-field', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title', 'title.raw'])) .toEqual({ query: { bool: { - filter: [ - { terms: { foo: ['bar', 'baz'] } }, - { term: { type: 'saved' } } - ], + filter: [{ + bool: { + should: [ + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1 + } + }], must: [ { simple_query_string: { query: 'y*', fields: [ 'saved.title', - 'saved.title.raw' + 'global.title', + 'saved.title.raw', + 'global.title.raw', ] } } diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js index 94f938c921ed0..5bbe199a0e7e8 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js @@ -22,14 +22,14 @@ import Boom from 'boom'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; -export function getSearchDsl(mappings, options = {}) { +export function getSearchDsl(mappings, schema, options = {}) { const { + namespace, type, search, searchFields, sortField, sortOrder, - filters, } = options; if (!type && sortField) { @@ -40,12 +40,8 @@ export function getSearchDsl(mappings, options = {}) { throw Boom.notAcceptable('sortOrder requires a sortField'); } - if (filters && !Array.isArray(filters)) { - throw Boom.notAcceptable('filters must be an array'); - } - return { - ...getQueryParams(mappings, type, search, searchFields, filters), + ...getQueryParams(mappings, schema, namespace, type, search, searchFields), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js index 6ed40a7929dcf..7acdb6d7bd6ed 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js @@ -29,7 +29,7 @@ describe('getSearchDsl', () => { describe('validation', () => { it('throws when sortField is passed without type', () => { expect(() => { - getSearchDsl({}, { + getSearchDsl({}, {}, { type: undefined, sortField: 'title' }); @@ -37,7 +37,7 @@ describe('getSearchDsl', () => { }); it('throws when sortOrder without sortField', () => { expect(() => { - getSearchDsl({}, { + getSearchDsl({}, {}, { type: 'foo', sortOrder: 'desc' }); @@ -46,38 +46,41 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, type, search, searchFields, filters) to getQueryParams', () => { + it('passes (mappings, schema, namespace, type, search, searchFields) to getQueryParams', () => { const spy = sandbox.spy(queryParamsNS, 'getQueryParams'); const mappings = { type: { properties: {} } }; + const schema = { isNamespaceAgnostic: () => {} }; const opts = { + namespace: 'foo-namespace', type: 'foo', search: 'bar', searchFields: ['baz'], - filters: [{ bool: {} }], }; - getSearchDsl(mappings, opts); + getSearchDsl(mappings, schema, opts); sinon.assert.calledOnce(spy); sinon.assert.calledWithExactly( spy, mappings, + schema, + opts.namespace, opts.type, opts.search, opts.searchFields, - opts.filters, ); }); it('passes (mappings, type, sortField, sortOrder) to getSortingParams', () => { const spy = sandbox.stub(sortParamsNS, 'getSortingParams').returns({}); const mappings = { type: { properties: {} } }; + const schema = { isNamespaceAgnostic: () => {} }; const opts = { type: 'foo', sortField: 'bar', sortOrder: 'baz' }; - getSearchDsl(mappings, opts); + getSearchDsl(mappings, schema, opts); sinon.assert.calledOnce(spy); sinon.assert.calledWithExactly( spy, diff --git a/src/server/saved_objects/service/lib/trim_id_prefix.js b/src/server/saved_objects/service/lib/trim_id_prefix.js index e5a0bbf649748..039ff0848159c 100644 --- a/src/server/saved_objects/service/lib/trim_id_prefix.js +++ b/src/server/saved_objects/service/lib/trim_id_prefix.js @@ -30,14 +30,15 @@ function assertNonEmptyString(value, name) { * @param {string} type * @return {string} */ -export function trimIdPrefix(id, type) { +export function trimIdPrefix(schema, id, namespace, type) { assertNonEmptyString(id, 'document id'); assertNonEmptyString(type, 'saved object type'); - const prefix = `${type}:`; + const namespacePrefix = namespace && !schema.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + const prefix = `${namespacePrefix}${type}:`; if (!id.startsWith(prefix)) { - return id; + throw new Error(`Unable to trim id ${id}`); } return id.slice(prefix.length); diff --git a/src/server/saved_objects/service/saved_objects_client.js b/src/server/saved_objects/service/saved_objects_client.js index 1d69d55f9f4ae..25c252d195bf1 100644 --- a/src/server/saved_objects/service/saved_objects_client.js +++ b/src/server/saved_objects/service/saved_objects_client.js @@ -102,7 +102,7 @@ export class SavedObjectsClient { * @param {object} [options={}] * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] - * @property {object} [options.extraDocumentProperties={}] - extra properties to append to the document body, outside of the object's type property + * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { @@ -115,6 +115,7 @@ export class SavedObjectsClient { * @param {array} objects - [{ type, id, attributes, extraDocumentProperties }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - overwrites existing documents + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]} */ async bulkCreate(objects, options = {}) { @@ -126,10 +127,12 @@ export class SavedObjectsClient { * * @param {string} type * @param {string} id + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} */ - async delete(type, id) { - return this._repository.delete(type, id); + async delete(type, id, options = {}) { + return this._repository.delete(type, id, options); } /** @@ -138,12 +141,12 @@ export class SavedObjectsClient { * @property {string} [options.search] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information - * @property {object} [options.filters] - ES Query filters to append * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] * @property {string} [options.sortField] * @property {string} [options.sortOrder] * @property {Array} [options.fields] + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ async find(options = {}) { @@ -154,8 +157,8 @@ export class SavedObjectsClient { * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @param {object} [options = {}] - * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -173,8 +176,8 @@ export class SavedObjectsClient { * * @param {string} type * @param {string} id - * @param {object} [options = {}] - * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { @@ -188,7 +191,7 @@ export class SavedObjectsClient { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object - * @param {array} [options.extraDocumentProperties = {}] - an object of extra properties to write into the underlying document + * @property {string} [options.namespace] * @returns {promise} */ async update(type, id, attributes, options = {}) { diff --git a/src/server/saved_objects/service/saved_objects_client.test.js b/src/server/saved_objects/service/saved_objects_client.test.js index 56127023dd1fe..e69b12d1218cc 100644 --- a/src/server/saved_objects/service/saved_objects_client.test.js +++ b/src/server/saved_objects/service/saved_objects_client.test.js @@ -26,9 +26,9 @@ test(`#create`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const type = 'foo'; - const attributes = {}; - const options = {}; + const type = Symbol(); + const attributes = Symbol(); + const options = Symbol(); const result = await client.create(type, attributes, options); expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); @@ -42,8 +42,8 @@ test(`#bulkCreate`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const objects = []; - const options = {}; + const objects = Symbol(); + const options = Symbol(); const result = await client.bulkCreate(objects, options); expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); @@ -57,11 +57,12 @@ test(`#delete`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const type = 'foo'; - const id = 1; - const result = await client.delete(type, id); + const type = Symbol(); + const id = Symbol(); + const options = Symbol(); + const result = await client.delete(type, id, options); - expect(mockRepository.delete).toHaveBeenCalledWith(type, id); + expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); expect(result).toBe(returnValue); }); @@ -72,7 +73,7 @@ test(`#find`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const options = {}; + const options = Symbol(); const result = await client.find(options); expect(mockRepository.find).toHaveBeenCalledWith(options); @@ -86,7 +87,7 @@ test(`#bulkGet`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const objects = {}; + const objects = Symbol(); const options = Symbol(); const result = await client.bulkGet(objects, options); @@ -101,8 +102,8 @@ test(`#get`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const type = 'foo'; - const id = 1; + const type = Symbol(); + const id = Symbol(); const options = Symbol(); const result = await client.get(type, id, options); @@ -117,10 +118,10 @@ test(`#update`, async () => { }; const client = new SavedObjectsClient(mockRepository); - const type = 'foo'; - const id = 1; - const attributes = {}; - const options = {}; + const type = Symbol(); + const id = Symbol(); + const attributes = Symbol(); + const options = Symbol(); const result = await client.update(type, id, attributes, options); expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); diff --git a/src/ui/ui_exports/ui_export_types/index.js b/src/ui/ui_exports/ui_export_types/index.js index 2247b685bd063..9f72ff9767f91 100644 --- a/src/ui/ui_exports/ui_export_types/index.js +++ b/src/ui/ui_exports/ui_export_types/index.js @@ -24,7 +24,8 @@ export { export { mappings, -} from './saved_object_mappings'; + savedObjectsSchema, +} from './saved_objects'; export { app, diff --git a/src/ui/ui_exports/ui_export_types/saved_object_mappings.js b/src/ui/ui_exports/ui_export_types/saved_objects.js similarity index 83% rename from src/ui/ui_exports/ui_export_types/saved_object_mappings.js rename to src/ui/ui_exports/ui_export_types/saved_objects.js index de3570ce2e864..025d369091207 100644 --- a/src/ui/ui_exports/ui_export_types/saved_object_mappings.js +++ b/src/ui/ui_exports/ui_export_types/saved_objects.js @@ -17,8 +17,8 @@ * under the License. */ -import { flatConcatAtType } from './reduce'; -import { alias, mapSpec, wrap } from './modify_reduce'; +import { flatConcatAtType, mergeAtType } from './reduce'; +import { alias, mapSpec, uniqueKeys, wrap } from './modify_reduce'; // mapping types export const mappings = wrap( @@ -29,3 +29,5 @@ export const mappings = wrap( })), flatConcatAtType ); + +export const savedObjectsSchema = wrap(uniqueKeys(), mergeAtType); diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz index c07188439b0e0..ac2a10f42f4dc 100644 Binary files a/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz and b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz differ diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json index 26c62bca335d9..4f3ead9f47c67 100644 --- a/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json +++ b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json @@ -188,6 +188,9 @@ } } }, + "namespace": { + "type": "keyword" + }, "type": { "type": "keyword" }, @@ -249,4 +252,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js index f246088f87cdc..bd32c89614993 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js @@ -47,12 +47,12 @@ export class SecureSavedObjectsClient { ); } - async delete(type, id) { + async delete(type, id, options = {}) { return await this._execute( type, 'delete', - { type, id }, - repository => repository.delete(type, id), + { type, id, options }, + repository => repository.delete(type, id, options), ); } @@ -150,10 +150,11 @@ export class SecureSavedObjectsClient { this._auditLogger.savedObjectsAuthorizationSuccess(username, action, authorizedTypes, { options }); - return await this._internalRepository.find({ - ...options, - type: authorizedTypes - }); + return await this._internalRepository.find( + { + ...options, + type: authorizedTypes, + }); } async _findWithTypes(options) { diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js index e9ac730f5831b..95a4e9aa0eaac 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js @@ -353,8 +353,9 @@ describe('#delete', () => { actions: mockActions, }); const id = Symbol(); + const options = Symbol(); - await expect(client.delete(type, id)).rejects.toThrowError(mockErrors.forbiddenError); + await expect(client.delete(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'delete')]); expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); @@ -366,6 +367,7 @@ describe('#delete', () => { { type, id, + options, } ); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -390,15 +392,17 @@ describe('#delete', () => { actions: createMockActions(), }); const id = Symbol(); + const options = Symbol(); - const result = await client.delete(type, id); + const result = await client.delete(type, id, options); expect(result).toBe(returnValue); - expect(mockRepository.delete).toHaveBeenCalledWith(type, id); + expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { type, id, + options, }); }); @@ -422,11 +426,12 @@ describe('#delete', () => { actions: createMockActions(), }); const id = Symbol(); + const options = Symbol(); - const result = await client.delete(type, id); + const result = await client.delete(type, id, options); expect(result).toBe(returnValue); - expect(mockRepository.delete).toHaveBeenCalledWith(type, id); + expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); @@ -488,7 +493,7 @@ describe('#find', () => { [type], [mockActions.getSavedObjectAction(type, 'find')], { - options + options, } ); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -532,7 +537,7 @@ describe('#find', () => { [type1, type2], [mockActions.getSavedObjectAction(type1, 'find')], { - options + options, } ); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -663,7 +668,7 @@ describe('#find', () => { [type], [mockActions.getSavedObjectAction(type, 'find')], { - options + options, } ); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -727,14 +732,14 @@ describe('#find', () => { actions: mockActions, }); - await client.find(); + await client.find({}); expect(mockCheckPrivileges).toHaveBeenCalledWith([ mockActions.getSavedObjectAction(type1, 'find'), mockActions.getSavedObjectAction(type2, 'find'), ]); expect(mockRepository.find).toHaveBeenCalledWith(expect.objectContaining({ - type: [type2] + type: [type2], })); }); @@ -1005,7 +1010,7 @@ describe('#get', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { type, id, - options + options, }); }); diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 480641cc792f2..476af92d326a9 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -41,6 +41,11 @@ export const spaces = (kibana) => new kibana.Plugin({ }], hacks: [], mappings, + savedObjectsSchema: { + space: { + isNamespaceAgnostic: true, + }, + }, home: ['plugins/spaces/register_feature'], injectDefaultVars: function () { return { @@ -82,9 +87,9 @@ export const spaces = (kibana) => new kibana.Plugin({ const spacesService = createSpacesService(server); server.decorate('server', 'spaces', spacesService); - const { addScopedSavedObjectsClientWrapperFactory, types } = server.savedObjects; + const { addScopedSavedObjectsClientWrapperFactory } = server.savedObjects; addScopedSavedObjectsClientWrapperFactory( - spacesSavedObjectsClientWrapperFactory(spacesService, types) + spacesSavedObjectsClientWrapperFactory(spacesService) ); initSpacesApi(server); diff --git a/x-pack/plugins/spaces/mappings.json b/x-pack/plugins/spaces/mappings.json index 91c6e324e197e..6c91d9b020ff6 100644 --- a/x-pack/plugins/spaces/mappings.json +++ b/x-pack/plugins/spaces/mappings.json @@ -1,7 +1,4 @@ { - "spaceId": { - "type": "keyword" - }, "space": { "properties": { "name": { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap index 9bd6165f8d057..8b1a258138355 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap @@ -1,37 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`current space (space_1) #bulk_create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/mock-id] is missing its expected space identifier."`; +exports[`default space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #bulk_get throws when base client returns documents with malformed ids 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; +exports[`default space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/mock-id] is missing its expected space identifier."`; +exports[`default space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #delete does not allow an object to be deleted via a different space 1`] = `"not found: foo space_1:object_2"`; +exports[`default space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #find throws when base client returns documents with malformed ids 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; +exports[`default space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #get returns error when the object belongs to a different space 1`] = `"not found: foo space_1:object_2"`; +exports[`default space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #get returns error when the object has a malformed identifier 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; +exports[`default space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #update does not allow an object to be updated via a different space 1`] = `"not found: foo space_1:object_2"`; +exports[`space_1 space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`current space (space_1) #update throws when the base client returns a malformed document id 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; +exports[`space_1 space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`default space #bulk_create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; +exports[`space_1 space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`default space #bulk_get throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; +exports[`space_1 space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`default space #create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; +exports[`space_1 space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`default space #delete does not allow an object to be deleted via a different space 1`] = `"not found: foo object_2"`; +exports[`space_1 space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; -exports[`default space #find throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; - -exports[`default space #get returns error when the object belongs to a different space 1`] = `"not found: foo object_2"`; - -exports[`default space #get throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; - -exports[`default space #update does not allow an object to be updated via a different space 1`] = `"not found: foo object_2"`; - -exports[`default space #update throws when the base client returns a malformed document id 1`] = `"Saved object [space/default:default] has an unexpected space identifier [default]."`; +exports[`space_1 space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/__snapshots__/query_filters.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/__snapshots__/query_filters.test.js.snap deleted file mode 100644 index d3b95886e8353..0000000000000 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/__snapshots__/query_filters.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`throws if types contains an empty entry 1`] = `"type is required to build filter clause"`; - -exports[`throws when no types are provided 1`] = `"At least one type must be provided to \\"getSpacesQueryFilters\\""`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js deleted file mode 100644 index 720fb7e28e117..0000000000000 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * Returns if the provided Saved Object type is "space aware". - * Most types should be space-aware, and those that aren't should typically strive to become space-aware. - * Types that are not space-aware will appear in every space, and are not bound by any space-specific access controls. - */ -export function isTypeSpaceAware(type) { - return type !== 'space' && type !== 'config'; -} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js deleted file mode 100644 index a88d4c35125f7..0000000000000 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isTypeSpaceAware } from "./is_type_space_aware"; - -const knownSpaceAwareTypes = [ - 'dashboard', - 'visualization', - 'saved_search', - 'timelion_sheet', - 'index_pattern' -]; - -const unawareTypes = ['space']; - -knownSpaceAwareTypes.forEach(type => test(`${type} should be space-aware`, () => { - expect(isTypeSpaceAware(type)).toBe(true); -})); - -unawareTypes.forEach(type => test(`${type} should not be space-aware`, () => { - expect(isTypeSpaceAware(type)).toBe(false); -})); - -test(`unknown types should default to space-aware`, () => { - expect(isTypeSpaceAware('an-unknown-type')).toBe(true); -}); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.js deleted file mode 100644 index cf1754c056c3f..0000000000000 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_SPACE_ID } from "../../../../common/constants"; -import { isTypeSpaceAware } from "./is_type_space_aware"; - -function getClauseForType(spaceId, type) { - const shouldFilterOnSpace = isTypeSpaceAware(type) && spaceId; - const isDefaultSpace = spaceId === DEFAULT_SPACE_ID; - - const bool = { - must: [] - }; - - if (!type) { - throw new Error(`type is required to build filter clause`); - } - - bool.must.push({ - term: { - type - } - }); - - if (shouldFilterOnSpace) { - if (isDefaultSpace) { - // The default space does not add its spaceId to the objects that belong to it, in order - // to be compatible with installations that are not always space-aware. - bool.must_not = [{ - exists: { - field: "spaceId" - } - }]; - } else { - bool.must.push({ - term: { - spaceId - } - }); - } - } - - return { - bool - }; -} - -export function getSpacesQueryFilters(spaceId, types = []) { - if (types.length === 0) { - throw new Error(`At least one type must be provided to "getSpacesQueryFilters"`); - } - - const filters = []; - - const typeClauses = types.map((type) => getClauseForType(spaceId, type)); - - if (typeClauses.length > 0) { - filters.push({ - bool: { - should: typeClauses, - minimum_should_match: 1 - } - }); - } - - return filters; -} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.test.js deleted file mode 100644 index a7bcacc524caa..0000000000000 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.test.js +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getSpacesQueryFilters } from './query_filters'; - -test('throws when no types are provided', () => { - expect(() => getSpacesQueryFilters('space_1', [])).toThrowErrorMatchingSnapshot(); -}); - -test('throws if types contains an empty entry', () => { - expect(() => getSpacesQueryFilters('space_1', ['dashboard', ''])).toThrowErrorMatchingSnapshot(); -}); - -test('creates a query that filters on type, but not on space, for types that are not space-aware', () => { - const spaceId = 'space_1'; - const type = 'space'; - - const expectedTypeClause = { - bool: { - must: [{ - term: { - type - } - }] - } - }; - expect(getSpacesQueryFilters(spaceId, [type])).toEqual([{ - bool: { - should: [expectedTypeClause], - minimum_should_match: 1 - } - }]); -}); - -test('creates a query that restricts a space-aware type to the provided space (space_1)', () => { - const spaceId = 'space_1'; - const type = 'dashboard'; - - const expectedTypeClause = { - bool: { - must: [{ - term: { - type - } - }, { - term: { - spaceId - } - }] - } - }; - - expect(getSpacesQueryFilters(spaceId, [type])).toEqual([{ - bool: { - should: [expectedTypeClause], - minimum_should_match: 1 - } - }]); -}); - -test('creates a query that restricts a space-aware type to the provided space (default)', () => { - const spaceId = 'default'; - const type = 'dashboard'; - - const expectedTypeClause = { - bool: { - must: [{ - term: { - type - } - }], - // The default space does not add its spaceId to the objects that belong to it, in order - // to be compatible with installations that are not always space-aware. - must_not: [{ - exists: { - field: 'spaceId' - } - }] - } - }; - - expect(getSpacesQueryFilters(spaceId, [type])).toEqual([{ - bool: { - should: [expectedTypeClause], - minimum_should_match: 1 - } - }]); -}); - -test('creates a query supporting a find operation on multiple types', () => { - const spaceId = 'space_1'; - const types = [ - 'dashboard', - 'space', - 'visualization', - ]; - - const expectedSpaceClause = { - term: { - spaceId - } - }; - - const expectedTypeClauses = [{ - bool: { - must: [{ - term: { - type: 'dashboard' - } - }, expectedSpaceClause] - } - }, { - bool: { - must: [{ - term: { - type: 'space' - } - }] - } - }, { - bool: { - must: [{ - term: { - type: 'visualization' - } - }, expectedSpaceClause] - } - }]; - - expect(getSpacesQueryFilters(spaceId, types)).toEqual([{ - bool: { - should: expectedTypeClauses, - minimum_should_match: 1 - } - }]); -}); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js index da7a448d86d2d..3f7c0cbe2124e 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js @@ -6,11 +6,10 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -export function spacesSavedObjectsClientWrapperFactory(spacesService, types) { +export function spacesSavedObjectsClientWrapperFactory(spacesService) { return ({ client, request }) => new SpacesSavedObjectsClient({ baseClient: client, request, spacesService, - types, }); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index c9fd34a60b6d8..7f66b145e593e 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -5,25 +5,17 @@ */ import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { isTypeSpaceAware } from './lib/is_type_space_aware'; -import { getSpacesQueryFilters } from './lib/query_filters'; -import uniq from 'lodash'; -import uuid from 'uuid'; export class SpacesSavedObjectsClient { constructor(options) { const { - request, baseClient, + request, spacesService, - types, } = options; this.errors = baseClient.errors; - this._client = baseClient; - this._types = types; - this._spaceId = spacesService.getSpaceId(request); } @@ -35,29 +27,18 @@ export class SpacesSavedObjectsClient { * @param {object} [options={}] * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] - * @property {object} [options.extraDocumentProperties={}] - extra properties to append to the document body, outside of the object's type property + * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { - - const spaceId = this._spaceId; - - const createOptions = { - ...options, - extraDocumentProperties: { - ...options.extraDocumentProperties - }, - id: this._prependSpaceId(type, options.id) - }; - - if (this._shouldAssignSpaceId(type, spaceId)) { - createOptions.extraDocumentProperties.spaceId = spaceId; - } else { - delete createOptions.extraDocumentProperties.spaceId; + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); } - const result = await this._client.create(type, attributes, createOptions); - return this._trimSpaceId(result); + return await this._client.create(type, attributes, { + ...options, + namespace: this._getNamespace(this._spaceId) + }); } /** @@ -66,33 +47,18 @@ export class SpacesSavedObjectsClient { * @param {array} objects - [{ type, id, attributes, extraDocumentProperties }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - overwrites existing documents + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]} */ async bulkCreate(objects, options = {}) { - const spaceId = this._spaceId; - const objectsToCreate = objects.map(object => { - - const objectToCreate = { - ...object, - extraDocumentProperties: { - ...object.extraDocumentProperties - }, - id: this._prependSpaceId(object.type, object.id) - }; - - if (this._shouldAssignSpaceId(object.type, spaceId)) { - objectToCreate.extraDocumentProperties.spaceId = spaceId; - } else { - delete objectToCreate.extraDocumentProperties.spaceId; - } + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); + } - return objectToCreate; + return await this._client.bulkCreate(objects, { + ...options, + namespace: this._getNamespace(this._spaceId) }); - - const result = await this._client.bulkCreate(objectsToCreate, options); - result.saved_objects.forEach(this._trimSpaceId.bind(this)); - - return result; } /** @@ -100,16 +66,19 @@ export class SpacesSavedObjectsClient { * * @param {string} type * @param {string} id + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} */ - async delete(type, id) { - const objectId = this._prependSpaceId(type, id); - - // attempt to retrieve document before deleting. - // this ensures that the document belongs to the current space. - await this.get(type, id); + async delete(type, id, options = {}) { + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); + } - return await this._client.delete(type, objectId); + return await this._client.delete(type, id, { + ...options, + namespace: this._getNamespace(this._spaceId) + }); } /** @@ -118,39 +87,31 @@ export class SpacesSavedObjectsClient { * @property {string} [options.search] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information - * @property {object} [options.filters] - ES Query filters to append * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] * @property {string} [options.sortField] * @property {string} [options.sortOrder] * @property {Array} [options.fields] + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ async find(options = {}) { - const spaceOptions = {}; - - let types = options.type || this._types; - if (!Array.isArray(types)) { - types = [types]; + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); } - const filters = options.filters || []; - - const spaceId = this._spaceId; - - spaceOptions.filters = [...filters, ...getSpacesQueryFilters(spaceId, types)]; - - const result = await this._client.find({ ...options, ...spaceOptions }); - result.saved_objects.forEach(this._trimSpaceId.bind(this)); - return result; + return await this._client.find({ + ...options, + namespace: this._getNamespace(this._spaceId) + }); } /** * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @param {object} [options = {}] - * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -160,38 +121,14 @@ export class SpacesSavedObjectsClient { * ]) */ async bulkGet(objects = [], options = {}) { - // ES 'mget' does not support queries, so we have to filter results after the fact. - const thisSpaceId = this._spaceId; - - const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId', 'type'], options.extraDocumentProperties); - - const objectsToRetrieve = objects.map(object => ({ - ...object, - id: this._prependSpaceId(object.type, object.id) - })); + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); + } - const result = await this._client.bulkGet(objectsToRetrieve, { + return await this._client.bulkGet(objects, { ...options, - extraDocumentProperties - }); - - result.saved_objects = result.saved_objects.map(savedObject => { - const { id, type, spaceId = DEFAULT_SPACE_ID } = this._trimSpaceId(savedObject); - - if (isTypeSpaceAware(type)) { - if (spaceId !== thisSpaceId) { - return { - id, - type, - error: { statusCode: 404, message: 'Not found' } - }; - } - } - - return savedObject; + namespace: this._getNamespace(this._spaceId) }); - - return result; } /** @@ -199,32 +136,19 @@ export class SpacesSavedObjectsClient { * * @param {string} type * @param {string} id - * @param {object} [options = {}] - * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document + * @param {object} [options={}] + * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { - // ES 'get' does not support queries, so we have to filter results after the fact. - - const objectId = this._prependSpaceId(type, id); - - const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId'], options.extraDocumentProperties); + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); + } - const response = await this._client.get(type, objectId, { + return await this._client.get(type, id, { ...options, - extraDocumentProperties + namespace: this._getNamespace(this._spaceId) }); - - const { spaceId: objectSpaceId = DEFAULT_SPACE_ID } = response; - - if (isTypeSpaceAware(type)) { - const thisSpaceId = this._spaceId; - if (objectSpaceId !== thisSpaceId) { - throw this._client.errors.createGenericNotFoundError(); - } - } - - return this._trimSpaceId(response); } /** @@ -234,67 +158,25 @@ export class SpacesSavedObjectsClient { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object - * @param {array} [options.extraDocumentProperties = {}] - an object of extra properties to write into the underlying document + * @property {string} [options.namespace] * @returns {promise} */ async update(type, id, attributes, options = {}) { - const updateOptions = { - ...options, - extraDocumentProperties: { - ...options.extraDocumentProperties - } - }; - - const objectId = this._prependSpaceId(type, id); - - // attempt to retrieve document before updating. - // this ensures that the document belongs to the current space. - if (isTypeSpaceAware(type)) { - await this.get(type, id); - - const spaceId = this._spaceId; - - if (this._shouldAssignSpaceId(type, spaceId)) { - updateOptions.extraDocumentProperties.spaceId = spaceId; - } else { - delete updateOptions.extraDocumentProperties.spaceId; - } + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); } - const result = await this._client.update(type, objectId, attributes, updateOptions); - return this._trimSpaceId(result); - } - - _collectExtraDocumentProperties(thisClientProperties, optionalProperties = []) { - return uniq([...thisClientProperties, ...optionalProperties]).value(); - } - - _shouldAssignSpaceId(type, spaceId) { - return spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(type); - } - - _prependSpaceId(type, id = uuid.v1()) { - if (this._spaceId === DEFAULT_SPACE_ID || !isTypeSpaceAware(type)) { - return id; - } - return `${this._spaceId}:${id}`; + return await this._client.update(type, id, attributes, { + ...options, + namespace: this._getNamespace(this._spaceId) + }); } - _trimSpaceId(savedObject) { - const prefix = `${this._spaceId}:`; - - const idHasPrefix = savedObject.id.startsWith(prefix); - - if (this._shouldAssignSpaceId(savedObject.type, this._spaceId)) { - if (idHasPrefix) { - savedObject.id = savedObject.id.slice(prefix.length); - } else { - throw new Error(`Saved object [${savedObject.type}/${savedObject.id}] is missing its expected space identifier.`); - } - } else if (idHasPrefix) { - throw new Error(`Saved object [${savedObject.type}/${savedObject.id}] has an unexpected space identifier [${this._spaceId}].`); + _getNamespace(spaceId) { + if (spaceId === DEFAULT_SPACE_ID) { + return undefined; } - return savedObject; + return spaceId; } } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index e7dbffa413c9e..a25ccc2136de9 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -6,29 +6,7 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { createSpacesService } from '../create_spaces_service'; - -jest.mock('uuid', () => ({ - v1: jest.fn(() => `mock-id`) -})); import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { cloneDeep } from 'lodash'; - -const createObjectEntry = (type, id, spaceId) => ({ - [id]: { - id, - type, - spaceId - } -}); - -const SAVED_OBJECTS = { - ...createObjectEntry('foo', 'object_0'), - ...createObjectEntry('foo', 'space_1:object_1', 'space_1'), - ...createObjectEntry('foo', 'space_2:object_2', 'space_2'), - ...createObjectEntry('space', 'space_1'), -}; - -const createSavedObjects = () => cloneDeep(SAVED_OBJECTS); const config = { 'server.basePath': '/' @@ -46,1723 +24,289 @@ const createMockRequest = (space) => ({ getBasePath: () => space.id !== DEFAULT_SPACE_ID ? `/s/${space.id}` : '', }); -const createMockClient = (space, { mangleSpaceIdentifier = false } = {}) => { - const errors = { - createGenericNotFoundError: jest.fn((type, id) => { - return new Error(`not found: ${type} ${id}`); - }) - }; - - const maybeTransformSavedObject = (savedObject) => { - if (!mangleSpaceIdentifier) { - return savedObject; - } - if (space.id === DEFAULT_SPACE_ID) { - savedObject.id = `default:${space.id}`; - } else { - savedObject.id = savedObject.id.split(':')[1]; - } - - return savedObject; - }; +const createMockClient = () => { + const errors = Symbol(); return { - get: jest.fn((type, id) => { - const result = createSavedObjects()[id]; - if (!result) { - throw errors.createGenericNotFoundError(type, id); - } - - return maybeTransformSavedObject(result); - }), - bulkGet: jest.fn((objects) => { - return { - saved_objects: objects.map(object => { - const result = createSavedObjects()[object.id]; - if (!result) { - return { - id: object.id, - type: object.type, - error: { statusCode: 404, message: 'Not found' } - }; - } - return maybeTransformSavedObject(result); - }) - }; - }), - find: jest.fn(({ type }) => { - // used to locate spaces when type is `space` within these tests - if (type === 'space') { - return { - saved_objects: [space] - }; - } - const objects = createSavedObjects(); - const result = Object.keys(objects) - .filter(key => objects[key].spaceId === space.id || (space.id === DEFAULT_SPACE_ID && !objects[key].spaceId)) - .map(key => maybeTransformSavedObject(objects[key])); - - return { - saved_objects: result - }; - }), - create: jest.fn((type, attributes, options) => { - return maybeTransformSavedObject({ - id: options.id || 'foo-id', - type, - attributes - }); - }), - bulkCreate: jest.fn((objects) => { - return { - saved_objects: cloneDeep(objects).map(maybeTransformSavedObject) - }; - }), - update: jest.fn((type, id, attributes) => { - return maybeTransformSavedObject({ - id, - type, - attributes - }); - }), + get: jest.fn(), + bulkGet: jest.fn(), + find: jest.fn(), + create: jest.fn(), + bulkCreate: jest.fn(), + update: jest.fn(), delete: jest.fn(), errors, }; }; -describe('default space', () => { - const currentSpace = { - id: 'default', - }; - - describe('#get', () => { - test(`returns the object when it belongs to the current space`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_0'; - const options = {}; - - const result = await client.get(type, id, options); - - expect(result).toEqual(SAVED_OBJECTS[id]); - }); - - test(`does not append the space id to the document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_0'; - const options = {}; - - await client.get(type, id, options); - - expect(baseClient.get).toHaveBeenCalledWith(type, id, { extraDocumentProperties: ['spaceId'] }); - }); - - test(`returns global objects that don't belong to a specific space`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'space'; - const id = 'space_1'; - const options = {}; - - const result = await client.get(type, id, options); - - expect(result).toEqual(SAVED_OBJECTS[id]); - }); - - test(`merges options.extraDocumentProperties`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_0'; - const options = { - extraDocumentProperties: ['otherSourceProp'] - }; - - await client.get(type, id, options); - - expect(baseClient.get).toHaveBeenCalledWith(type, id, { - extraDocumentProperties: ['spaceId', 'otherSourceProp'] - }); - }); - - test(`returns error when the object belongs to a different space`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_2'; - const options = {}; - - await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); - }); - - test(`throws when the base client returns a malformed document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_0'; - const options = {}; - - await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#bulk_get', () => { - test(`only returns objects belonging to the current space`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const options = {}; - - const result = await client.bulkGet([{ - type, - id: 'object_0' - }, { - type, - id: 'object_2' - }], options); - - expect(result).toEqual({ - saved_objects: [{ - id: 'object_0', - type: 'foo', - }, { - id: 'object_2', - type: 'foo', - error: { - message: 'Not found', - statusCode: 404 - } - }] - }); - }); - - test(`does not append the space id to the document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const options = {}; - - const objects = [{ - type, - id: 'object_0' - }, { - type, - id: 'object_2' - }]; - - await client.bulkGet(objects, options); - - expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { ...options, extraDocumentProperties: ["spaceId", "type"] }); - }); - - test(`returns global objects that don't belong to a specific space`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const options = {}; - - const result = await client.bulkGet([{ - type, - id: 'object_0' - }, { - type, - id: 'space_1' - }], options); - - expect(result).toEqual({ - saved_objects: [{ - id: 'object_0', - type: 'foo', - }, { - id: 'space_1', - type: 'space', - }] - }); - }); - - test(`merges options.extraDocumentProperties`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - - const objects = [{ - type, - id: 'object_1' - }, { - type, - id: 'object_2' - }]; - - const options = { - extraDocumentProperties: ['otherSourceProp'] - }; - - await client.bulkGet(objects, options); - - expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { - extraDocumentProperties: ['spaceId', 'type', 'otherSourceProp'] - }); - }); - - test(`throws when the base client returns a malformed document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_0'; - const options = {}; - - await expect(client.bulkGet([{ type, id }], options)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#find', () => { - test(`creates ES query filters restricting objects to the current space`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = ['foo', 'space']; - const options = { - type - }; - - await client.find(options); - - expect(baseClient.find).toHaveBeenCalledWith({ - type, - filters: [{ - bool: { - minimum_should_match: 1, - should: [{ - bool: { - must: [{ - term: { - type: 'foo' - }, - }], - must_not: [{ - exists: { - field: "spaceId" - } - }] - } - }, { - bool: { - must: [{ - term: { - type: 'space' - }, - }], - } - }] - } - }] - }); - }); - - test(`merges incoming filters with filters generated by Spaces Saved Objects Client`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const otherFilters = [{ - bool: { - must: [{ - term: { - foo: 'bar' - } - }] - } - }]; - - const options = { - type, - filters: otherFilters - }; - - await client.find(options); - - expect(baseClient.find).toHaveBeenCalledWith({ - type, - filters: [...otherFilters, { - bool: { - minimum_should_match: 1, - should: [{ - bool: { - must: [{ - term: { - type - }, - }], - must_not: [{ - exists: { - field: "spaceId" - } - }] - } - }] - } - }] - }); - }); - - test(`throws when the base client returns a malformed document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const options = { type }; - - await expect(client.find(options)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#create', () => { - - test('does not assign a space-unaware (global) object to a space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await client.create(type, attributes); - - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); - }); - - test('does not assign a spaceId to space-aware objects belonging to the default space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await client.create(type, attributes); - - // called without extraDocumentProperties - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); - }); - - test(`throws when the base client returns a malformed document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.create(type, attributes)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#bulk_create', () => { - test('allows for bulk creation when all types are space-aware', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; - - await client.bulkCreate(objects, {}); - - const expectedCalledWithObjects = objects.map(object => ({ - ...object, - extraDocumentProperties: {}, - id: 'mock-id' - })); - - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); - - test('allows for bulk creation when all types are not space-aware (global)', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - const objects = [{ - type: 'space', - attributes - }, { - type: 'space', - attributes - }]; - - await client.bulkCreate(objects, {}); - - expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => { - return { ...o, extraDocumentProperties: {}, id: 'mock-id' }; - }), {}); - }); - - test('allows space-aware and non-space-aware (global) objects to be created at the same time', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - const objects = [{ - type: 'space', - attributes - }, { - type: 'foo', - attributes - }]; - - await client.bulkCreate(objects, {}); - - const expectedCalledWithObjects = [...objects]; - expectedCalledWithObjects[0] = { - ...expectedCalledWithObjects[0], - extraDocumentProperties: {}, - id: 'mock-id' - }; - expectedCalledWithObjects[1] = { - ...expectedCalledWithObjects[1], - extraDocumentProperties: {}, - id: 'mock-id' - }; - - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); - - test('does not assign a spaceId to space-aware objects that belong to the default space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; - - await client.bulkCreate(objects, {}); - - // called with empty extraDocumentProperties - expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => ({ - ...o, - extraDocumentProperties: {}, - id: 'mock-id' - })), {}); - }); - - test(`throws when the base client returns a malformed document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; - - await expect(client.bulkCreate(objects, {})).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#update', () => { - test('allows an object to be updated if it exists in the same space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_0'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; +[ + { id: DEFAULT_SPACE_ID, expectedNamespace: undefined }, + { id: 'space_1', expectedNamespace: 'space_1' } +].forEach(currentSpace => { + describe(`${currentSpace.id} space`, () => { - await client.update(type, id, attributes); + describe('#get', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: {} }); - }); - - test('allows a global object to be updated', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.get(null, null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - const id = 'space_1'; - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.get.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - await client.update(type, id, attributes); - - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: {} }); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + const type = Symbol(); + const id = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.get(type, id, options); - test('does not allow an object to be updated via a different space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.get).toHaveBeenCalledWith(type, id, { foo: 'bar', namespace: currentSpace.expectedNamespace }); }); - - const id = 'object_2'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); - }); - - test(`throws when the base client returns a malformed document id`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'space_1'; - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); }); - }); - describe('#delete', () => { - test('allows an object to be deleted if it exists in the same space', async () => { + describe('#bulkGet', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.bulkGet(null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - const id = 'object_0'; - const type = 'foo'; + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.bulkGet.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - await client.delete(type, id); - - expect(baseClient.delete).toHaveBeenCalledWith(type, id); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - test(`allows a global object to be deleted`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const objects = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.bulkGet(objects, options); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { foo: 'bar', namespace: currentSpace.expectedNamespace }); }); - - const id = 'space_1'; - const type = 'space'; - - await client.delete(type, id); - - expect(baseClient.delete).toHaveBeenCalledWith(type, id); }); - test('does not allow an object to be deleted via a different space', async () => { + describe('#find', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.find({ namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - const id = 'object_2'; - const type = 'foo'; - - await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); -}); - -describe('current space (space_1)', () => { - const currentSpace = { - id: 'space_1', - }; + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.find.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - describe('#get', () => { - test(`returns the object when it belongs to the current space`, async () => { + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.find(options); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_1'; - const options = {}; - - const result = await client.get(type, id, options); - - expect(result).toEqual({ - id, - type, - spaceId: currentSpace.id + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.find).toHaveBeenCalledWith({ foo: 'bar', namespace: currentSpace.expectedNamespace }); }); }); - test('appends the space id to the document id', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_1'; - const options = {}; - - await client.get(type, id, options); - - expect(baseClient.get).toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`, { ...options, extraDocumentProperties: ['spaceId'] }); - }); - - test(`returns global objects that don't belong to a specific space`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'space'; - const id = 'space_1'; - const options = {}; - - const result = await client.get(type, id, options); - - expect(result).toEqual(SAVED_OBJECTS[id]); - }); - - test(`merges options.extraDocumentProperties`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const id = 'object_1'; - const options = { - extraDocumentProperties: ['otherSourceProp'] - }; - - await client.get(type, id, options); - - expect(baseClient.get).toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`, { - extraDocumentProperties: ['spaceId', 'otherSourceProp'] - }); - }); + describe('#create', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - test(`returns error when the object belongs to a different space`, async () => { + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + }); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.create('foo', {}, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - const type = 'foo'; - const id = 'object_2'; - const options = {}; + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.create.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - test(`returns error when the object has a malformed identifier`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); + const type = Symbol(); + const attributes = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.create(type, attributes, options); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { foo: 'bar', namespace: currentSpace.expectedNamespace }); }); - - const type = 'foo'; - const id = 'object_1'; - const options = {}; - - await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); }); - }); - describe('#bulk_get', () => { - test(`only returns objects belonging to the current space`, async () => { + describe('#bulkCreate', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.bulkCreate(null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - const type = 'foo'; - const options = {}; + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.bulkCreate.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - const result = await client.bulkGet([{ - type, - id: 'object_1' - }, { - type, - id: 'object_2' - }], options); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - expect(result).toEqual({ - saved_objects: [{ - id: 'object_1', - spaceId: 'space_1', - type: 'foo', - }, { - id: 'object_2', - type: 'foo', - error: { - message: 'Not found', - statusCode: 404 - } - }] - }); - }); + const objects = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.bulkCreate(objects, options); - test('appends the space id to the document id', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, { foo: 'bar', namespace: currentSpace.expectedNamespace }); }); - - const type = 'foo'; - const options = {}; - const objects = [{ - type, - id: 'object_1' - }, { - type, - id: 'object_2' - }]; - - await client.bulkGet(objects, options); - - const expectedObjects = objects.map(o => ({ ...o, id: `${currentSpace.id}:${o.id}` })); - expect(baseClient.bulkGet) - .toHaveBeenCalledWith(expectedObjects, { ...options, extraDocumentProperties: ["spaceId", "type"] }); }); - test(`returns global objects that don't belong to a specific space`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const options = {}; + describe('#update', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - const result = await client.bulkGet([{ - type, - id: 'object_1' - }, { - type: 'space', - id: 'space_1' - }], options); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - expect(result).toEqual({ - saved_objects: [{ - id: 'object_1', - spaceId: 'space_1', - type: 'foo', - }, { - id: 'space_1', - type: 'space', - }] + await expect(client.update(null, null, null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - }); - test(`merges options.extraDocumentProperties`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.update.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - const type = 'foo'; + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - const objects = [{ - type, - id: 'object_1' - }, { - type, - id: 'object_2' - }]; + const type = Symbol(); + const id = Symbol(); + const attributes = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.update(type, id, attributes, options); - const options = { - extraDocumentProperties: ['otherSourceProp'] - }; - - await client.bulkGet(objects, options); - - const expectedCalledWithObjects = objects.map(object => { - const id = `${currentSpace.id}:${object.id}`; - return { - ...object, - id - }; - }); - - expect(baseClient.bulkGet).toHaveBeenCalledWith(expectedCalledWithObjects, { - extraDocumentProperties: ['spaceId', 'type', 'otherSourceProp'] + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { foo: 'bar', namespace: currentSpace.expectedNamespace }); }); }); - test(`throws when base client returns documents with malformed ids`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const objects = [{ - type, - id: 'object_1' - }]; - const options = {}; + describe('#delete', () => { + test(`throws error if options.namespace is specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); - await expect(client.bulkGet(objects, options)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - describe('#find', () => { - test(`creates ES query filters restricting objects to the current space`, async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.delete(null, null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); - const type = ['foo', 'space']; - const options = { - type - }; + test(`supplements options with undefined namespace`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.delete.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); - await client.find(options); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); - expect(baseClient.find).toHaveBeenCalledWith({ - type, - filters: [{ - bool: { - minimum_should_match: 1, - should: [{ - bool: { - must: [{ - term: { - type: 'foo' - }, - }, { - term: { - spaceId: 'space_1' - } - }], - } - }, { - bool: { - must: [{ - term: { - type: 'space' - }, - }], - } - }] - } - }] - }); - }); + const type = Symbol(); + const id = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.delete(type, id, options); - test(`merges incoming filters with filters generated by Spaces Saved Objects Client`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const otherFilters = [{ - bool: { - must: [{ - term: { - foo: 'bar' - } - }] - } - }]; - - const options = { - type, - filters: otherFilters - }; - - await client.find(options); - - expect(baseClient.find).toHaveBeenCalledWith({ - type, - filters: [...otherFilters, { - bool: { - minimum_should_match: 1, - should: [{ - bool: { - must: [{ - term: { - type - }, - }, { - term: { - spaceId: 'space_1' - } - }] - } - }] - } - }] + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.delete).toHaveBeenCalledWith(type, id, { foo: 'bar', namespace: currentSpace.expectedNamespace }); }); }); - - test(`throws when base client returns documents with malformed ids`, async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const options = { - type, - }; - - await expect(client.find(options)).rejects.toThrowErrorMatchingSnapshot(); - }); }); - describe('#create', () => { - test('automatically assigns the object to the current space via extraDocumentProperties', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await client.create(type, attributes); - - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { - id: `${currentSpace.id}:mock-id`, - extraDocumentProperties: { - spaceId: 'space_1' - } - }); - }); - - test('does not assign a space-unaware (global) object to a space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await client.create(type, attributes); - - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); - }); - - test('throws when the base client returns a malformed document id', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.create(type, attributes)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#bulk_create', () => { - test('allows for bulk creation when all types are space-aware', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; - - await client.bulkCreate(objects, {}); - - const expectedCalledWithObjects = objects.map(object => ({ - ...object, - extraDocumentProperties: { - spaceId: 'space_1' - }, - id: `${currentSpace.id}:mock-id` - })); - - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); - - test('allows for bulk creation when all types are not space-aware', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - const objects = [{ - type: 'space', - attributes - }, { - type: 'space', - attributes - }]; - - await client.bulkCreate(objects, {}); - - expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => { - return { ...o, extraDocumentProperties: {}, id: 'mock-id' }; - }), {}); - }); - - test('allows space-aware and non-space-aware (global) objects to be created at the same time', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - const objects = [{ - type: 'space', - attributes - }, { - type: 'foo', - attributes - }]; - - await client.bulkCreate(objects, {}); - - const expectedCalledWithObjects = [...objects]; - expectedCalledWithObjects[0] = { - ...expectedCalledWithObjects[0], - extraDocumentProperties: {}, - id: 'mock-id' - }; - expectedCalledWithObjects[1] = { - ...expectedCalledWithObjects[1], - extraDocumentProperties: { - spaceId: 'space_1' - }, - id: `${currentSpace.id}:mock-id` - }; - - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); - - test('throws when the base client returns a malformed document id', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.bulkCreate([{ type, attributes }])).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#update', () => { - test('allows an object to be updated if it exists in the same space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_1'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await client.update(type, id, attributes); - - expect(baseClient.update) - .toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`, attributes, { extraDocumentProperties: { spaceId: 'space_1' } }); - }); - - test('allows a global object to be updated', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'space_1'; - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await client.update(type, id, attributes); - - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: {} }); - }); - - test('does not allow an object to be updated via a different space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_2'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); - }); - - test('throws when the base client returns a malformed document id', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_1'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('#delete', () => { - test('allows an object to be deleted if it exists in the same space', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_1'; - const type = 'foo'; - - await client.delete(type, id); - - expect(baseClient.delete).toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`); - }); - - test('allows a global object to be deleted', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'space_1'; - const type = 'space'; - - await client.delete(type, id); - - expect(baseClient.delete).toHaveBeenCalledWith(type, id); - }); - - test('does not allow an object to be deleted via a different space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_2'; - const type = 'foo'; - - await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); }); diff --git a/x-pack/test/rbac_api_integration/apis/privileges/index.js b/x-pack/test/rbac_api_integration/apis/privileges/index.js index 9563a1b65540f..aa66f8f7d2d96 100644 --- a/x-pack/test/rbac_api_integration/apis/privileges/index.js +++ b/x-pack/test/rbac_api_integration/apis/privileges/index.js @@ -38,6 +38,9 @@ export default function ({ getService }) { 'action:saved_objects/graph-workspace/get', 'action:saved_objects/graph-workspace/bulk_get', 'action:saved_objects/graph-workspace/find', + 'action:saved_objects/space/get', + 'action:saved_objects/space/bulk_get', + 'action:saved_objects/space/find', 'action:saved_objects/index-pattern/get', 'action:saved_objects/index-pattern/bulk_get', 'action:saved_objects/index-pattern/find', diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js index 5bb42acacd392..ca9b9bb2ec1c2 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js @@ -69,50 +69,6 @@ export default function ({ getService }) { }); }; - const expectAllResultsIncludingInvalidTypes = (resp) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 5, - saved_objects: [ - { - id: '91200a00-9efd-11e7-acb3-3dab96693fab', - type: 'index-pattern', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - attributes: resp.body.saved_objects[0].attributes - }, - { - id: '7.0.0-alpha1', - type: 'config', - updated_at: '2017-09-21T18:49:16.302Z', - version: 1, - attributes: resp.body.saved_objects[1].attributes - }, - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: 1, - attributes: resp.body.saved_objects[2].attributes - }, - { - id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: 1, - attributes: resp.body.saved_objects[3].attributes - }, - { - id: 'visualization:dd7caf20-9efd-11e7-acb3-3dab96693faa', - type: 'not-a-visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: 1 - }, - ] - }); - }; - const createExpectEmpty = (page, perPage, total) => (resp) => { expect(resp.body).to.eql({ page: page, @@ -290,7 +246,7 @@ export default function ({ getService }) { noType: { description: 'all objects', statusCode: 200, - response: expectAllResultsIncludingInvalidTypes, + response: expectResultsWithValidTypes, }, }, }); @@ -324,7 +280,7 @@ export default function ({ getService }) { noType: { description: 'all objects', statusCode: 200, - response: expectAllResultsIncludingInvalidTypes, + response: expectResultsWithValidTypes, }, } }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js index 37bd80f56a5c8..517aab85312fa 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js @@ -6,7 +6,7 @@ import expect from 'expect.js'; import { SPACES } from './lib/spaces'; -import { getIdPrefix, getUrlPrefix, getExpectedSpaceIdProperty } from './lib/space_test_utils'; +import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -73,7 +73,6 @@ export default function ({ getService }) { type: 'visualization', updated_at: '2017-09-21T18:51:23.794Z', version: resp.body.saved_objects[0].version, - ...getExpectedSpaceIdProperty(spaceId), attributes: { title: 'Count of requests', description: '', diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js index 14d6ba54de2e8..4a7bfa30d40dc 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js @@ -31,17 +31,17 @@ export default function ({ getService }) { } }); - const expectedDocumentId = spaceId === DEFAULT_SPACE_ID ? resp.body.id : `${spaceId}:${resp.body.id}`; + const expectedSpacePrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`; // query ES directory to assert on space id const { _source } = await es.get({ - id: `visualization:${expectedDocumentId}`, + id: `${expectedSpacePrefix}visualization:${resp.body.id}`, type: 'doc', index: '.kibana' }); const { - spaceId: actualSpaceId = '**not defined**' + namespace: actualSpaceId = '**not defined**' } = _source; if (spaceId === DEFAULT_SPACE_ID) { @@ -75,7 +75,7 @@ export default function ({ getService }) { }); const { - spaceId: actualSpaceId = '**not defined**' + namespace: actualSpaceId = '**not defined**' } = _source; expect(actualSpaceId).to.eql('**not defined**'); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js index fe3cb7bb2be06..07bb72a30e982 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js @@ -7,7 +7,6 @@ import expect from 'expect.js'; import { SPACES } from './lib/spaces'; import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -47,16 +46,10 @@ export default function ({ getService }) { )); it(`should return ${tests.inOtherSpace.statusCode} when deleting a doc belonging to another space`, async () => { - const documentId = `${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`; - - let expectedObjectId = documentId; - - if (spaceId !== DEFAULT_SPACE_ID) { - expectedObjectId = `${spaceId}:${expectedObjectId}`; - } + const expectedObjectId = `${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`; await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${documentId}`) + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${expectedObjectId}`) .expect(tests.inOtherSpace.statusCode) .then(tests.inOtherSpace.response('dashboard', expectedObjectId)); }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js index 01383433774ef..ea59e01eff96d 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js @@ -44,7 +44,7 @@ export default function ({ getService }) { id: '7.0.0-alpha1', type: 'config', updated_at: '2017-09-21T18:49:16.302Z', - version: 3, + version: 1, }, { id: `default`, type: 'space', @@ -85,7 +85,9 @@ export default function ({ getService }) { .sort(sortById); expectedSavedObjects.forEach((object, index) => { - object.attributes = resp.body.saved_objects[index].attributes; + if (resp.body.saved_objects[index]) { + object.attributes = resp.body.saved_objects[index].attributes; + } }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js index 6a8e21e9b5c99..0369bffba26e5 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js @@ -7,7 +7,6 @@ import expect from 'expect.js'; import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; import { SPACES } from './lib/spaces'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -16,10 +15,6 @@ export default function ({ getService }) { describe('get', () => { const expectResults = (spaceId) => () => (resp) => { - - // The default space does not assign a space id. - const expectedSpaceId = spaceId === 'default' ? undefined : spaceId; - const expectedBody = { id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, type: 'visualization', @@ -37,10 +32,6 @@ export default function ({ getService }) { } }; - if (expectedSpaceId) { - expectedBody.spaceId = expectedSpaceId; - } - expect(resp.body).to.eql(expectedBody); }; @@ -60,17 +51,10 @@ export default function ({ getService }) { it(`should return ${tests.exists.statusCode}`, async () => { const objectId = `${getIdPrefix(otherSpaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`; - let expectedObjectId = objectId; - const testingMismatchedSpaces = spaceId !== otherSpaceId; - - if (testingMismatchedSpaces && spaceId !== DEFAULT_SPACE_ID) { - expectedObjectId = `${spaceId}:${expectedObjectId}`; - } - return supertest .get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${objectId}`) .expect(tests.exists.statusCode) - .then(tests.exists.response('visualization', expectedObjectId)); + .then(tests.exists.response('visualization', objectId)); }); }); }; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js index fab201a5b3c00..5d7f40f9c54b1 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js @@ -13,12 +13,3 @@ export function getUrlPrefix(spaceId) { export function getIdPrefix(spaceId) { return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } - -export function getExpectedSpaceIdProperty(spaceId) { - if (spaceId === DEFAULT_SPACE_ID) { - return {}; - } - return { - spaceId - }; -} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js index 3aea0aadde82e..4bc938ba7a499 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js @@ -7,7 +7,6 @@ import expect from 'expect.js'; import { SPACES } from './lib/spaces'; import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -92,7 +91,7 @@ export default function ({ getService }) { } }) .expect(tests.inOtherSpace.statusCode) - .then(tests.inOtherSpace.response(`visualization`, `${spaceId === DEFAULT_SPACE_ID ? '' : (spaceId + ':')}${id}`)); + .then(tests.inOtherSpace.response(`visualization`, `${id}`)); }); describe('unknown id', () => { @@ -105,7 +104,7 @@ export default function ({ getService }) { } }) .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response(`visualization`, `${spaceId === DEFAULT_SPACE_ID ? '' : (spaceId + ':')}not an id`)); + .then(tests.doesntExist.response(`visualization`, `not an id`)); }); }); }); diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz index 0be0d4c12010a..a795be4f88ea3 100644 Binary files a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz and b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz differ diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json index bb9b0c33b7394..d8d4c52985bf5 100644 --- a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -30,9 +30,6 @@ } } }, - "spaceId": { - "type": "keyword" - }, "space": { "properties": { "name": { @@ -308,4 +305,4 @@ }, "aliases": {} } -} \ No newline at end of file +}