From f208d3cdba53a2968518904533ced9df8a153a4b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 7 May 2018 09:46:48 -0400 Subject: [PATCH] Crude and incomplete impl of Space-Aware Saved Objects Client --- .../client/saved_objects_client.js | 40 ++++- x-pack/plugins/spaces/index.js | 4 + .../saved_objects_client_wrapper.js | 14 ++ .../spaces_saved_objects_client.js | 152 ++++++++++++++++++ 4 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 79dc55540e039d..141c240382929e 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -107,6 +107,7 @@ export class SavedObjectsClient { async create(type, attributes = {}, options = {}) { const { id, + extraBodyProperties = {}, overwrite = false } = options; @@ -122,7 +123,8 @@ export class SavedObjectsClient { body: { type, updated_at: time, - [type]: attributes + [type]: attributes, + ...extraBodyProperties }, }); @@ -169,7 +171,8 @@ export class SavedObjectsClient { { type: object.type, updated_at: time, - [object.type]: object.attributes + [object.type]: object.attributes, + ...object.extraBodyProperties } ]; }; @@ -272,6 +275,7 @@ export class SavedObjectsClient { sortField, sortOrder, fields, + queryDecorator, } = options; if (searchFields && !Array.isArray(searchFields)) { @@ -300,6 +304,10 @@ export class SavedObjectsClient { } }; + if (esOptions.body.query && typeof queryDecorator === 'function') { + esOptions.body.query = queryDecorator(esOptions.body.query); + } + const response = await this._callCluster('search', esOptions); if (response.status === 404) { @@ -342,7 +350,7 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet(objects = []) { + async bulkGet(objects = [], options = {}) { if (objects.length === 0) { return { saved_objects: [] }; } @@ -357,8 +365,15 @@ export class SavedObjectsClient { } }); + const { docs } = response; + + let docsToReturn = docs; + if (typeof options.documentFilter === 'function') { + docsToReturn = docs.filter(options.documentFilter); + } + return { - saved_objects: response.docs.map((doc, i) => { + saved_objects: docsToReturn.map((doc, i) => { const { id, type } = objects[i]; if (!doc.found) { @@ -370,13 +385,19 @@ export class SavedObjectsClient { } const time = doc._source.updated_at; - return { + const savedObject = { id, type, ...time && { updated_at: time }, version: doc._version, attributes: doc._source[type] }; + + if (typeof options.resultDecorator === 'function') { + return options.resultDecorator(savedObject, doc); + } + + return savedObject; }) }; } @@ -388,7 +409,7 @@ export class SavedObjectsClient { * @param {string} id * @returns {promise} - { id, type, version, attributes } */ - async get(type, id) { + async get(type, id, options = {}) { const response = await this._callCluster('get', { id: this._generateEsId(type, id), type: this._type, @@ -396,6 +417,10 @@ export class SavedObjectsClient { ignore: [404] }); + if (typeof options.responseInterceptor === 'function') { + options.responseInterceptor(response); + } + const docNotFound = response.found === false; const indexNotFound = response.status === 404; if (docNotFound || indexNotFound) { @@ -435,7 +460,8 @@ export class SavedObjectsClient { body: { doc: { updated_at: time, - [type]: attributes + [type]: attributes, + ...options.extraBodyProperties } }, }); diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 5780998378cf84..36aa5560e7e28c 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -11,6 +11,7 @@ import { initSpacesApi } from './server/routes/api/v1/spaces'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import mappings from './mappings.json'; +import { spacesSavedObjectsClientWrapper } from './server/lib/saved_objects_client/saved_objects_client_wrapper'; export const spaces = (kibana) => new kibana.Plugin({ id: 'spaces', @@ -55,6 +56,9 @@ export const spaces = (kibana) => new kibana.Plugin({ const config = server.config(); validateConfig(config, message => server.log(['spaces', 'warning'], message)); + const savedObjectsClientProvider = server.getSavedObjectsClientProvider(); + savedObjectsClientProvider.addClientWrapper(spacesSavedObjectsClientWrapper); + initSpacesApi(server); initSpacesRequestInterceptors(server); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js new file mode 100644 index 00000000000000..1b09b209ee9445 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js @@ -0,0 +1,14 @@ +/* + * 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 { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; + +export function spacesSavedObjectsClientWrapper(baseClient, options) { + return new SpacesSavedObjectsClient({ + baseClient, + ...options + }); +} 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 new file mode 100644 index 00000000000000..c3e8a5614cddb3 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -0,0 +1,152 @@ +/* + * 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. + */ + +export class SpacesSavedObjectsClient { + constructor(options) { + const { + request, + baseClient, + spaceUrlContext, + } = options; + + this.errors = baseClient.errors; + + this._client = baseClient; + this._request = request; + + this._spaceUrlContext = spaceUrlContext; + } + + async create(type, attributes = {}, options = {}) { + + if (this._isTypeSpaceAware(type)) { + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId: await this._getSpaceId() + }; + } + + return await this._client.create(type, attributes, options); + } + + async bulkCreate(objects, options = {}) { + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId: await this._getSpaceId() + }; + + return await this._client.bulkCreate(objects, options); + } + + async delete(type, id) { + return await this._client.delete(type, id); + } + + async find(options = {}) { + const spaceOptions = {}; + + if (this._isTypeSpaceAware(options.type)) { + const spaceId = await this._getSpaceId(); + + spaceOptions.queryDecorator = (query) => { + const { bool = {} } = query; + + if (!Array.isArray(bool.filter)) { + bool.filter = []; + } + + bool.filter.push({ + term: { + spaceId + } + }); + + return query; + }; + } + + return await this._client.find({ ...options, ...spaceOptions }); + } + + async bulkGet(objects = []) { + // ES 'mget' does not support queries, so we have to filter results after the fact. + const thisSpaceId = await this._getSpaceId(); + + return await this._client.bulkGet(objects, { + documentFilter: (doc) => { + if (!doc.found) return true; + + const { type, spaceId } = doc._source; + + if (this._isTypeSpaceAware(type)) { + return spaceId === thisSpaceId; + } + + return true; + }, + resultDecorator(savedObject, doc) { + savedObject.attributes = { + ...savedObject.attributes, + spaceId: doc._source.spaceId + }; + return savedObject; + } + }); + } + + async get(type, id) { + // ES 'get' does not support queries, so we have to filter results after the fact. + let thisSpaceId; + + if (this._isTypeSpaceAware(type)) { + thisSpaceId = await this._getSpaceId(); + } + + return await this._client.get(type, id, { + responseInterceptor: (response) => { + if (!this._isTypeSpaceAware(type)) { + return response; + } + + if (response.found && response.status !== 404) { + const { spaceId } = response._source; + if (spaceId !== thisSpaceId) { + response.found = false; + response._source = {}; + } + } + + return response; + } + }); + } + + async update(type, id, attributes, options = {}) { + return await this._client.update(type, id, attributes, options); + } + + _isTypeSpaceAware(type) { + return type !== 'space'; + } + + async _getSpaceId() { + if (!this._spaceId) { + const { + saved_objects: spaces = [] + } = await this.find({ + type: 'space', + search: `"${this._spaceUrlContext}"`, + search_fields: ['urlContext'], + }); + + if (spaces.length > 0) { + this._spaceId = spaces[0].id; + } + } + + return this._spaceId; + } +}