Typings for saved object client (#29951)

* WIP typings for saved object client

* Move more files to TS

* type saved objects client

* clean up typings for saved object client

* tie typings form server and client for saved objects together

* add missing html import typing to x-pack

* Add missing buildSourcePatterns

* Removed accidental comma

* add typings for saved_object_client tests and fix test cases

* duplicate case_conversion helpers for the moment

* Address PR review

* Fix some documentation

* Replace ts-ignore by any imports

* Remove expect.js from test

* Add more typings to prevent CI failure
This commit is contained in:
Tim Roes 2019-02-07 12:43:17 +01:00 committed by GitHub
parent ee6135feaa
commit fb2f0569e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 970 additions and 733 deletions

View file

@ -18,8 +18,11 @@
*/
export {
MigrationVersion,
SavedObject,
SavedObjectAttributes,
SavedObjectsClient,
SavedObjectsClientWrapperFactory,
SavedObjectReference,
SavedObjectsService,
} from './service';

View file

@ -19,4 +19,14 @@
export { SavedObjectsService } from './create_saved_objects_service';
export { SavedObjectsClientWrapperFactory } from './lib';
export { SavedObject, SavedObjectsClient } from './saved_objects_client';
export {
FindOptions,
GetResponse,
UpdateResponse,
CreateResponse,
MigrationVersion,
SavedObject,
SavedObjectAttributes,
SavedObjectsClient,
SavedObjectReference,
} from './saved_objects_client';

View file

@ -26,6 +26,7 @@ export interface BaseOptions {
export interface CreateOptions extends BaseOptions {
id?: string;
override?: boolean;
references?: SavedObjectReference[];
}
export interface BulkCreateObject<T extends SavedObjectAttributes = any> {
@ -48,6 +49,7 @@ export interface FindOptions extends BaseOptions {
fields?: string[];
search?: string;
searchFields?: string[];
hasReference?: { type: string; id: string };
}
export interface FindResponse<T extends SavedObjectAttributes = any> {
@ -71,6 +73,10 @@ export interface BulkGetResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
}
export interface MigrationVersion {
[pluginName: string]: string;
}
export interface SavedObjectAttributes {
[key: string]: SavedObjectAttributes | string | number | boolean | null;
}
@ -85,6 +91,7 @@ export interface SavedObject<T extends SavedObjectAttributes = any> {
};
attributes: T;
references: SavedObjectReference[];
migrationVersion?: MigrationVersion;
}
export interface SavedObjectReference {
@ -93,6 +100,10 @@ export interface SavedObjectReference {
id: string;
}
export type GetResponse<T extends SavedObjectAttributes = any> = SavedObject<T>;
export type CreateResponse<T extends SavedObjectAttributes = any> = SavedObject<T>;
export type UpdateResponse<T extends SavedObjectAttributes = any> = SavedObject<T>;
export declare class SavedObjectsClient {
public static errors: typeof errors;
public errors: typeof errors;
@ -103,7 +114,7 @@ export declare class SavedObjectsClient {
type: string,
attributes: T,
options?: CreateOptions
): Promise<SavedObject<T>>;
): Promise<CreateResponse<T>>;
public bulkCreate<T extends SavedObjectAttributes = any>(
objects: Array<BulkCreateObject<T>>,
options?: CreateOptions
@ -120,11 +131,11 @@ export declare class SavedObjectsClient {
type: string,
id: string,
options?: BaseOptions
): Promise<SavedObject<T>>;
): Promise<GetResponse<T>>;
public update<T extends SavedObjectAttributes = any>(
type: string,
id: string,
attributes: Partial<T>,
options?: UpdateOptions
): Promise<SavedObject<T>>;
): Promise<UpdateResponse<T>>;
}

View file

@ -18,6 +18,7 @@
*/
import { Brand } from '../../../core/public/chrome';
import { SavedObjectsClient } from '../saved_objects';
import { BreadcrumbsApi } from './api/breadcrumbs';
import { HelpExtensionApi } from './api/help_extension';
import { ChromeNavLinks } from './api/nav';
@ -34,6 +35,7 @@ declare interface Chrome extends ChromeNavLinks {
getBasePath(): string;
getXsrfToken(): string;
getKibanaVersion(): string;
getSavedObjectsClient(): SavedObjectsClient;
getUiSettingsClient(): any;
setVisible(visible: boolean): any;
getInjected(key: string, defaultValue?: any): any;
@ -43,7 +45,6 @@ declare interface Chrome extends ChromeNavLinks {
addApplicationClass(classNames: string | string[]): this;
removeApplicationClass(classNames: string | string[]): this;
getApplicationClasses(): string;
getSavedObjectsClient(): any;
}
declare const chrome: Chrome;

View file

@ -24,16 +24,20 @@ import uiRoutes from '../routes';
import template from './error_auto_create_index.html';
uiRoutes
.when('/error/action.auto_create_index', {
template,
k7Breadcrumbs: () => [{ text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { defaultMessage: 'Error' }) }],
});
uiRoutes.when('/error/action.auto_create_index', {
template,
k7Breadcrumbs: () => [
{
text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', {
defaultMessage: 'Error',
}),
},
],
});
export function isAutoCreateIndexError(error) {
export function isAutoCreateIndexError(error: object) {
return (
get(error, 'res.status') === 503 &&
get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
get(error, 'res.status') === 503 && get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
);
}

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { kfetch, addInterceptor, KFetchOptions } from './kfetch';
export { kfetch, addInterceptor, KFetchOptions, KFetchQuery } from './kfetch';
export { kfetchAbortable } from './kfetch_abortable';

View file

@ -24,7 +24,7 @@ import url from 'url';
import chrome from '../chrome';
import { KFetchError } from './kfetch_error';
interface KFetchQuery {
export interface KFetchQuery {
[key: string]: string | number | boolean | undefined;
}

20
src/ui/public/promises/index.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
/*
* 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 { PromiseService } from './promises';

25
src/ui/public/promises/promises.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
/*
* 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 interface PromiseService {
resolve: <T>(value: T | PromiseLike<T>) => ng.IPromise<T>;
// TODO: add additional typing
[key: string]: any;
}

View file

@ -1,368 +0,0 @@
/*
* 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.
*/
jest.mock('ui/kfetch', () => ({}));
import sinon from 'sinon';
import expect from 'expect.js';
import { SavedObjectsClient } from '../saved_objects_client';
import { SavedObject } from '../saved_object';
describe('SavedObjectsClient', () => {
const doc = {
id: 'AVwSwFxtcMV38qjDZoQg',
type: 'config',
attributes: { title: 'Example title' },
version: 'foo'
};
let kfetchStub;
let savedObjectsClient;
beforeEach(() => {
kfetchStub = sinon.stub();
require('ui/kfetch').kfetch = async (...args) => {
return kfetchStub(...args);
};
savedObjectsClient = new SavedObjectsClient();
});
describe('#_getPath', () => {
test('returns without arguments', () => {
const path = savedObjectsClient._getPath();
const expected = `/api/saved_objects/`;
expect(path).to.be(expected);
});
test('appends path', () => {
const path = savedObjectsClient._getPath(['some', 'path']);
const expected = `/api/saved_objects/some/path`;
expect(path).to.be(expected);
});
});
describe('#_request', () => {
const body = { foo: 'Foo', bar: 'Bar' };
test('passes options to kfetch', () => {
kfetchStub.withArgs({
method: 'POST',
pathname: '/api/path',
query: undefined,
body: JSON.stringify(body)
}).returns(Promise.resolve({}));
savedObjectsClient._request({ method: 'POST', path: '/api/path', body });
sinon.assert.calledOnce(kfetchStub);
});
test('throws error when body is provided for GET', async () => {
try {
await savedObjectsClient._request({ method: 'GET', path: '/api/path', body });
expect().fail('should have error');
} catch (e) {
expect(e.message).to.eql('body not permitted for GET requests');
}
});
});
describe('#get', () => {
beforeEach(() => {
kfetchStub.withArgs({
method: 'POST',
pathname: `/api/saved_objects/_bulk_get`,
query: undefined,
body: sinon.match.any
}).returns(Promise.resolve({ saved_objects: [doc] }));
});
test('returns a promise', () => {
expect(savedObjectsClient.get('index-pattern', 'logstash-*')).to.be.a(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.get();
expect().fail('should have error');
} catch (e) {
expect(e.message).to.be('requires type and id');
}
});
test('requires id', async () => {
try {
await savedObjectsClient.get('index-pattern');
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be('requires type and id');
}
});
test('resolves with instantiated SavedObject', async () => {
const response = await savedObjectsClient.get(doc.type, doc.id);
expect(response).to.be.a(SavedObject);
expect(response.type).to.eql('config');
expect(response.get('title')).to.eql('Example title');
expect(response._client).to.be.a(SavedObjectsClient);
});
test('makes HTTP call', async () => {
await savedObjectsClient.get(doc.type, doc.id);
sinon.assert.calledOnce(kfetchStub);
});
test('handles HTTP call when it fails', async () => {
kfetchStub.withArgs({
method: 'POST',
pathname: `/api/saved_objects/_bulk_get`,
query: undefined,
body: sinon.match.any
}).rejects(new Error('Request failed'));
try {
await savedObjectsClient.get(doc.type, doc.id);
throw new Error('should have error');
} catch (e) {
expect(e.message).to.be('Request failed');
}
});
});
describe('#delete', () => {
beforeEach(() => {
kfetchStub.withArgs({
method: 'DELETE',
pathname: `/api/saved_objects/index-pattern/logstash-*`,
query: undefined,
body: undefined,
}).returns(Promise.resolve({}));
});
test('returns a promise', () => {
expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).to.be.a(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.delete();
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be('requires type and id');
}
});
test('requires id', async () => {
try {
await savedObjectsClient.delete('index-pattern');
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be('requires type and id');
}
});
test('makes HTTP call', () => {
savedObjectsClient.delete('index-pattern', 'logstash-*');
sinon.assert.calledOnce(kfetchStub);
});
});
describe('#update', () => {
const requireMessage = 'requires type, id and attributes';
beforeEach(() => {
kfetchStub.withArgs({
method: 'PUT',
pathname: `/api/saved_objects/index-pattern/logstash-*`,
query: undefined,
body: sinon.match.any
}).returns(Promise.resolve({ data: 'api-response' }));
});
test('returns a promise', () => {
expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).to.be.a(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.update();
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be(requireMessage);
}
});
test('requires id', async () => {
try {
await savedObjectsClient.update('index-pattern');
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be(requireMessage);
}
});
test('requires attributes', async () => {
try {
await savedObjectsClient.update('index-pattern', 'logstash-*');
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be(requireMessage);
}
});
test('makes HTTP call', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
const body = { attributes, version: 'foo' };
const options = { version: 'foo' };
savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
body: JSON.stringify(body)
}));
});
});
describe('#create', () => {
const requireMessage = 'requires type and attributes';
beforeEach(() => {
kfetchStub.withArgs({
method: 'POST',
pathname: `/api/saved_objects/index-pattern`,
query: undefined,
body: sinon.match.any
}).returns(Promise.resolve({}));
});
test('returns a promise', () => {
expect(savedObjectsClient.create('index-pattern', {})).to.be.a(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.create();
expect().throw('should have error');
} catch (e) {
expect(e.message).to.be(requireMessage);
}
});
test('allows for id to be provided', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
const path = `/api/saved_objects/index-pattern/myId`;
kfetchStub.withArgs({
method: 'POST',
pathname: path,
query: undefined,
body: sinon.match.any
}).returns(Promise.resolve({}));
savedObjectsClient.create('index-pattern', attributes, { id: 'myId' });
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
pathname: path
}));
});
test('makes HTTP call', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
savedObjectsClient.create('index-pattern', attributes);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
pathname: sinon.match.string,
body: JSON.stringify({ attributes }),
}));
});
});
describe('#bulk_create', () => {
beforeEach(() => {
kfetchStub.withArgs({
method: 'POST',
pathname: `/api/saved_objects/_bulk_create`,
query: sinon.match.any,
body: sinon.match.any
}).returns(Promise.resolve({ saved_objects: [doc] }));
});
test('returns a promise', () => {
expect(savedObjectsClient.bulkCreate([doc], {})).to.be.a(Promise);
});
test('resolves with instantiated SavedObjects', async () => {
const response = await savedObjectsClient.bulkCreate([doc], {});
expect(response).to.have.property('savedObjects');
expect(response.savedObjects.length).to.eql(1);
expect(response.savedObjects[0]).to.be.a(SavedObject);
});
test('makes HTTP call', async () => {
await savedObjectsClient.bulkCreate([doc], {});
sinon.assert.calledOnce(kfetchStub);
});
});
describe('#find', () => {
const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' };
beforeEach(() => {
kfetchStub.returns(Promise.resolve({ saved_objects: [object] }));
});
test('returns a promise', () => {
expect(savedObjectsClient.find()).to.be.a(Promise);
});
test('accepts type', () => {
const body = { type: 'index-pattern', invalid: true };
savedObjectsClient.find(body);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
pathname: `/api/saved_objects/_find`,
query: { type: 'index-pattern', invalid: true }
}));
});
test('accepts fields', () => {
const body = { fields: ['title', 'description'] };
savedObjectsClient.find(body);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
pathname: `/api/saved_objects/_find`,
query: { fields: [ 'title', 'description' ] }
}));
});
test('accepts from/size', () => {
const body = { from: 50, size: 10 };
savedObjectsClient.find(body);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.alwaysCalledWith(kfetchStub, sinon.match({
pathname: `/api/saved_objects/_find`,
query: { from: 50, size: 10 }
}));
});
});
});

View file

@ -18,6 +18,9 @@
*/
import { find } from 'lodash';
import { SavedObjectAttributes } from '../../../server/saved_objects';
import { SavedObject } from './saved_object';
import { SavedObjectsClient } from './saved_objects_client';
/**
* Returns an object matching a given title
@ -27,22 +30,30 @@ import { find } from 'lodash';
* @param title {string}
* @returns {Promise<SavedObject|undefined>}
*/
export function findObjectByTitle(savedObjectsClient, type, title) {
if (!title) return Promise.resolve();
export function findObjectByTitle<T extends SavedObjectAttributes>(
savedObjectsClient: SavedObjectsClient,
type: string,
title: string
): Promise<SavedObject<T> | void> {
if (!title) {
return Promise.resolve();
}
// Elastic search will return the most relevant results first, which means exact matches should come
// first, and so we shouldn't need to request everything. Using 10 just to be on the safe side.
return savedObjectsClient.find({
type,
perPage: 10,
search: `"${title}"`,
searchFields: ['title'],
fields: ['title']
}).then(response => {
const match = find(response.savedObjects, (obj) => {
return obj.get('title').toLowerCase() === title.toLowerCase();
});
return savedObjectsClient
.find<T>({
type,
perPage: 10,
search: `"${title}"`,
searchFields: ['title'],
fields: ['title'],
})
.then(response => {
const match = find(response.savedObjects, obj => {
return obj.get('title').toLowerCase() === title.toLowerCase();
});
return match;
});
return match;
});
}

View file

@ -1,67 +0,0 @@
/*
* 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 _ from 'lodash';
export class SavedObject {
constructor(client, { id, type, version, attributes, error, migrationVersion, references } = {}) {
this._client = client;
this.id = id;
this.type = type;
this.attributes = attributes || {};
this.references = references || [];
this._version = version;
this.migrationVersion = migrationVersion;
if (error) {
this.error = error;
}
}
get(key) {
return _.get(this.attributes, key);
}
set(key, value) {
return _.set(this.attributes, key, value);
}
has(key) {
return _.has(this.attributes, key);
}
save() {
if (this.id) {
return this._client.update(
this.type,
this.id,
this.attributes,
{
migrationVersion: this.migrationVersion,
references: this.references,
},
);
} else {
return this._client.create(this.type, this.attributes, { migrationVersion: this.migrationVersion, references: this.references });
}
}
delete() {
return this._client.delete(this.type, this.id);
}
}

View file

@ -0,0 +1,81 @@
/*
* 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 { get, has, set } from 'lodash';
import {
SavedObject as SavedObjectType,
SavedObjectAttributes,
} from '../../../server/saved_objects';
import { SavedObjectsClient } from './saved_objects_client';
export class SavedObject<T extends SavedObjectAttributes> {
public attributes: T;
// tslint:disable-next-line variable-name We want to use the same interface this class had in JS
public _version?: SavedObjectType<T>['version'];
public id: SavedObjectType<T>['id'];
public type: SavedObjectType<T>['type'];
public migrationVersion: SavedObjectType<T>['migrationVersion'];
public error: SavedObjectType<T>['error'];
public references: SavedObjectType<T>['references'];
constructor(
private client: SavedObjectsClient,
{ id, type, version, attributes, error, references, migrationVersion }: SavedObjectType<T>
) {
this.id = id;
this.type = type;
this.attributes = attributes || {};
this.references = references || [];
this._version = version;
this.migrationVersion = migrationVersion;
if (error) {
this.error = error;
}
}
public get(key: string): any {
return get(this.attributes, key);
}
public set(key: string, value: any): T {
return set(this.attributes, key, value);
}
public has(key: string): boolean {
return has(this.attributes, key);
}
public save() {
if (this.id) {
return this.client.update(this.type, this.id, this.attributes, {
migrationVersion: this.migrationVersion,
references: this.references,
});
} else {
return this.client.create(this.type, this.attributes, {
migrationVersion: this.migrationVersion,
references: this.references,
});
}
}
public delete() {
return this.client.delete(this.type, this.id);
}
}

View file

@ -22,5 +22,5 @@ import { uiRegistry } from '../registry/_registry';
export const SavedObjectRegistryProvider = uiRegistry({
name: 'savedObjects',
index: ['loaderProperties.name'],
order: ['loaderProperties.name']
order: ['loaderProperties.name'],
});

View file

@ -1,260 +0,0 @@
/*
* 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 _ from 'lodash';
import { resolve as resolveUrl } from 'url';
import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../../../utils/case_conversion';
import { SavedObject } from './saved_object';
import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index';
import { kfetch } from 'ui/kfetch';
const join = (...uriComponents) => (
uriComponents.filter(Boolean).map(encodeURIComponent).join('/')
);
/**
* Interval that requests are batched for
* @type {integer}
*/
const BATCH_INTERVAL = 100;
const API_BASE_URL = '/api/saved_objects/';
export class SavedObjectsClient {
constructor() {
this.batchQueue = [];
}
/**
* Persists an object
*
* @param {string} type
* @param {object} [attributes={}]
* @param {object} [options={}]
* @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion]
* @property {array} [options.references] [{ name, type, id }]
* @returns {promise} - SavedObject({ id, type, version, attributes })
*/
create = (type, attributes = {}, options = {}) => {
if (!type || !attributes) {
return Promise.reject(new Error('requires type and attributes'));
}
const path = this._getPath([type, options.id]);
const query = _.pick(options, ['overwrite']);
return this
._request({
method: 'POST',
path,
query,
body: {
attributes,
migrationVersion: options.migrationVersion,
references: options.references,
},
})
.catch(error => {
if (isAutoCreateIndexError(error)) {
return showAutoCreateIndexErrorPage();
}
throw error;
})
.then(resp => this._createSavedObject(resp));
}
/**
* Creates multiple documents at once
*
* @param {array} objects - [{ type, id, attributes, references, migrationVersion }]
* @param {object} [options={}]
* @property {boolean} [options.overwrite=false]
* @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]}
*/
bulkCreate = (objects = [], options = {}) => {
const path = this._getPath(['_bulk_create']);
const query = _.pick(options, ['overwrite']);
return this._request({ method: 'POST', path, query, body: objects }).then(resp => {
resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d));
return keysToCamelCaseShallow(resp);
});
}
/**
* Deletes an object
*
* @param {string} type
* @param {string} id
* @returns {promise}
*/
delete = (type, id) => {
if (!type || !id) {
return Promise.reject(new Error('requires type and id'));
}
return this._request({ method: 'DELETE', path: this._getPath([type, id]) });
}
/**
* Search for objects
*
* @param {object} [options={}]
* @property {string} options.type
* @property {string} options.search
* @property {string} options.defaultSearchOperator
* @property {string} options.searchFields - see Elasticsearch Simple Query String
* Query field argument for more information
* @property {integer} [options.page=1]
* @property {integer} [options.perPage=20]
* @property {array} options.fields
* @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]}
*/
find = (options = {}) => {
const path = this._getPath(['_find']);
const query = keysToSnakeCaseShallow(options);
return this._request({ method: 'GET', path, query }).then(resp => {
resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d));
return keysToCamelCaseShallow(resp);
});
}
/**
* Fetches a single object
*
* @param {string} type
* @param {string} id
* @returns {promise} - SavedObject({ id, type, version, attributes })
*/
get = (type, id) => {
if (!type || !id) {
return Promise.reject(new Error('requires type and id'));
}
return new Promise((resolve, reject) => {
this.batchQueue.push({ type, id, resolve, reject });
this._processBatchQueue();
});
}
/**
* Returns an array of objects by id
*
* @param {array} objects - an array ids, or an array of objects containing id and optionally type
* @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ] }
* @example
*
* bulkGet([
* { id: 'one', type: 'config' },
* { id: 'foo', type: 'index-pattern' }
* ])
*/
bulkGet = (objects = []) => {
const path = this._getPath(['_bulk_get']);
const filteredObjects = objects.map(obj => _.pick(obj, ['id', 'type']));
return this._request({ method: 'POST', path, body: filteredObjects }).then(resp => {
resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d));
return keysToCamelCaseShallow(resp);
});
}
/**
* Updates an object
*
* @param {string} type
* @param {string} id
* @param {object} attributes
* @param {object} options
* @prop {integer} options.version - ensures version matches that of persisted object
* @prop {object} options.migrationVersion - The optional migrationVersion of this document
* @prop {array} option.references - the references of the saved object
* @returns {promise}
*/
update(type, id, attributes, { version, migrationVersion, references } = {}) {
if (!type || !id || !attributes) {
return Promise.reject(new Error('requires type, id and attributes'));
}
const path = this._getPath([type, id]);
const body = {
attributes,
migrationVersion,
references,
version
};
return this._request({ method: 'PUT', path, body }).then(resp => {
return this._createSavedObject(resp);
});
}
/**
* Throttled processing of get requests into bulk requests at 100ms interval
*/
_processBatchQueue = _.throttle(() => {
const queue = _.cloneDeep(this.batchQueue);
this.batchQueue = [];
this.bulkGet(queue).then(({ savedObjects }) => {
queue.forEach((queueItem) => {
const foundObject = savedObjects.find(savedObject => {
return savedObject.id === queueItem.id & savedObject.type === queueItem.type;
});
if (!foundObject) {
return queueItem.resolve(this._createSavedObject(_.pick(queueItem, ['id', 'type'])));
}
queueItem.resolve(foundObject);
});
}).catch((err) => {
queue.forEach((queueItem) => {
queueItem.reject(err);
});
});
}, BATCH_INTERVAL, { leading: false });
_createSavedObject(options) {
return new SavedObject(this, options);
}
_getPath(path) {
if (!path) {
return API_BASE_URL;
}
return resolveUrl(API_BASE_URL, join(...path));
}
_request({ method, path, query, body }) {
if (method === 'GET' && body) {
return Promise.reject(new Error('body not permitted for GET requests'));
}
return kfetch({ method, pathname: path, query, body: JSON.stringify(body) });
}
}

View file

@ -0,0 +1,357 @@
/*
* 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.
*/
jest.mock('ui/kfetch', () => ({}));
import * as sinon from 'sinon';
import { FindOptions } from '../../../server/saved_objects/service';
import { SavedObject } from './saved_object';
import { SavedObjectsClient } from './saved_objects_client';
describe('SavedObjectsClient', () => {
const doc = {
id: 'AVwSwFxtcMV38qjDZoQg',
type: 'config',
attributes: { title: 'Example title' },
version: 'foo',
};
let kfetchStub: sinon.SinonStub;
let savedObjectsClient: SavedObjectsClient;
beforeEach(() => {
kfetchStub = sinon.stub();
require('ui/kfetch').kfetch = async (...args: any[]) => {
return kfetchStub(...args);
};
savedObjectsClient = new SavedObjectsClient();
});
describe('#get', () => {
beforeEach(() => {
kfetchStub
.withArgs({
method: 'POST',
pathname: `/api/saved_objects/_bulk_get`,
query: undefined,
body: sinon.match.any,
})
.returns(Promise.resolve({ saved_objects: [doc] }));
});
test('returns a promise', () => {
expect(savedObjectsClient.get('index-pattern', 'logstash-*')).toBeInstanceOf(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.get(undefined as any, undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe('requires type and id');
}
});
test('requires id', async () => {
try {
await savedObjectsClient.get('index-pattern', undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe('requires type and id');
}
});
test('resolves with instantiated SavedObject', async () => {
const response = await savedObjectsClient.get(doc.type, doc.id);
expect(response).toBeInstanceOf(SavedObject);
expect(response.type).toBe('config');
expect(response.get('title')).toBe('Example title');
});
test('makes HTTP call', async () => {
await savedObjectsClient.get(doc.type, doc.id);
sinon.assert.calledOnce(kfetchStub);
});
test('handles HTTP call when it fails', async () => {
kfetchStub
.withArgs({
method: 'POST',
pathname: `/api/saved_objects/_bulk_get`,
query: undefined,
body: sinon.match.any,
})
.rejects(new Error('Request failed'));
try {
await savedObjectsClient.get(doc.type, doc.id);
throw new Error('should have error');
} catch (e) {
expect(e.message).toBe('Request failed');
}
});
});
describe('#delete', () => {
beforeEach(() => {
kfetchStub
.withArgs({
method: 'DELETE',
pathname: `/api/saved_objects/index-pattern/logstash-*`,
query: undefined,
body: undefined,
})
.returns(Promise.resolve({}));
});
test('returns a promise', () => {
expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).toBeInstanceOf(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.delete(undefined as any, undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe('requires type and id');
}
});
test('requires id', async () => {
try {
await savedObjectsClient.delete('index-pattern', undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe('requires type and id');
}
});
test('makes HTTP call', () => {
savedObjectsClient.delete('index-pattern', 'logstash-*');
sinon.assert.calledOnce(kfetchStub);
});
});
describe('#update', () => {
const requireMessage = 'requires type, id and attributes';
beforeEach(() => {
kfetchStub
.withArgs({
method: 'PUT',
pathname: `/api/saved_objects/index-pattern/logstash-*`,
query: undefined,
body: sinon.match.any,
})
.returns(Promise.resolve({ data: 'api-response' }));
});
test('returns a promise', () => {
expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).toBeInstanceOf(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.update(undefined as any, undefined as any, undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe(requireMessage);
}
});
test('requires id', async () => {
try {
await savedObjectsClient.update('index-pattern', undefined as any, undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe(requireMessage);
}
});
test('requires attributes', async () => {
try {
await savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe(requireMessage);
}
});
test('makes HTTP call', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
const body = { attributes, version: 'foo' };
const options = { version: 'foo' };
savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(
kfetchStub,
sinon.match({
body: JSON.stringify(body),
})
);
});
});
describe('#create', () => {
const requireMessage = 'requires type and attributes';
beforeEach(() => {
kfetchStub
.withArgs({
method: 'POST',
pathname: `/api/saved_objects/index-pattern`,
query: undefined,
body: sinon.match.any,
})
.returns(Promise.resolve({}));
});
test('returns a promise', () => {
expect(savedObjectsClient.create('index-pattern', {})).toBeInstanceOf(Promise);
});
test('requires type', async () => {
try {
await savedObjectsClient.create(undefined as any, undefined as any);
fail('should have error');
} catch (e) {
expect(e.message).toBe(requireMessage);
}
});
test('allows for id to be provided', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
const path = `/api/saved_objects/index-pattern/myId`;
kfetchStub
.withArgs({
method: 'POST',
pathname: path,
query: undefined,
body: sinon.match.any,
})
.returns(Promise.resolve({}));
savedObjectsClient.create('index-pattern', attributes, { id: 'myId' });
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(
kfetchStub,
sinon.match({
pathname: path,
})
);
});
test('makes HTTP call', () => {
const attributes = { foo: 'Foo', bar: 'Bar' };
savedObjectsClient.create('index-pattern', attributes);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(
kfetchStub,
sinon.match({
pathname: sinon.match.string,
body: JSON.stringify({ attributes }),
})
);
});
});
describe('#bulk_create', () => {
beforeEach(() => {
kfetchStub
.withArgs({
method: 'POST',
pathname: `/api/saved_objects/_bulk_create`,
query: sinon.match.any,
body: sinon.match.any,
})
.returns(Promise.resolve({ saved_objects: [doc] }));
});
test('returns a promise', () => {
expect(savedObjectsClient.bulkCreate([doc], {})).toBeInstanceOf(Promise);
});
test('resolves with instantiated SavedObjects', async () => {
const response = await savedObjectsClient.bulkCreate([doc], {});
expect(response).toHaveProperty('savedObjects');
expect(response.savedObjects.length).toBe(1);
expect(response.savedObjects[0]).toBeInstanceOf(SavedObject);
});
test('makes HTTP call', async () => {
await savedObjectsClient.bulkCreate([doc], {});
sinon.assert.calledOnce(kfetchStub);
});
});
describe('#find', () => {
const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' };
beforeEach(() => {
kfetchStub.returns(Promise.resolve({ saved_objects: [object] }));
});
test('returns a promise', () => {
expect(savedObjectsClient.find()).toBeInstanceOf(Promise);
});
test('accepts type', () => {
const body = { type: 'index-pattern', invalid: true };
savedObjectsClient.find(body);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(
kfetchStub,
sinon.match({
pathname: `/api/saved_objects/_find`,
query: { type: 'index-pattern', invalid: true },
})
);
});
test('accepts fields', () => {
const body = { fields: ['title', 'description'] };
savedObjectsClient.find(body);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.calledWithExactly(
kfetchStub,
sinon.match({
pathname: `/api/saved_objects/_find`,
query: { fields: ['title', 'description'] },
})
);
});
test('accepts pagination params', () => {
const options: FindOptions = { perPage: 10, page: 6 };
savedObjectsClient.find(options);
sinon.assert.calledOnce(kfetchStub);
sinon.assert.alwaysCalledWith(
kfetchStub,
sinon.match({
pathname: `/api/saved_objects/_find`,
query: { per_page: 10, page: 6 },
})
);
});
});
});

View file

@ -0,0 +1,358 @@
/*
* 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 { cloneDeep, pick, throttle } from 'lodash';
import { resolve as resolveUrl } from 'url';
import {
MigrationVersion,
SavedObject as PlainSavedObject,
SavedObjectAttributes,
SavedObjectReference,
SavedObjectsClient as SavedObjectsApi,
} from '../../../../src/server/saved_objects';
import {
CreateResponse,
FindOptions,
UpdateResponse,
} from '../../../../src/server/saved_objects/service';
import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index';
import { kfetch, KFetchQuery } from '../kfetch';
import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion';
import { SavedObject } from './saved_object';
interface RequestParams {
method: 'POST' | 'GET' | 'PUT' | 'DELETE';
path: string;
query?: KFetchQuery;
body?: object;
}
interface CreateOptions {
id?: string;
overwrite?: boolean;
migrationVersion?: MigrationVersion;
references?: SavedObjectReference[];
}
interface BulkCreateOptions<T extends SavedObjectAttributes = SavedObjectAttributes>
extends CreateOptions {
type: string;
attributes: T;
}
interface UpdateOptions {
version?: string;
migrationVersion?: MigrationVersion;
references?: SavedObjectReference[];
}
interface BatchResponse<T extends SavedObjectAttributes = SavedObjectAttributes> {
savedObjects: Array<SavedObject<T>>;
}
interface FindResults<T extends SavedObjectAttributes = SavedObjectAttributes>
extends BatchResponse<T> {
total: number;
perPage: number;
page: number;
}
interface BatchQueueEntry {
type: string;
id: string;
resolve: <T extends SavedObjectAttributes>(value: SavedObject<T> | PlainSavedObject<T>) => void;
reject: (reason?: any) => void;
}
const join = (...uriComponents: Array<string | undefined>) =>
uriComponents
.filter((comp): comp is string => Boolean(comp))
.map(encodeURIComponent)
.join('/');
/**
* Interval that requests are batched for
* @type {integer}
*/
const BATCH_INTERVAL = 100;
const API_BASE_URL = '/api/saved_objects/';
export class SavedObjectsClient {
/**
* Throttled processing of get requests into bulk requests at 100ms interval
*/
private processBatchQueue = throttle(
() => {
const queue = cloneDeep(this.batchQueue);
this.batchQueue = [];
this.bulkGet(queue)
.then(({ savedObjects }) => {
queue.forEach(queueItem => {
const foundObject = savedObjects.find(savedObject => {
return savedObject.id === queueItem.id && savedObject.type === queueItem.type;
});
if (!foundObject) {
return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type'])));
}
queueItem.resolve(foundObject);
});
})
.catch(err => {
queue.forEach(queueItem => {
queueItem.reject(err);
});
});
},
BATCH_INTERVAL,
{ leading: false }
);
private batchQueue: BatchQueueEntry[];
constructor() {
this.batchQueue = [];
}
/**
* Persists an object
*
* @param {string} type
* @param {object} [attributes={}]
* @param {object} [options={}]
* @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion]
* @returns
*/
public create = <T extends SavedObjectAttributes>(
type: string,
attributes: T,
options: CreateOptions = {}
): Promise<SavedObject<T>> => {
if (!type || !attributes) {
return Promise.reject(new Error('requires type and attributes'));
}
const path = this.getPath([type, options.id]);
const query = {
overwrite: options.overwrite,
};
const createRequest: Promise<CreateResponse<T>> = this.request({
method: 'POST',
path,
query,
body: {
attributes,
migrationVersion: options.migrationVersion,
references: options.references,
},
});
return createRequest
.then(resp => this.createSavedObject(resp))
.catch((error: object) => {
if (isAutoCreateIndexError(error)) {
showAutoCreateIndexErrorPage();
}
throw error;
});
};
/**
* Creates multiple documents at once
*
* @param {array} objects - [{ type, id, attributes, references, migrationVersion }]
* @param {object} [options={}]
* @property {boolean} [options.overwrite=false]
* @returns The result of the create operation containing created saved objects.
*/
public bulkCreate = (objects: BulkCreateOptions[] = [], options: KFetchQuery = {}) => {
const path = this.getPath(['_bulk_create']);
const query = pick(options, ['overwrite']) as Pick<KFetchQuery, 'overwrite'>;
const request: ReturnType<SavedObjectsApi['bulkCreate']> = this.request({
method: 'POST',
path,
query,
body: objects,
});
return request.then(resp => {
resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d));
return keysToCamelCaseShallow(resp) as BatchResponse;
});
};
/**
* Deletes an object
*
* @param type
* @param id
* @returns
*/
public delete = (type: string, id: string): ReturnType<SavedObjectsApi['delete']> => {
if (!type || !id) {
return Promise.reject(new Error('requires type and id'));
}
return this.request({ method: 'DELETE', path: this.getPath([type, id]) });
};
/**
* Search for objects
*
* @param {object} [options={}]
* @property {string} options.type
* @property {string} options.search
* @property {string} options.searchFields - see Elasticsearch Simple Query String
* Query field argument for more information
* @property {integer} [options.page=1]
* @property {integer} [options.perPage=20]
* @property {array} options.fields
* @property {object} [options.hasReference] - { type, id }
* @returns A find result with objects matching the specified search.
*/
public find = <T extends SavedObjectAttributes>(
options: FindOptions = {}
): Promise<FindResults<T>> => {
const path = this.getPath(['_find']);
const query = keysToSnakeCaseShallow(options);
const request: ReturnType<SavedObjectsApi['find']> = this.request({
method: 'GET',
path,
query,
});
return request.then(resp => {
resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d));
return keysToCamelCaseShallow(resp) as FindResults<T>;
});
};
/**
* Fetches a single object
*
* @param {string} type
* @param {string} id
* @returns The saved object for the given type and id.
*/
public get = <T extends SavedObjectAttributes>(
type: string,
id: string
): Promise<SavedObject<T>> => {
if (!type || !id) {
return Promise.reject(new Error('requires type and id'));
}
return new Promise((resolve, reject) => {
this.batchQueue.push({ type, id, resolve, reject } as BatchQueueEntry);
this.processBatchQueue();
});
};
/**
* Returns an array of objects by id
*
* @param {array} objects - an array ids, or an array of objects containing id and optionally type
* @returns The saved objects with the given type and ids requested
* @example
*
* bulkGet([
* { id: 'one', type: 'config' },
* { id: 'foo', type: 'index-pattern' }
* ])
*/
public bulkGet = (objects: Array<{ id: string; type: string }> = []) => {
const path = this.getPath(['_bulk_get']);
const filteredObjects = objects.map(obj => pick(obj, ['id', 'type']));
const request: ReturnType<SavedObjectsApi['bulkGet']> = this.request({
method: 'POST',
path,
body: filteredObjects,
});
return request.then(resp => {
resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d));
return keysToCamelCaseShallow(resp) as BatchResponse;
});
};
/**
* Updates an object
*
* @param {string} type
* @param {string} id
* @param {object} attributes
* @param {object} options
* @prop {integer} options.version - ensures version matches that of persisted object
* @prop {object} options.migrationVersion - The optional migrationVersion of this document
* @returns
*/
public update<T extends SavedObjectAttributes>(
type: string,
id: string,
attributes: T,
{ version, migrationVersion, references }: UpdateOptions = {}
): Promise<SavedObject<T>> {
if (!type || !id || !attributes) {
return Promise.reject(new Error('requires type, id and attributes'));
}
const path = this.getPath([type, id]);
const body = {
attributes,
migrationVersion,
references,
version,
};
const request: Promise<UpdateResponse<T>> = this.request({
method: 'PUT',
path,
body,
});
return request.then(resp => {
return this.createSavedObject(resp);
});
}
private createSavedObject<T extends SavedObjectAttributes>(
options: PlainSavedObject<T>
): SavedObject<T> {
return new SavedObject(this, options);
}
private getPath(path: Array<string | undefined>): string {
return resolveUrl(API_BASE_URL, join(...path));
}
private request({ method, path, query, body }: RequestParams) {
if (method === 'GET' && body) {
return Promise.reject(new Error('body not permitted for GET requests'));
}
return kfetch({ method, pathname: path, query, body: JSON.stringify(body) });
}
}

View file

@ -18,33 +18,37 @@
*/
import chrome from '../chrome';
import { PromiseService } from '../promises';
import { SavedObjectsClient } from './saved_objects_client';
type Args<T extends (...args: any[]) => any> = T extends (...args: infer X) => any ? X : never;
// Provide an angular wrapper around savedObjectClient so all actions get resolved in an Angular Promise
// If you do not need the promise to execute in an angular digest cycle then you should not use this
// and get savedObjectClient directly from chrome.
export function SavedObjectsClientProvider(Promise) {
export function SavedObjectsClientProvider(Promise: PromiseService) {
const savedObjectsClient = chrome.getSavedObjectsClient();
return {
create: (...args) => {
create: (...args: Args<SavedObjectsClient['create']>) => {
return Promise.resolve(savedObjectsClient.create(...args));
},
bulkCreate: (...args) => {
bulkCreate: (...args: Args<SavedObjectsClient['bulkCreate']>) => {
return Promise.resolve(savedObjectsClient.bulkCreate(...args));
},
delete: (...args) => {
delete: (...args: Args<SavedObjectsClient['delete']>) => {
return Promise.resolve(savedObjectsClient.delete(...args));
},
find: (...args) => {
find: (...args: Args<SavedObjectsClient['find']>) => {
return Promise.resolve(savedObjectsClient.find(...args));
},
get: (...args) => {
get: (...args: Args<SavedObjectsClient['get']>) => {
return Promise.resolve(savedObjectsClient.get(...args));
},
bulkGet: (...args) => {
bulkGet: (...args: Args<SavedObjectsClient['bulkGet']>) => {
return Promise.resolve(savedObjectsClient.bulkGet(...args));
},
update: (...args) => {
update: (...args: Args<SavedObjectsClient['update']>) => {
return Promise.resolve(savedObjectsClient.update(...args));
},
};

View file

@ -0,0 +1,36 @@
/*
* 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.
*/
// TODO: This file is copied from src/utils/case_conversion.ts
// because TS-imports from utils in ui are currently not possible.
// When the build process is updated, this file can be removed
import _ from 'lodash';
export function keysToSnakeCaseShallow(object: Record<string, any>) {
return _.mapKeys(object, (value, key) => {
return _.snakeCase(key);
});
}
export function keysToCamelCaseShallow(object: Record<string, any>) {
return _.mapKeys(object, (value, key) => {
return _.camelCase(key);
});
}

View file

@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"include": [
"typings/**/*",
"common/**/*",
"server/**/*",
"plugins/**/*",

10
x-pack/typings/index.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/*
* 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.
*/
declare module '*.html' {
const template: string;
export default template;
}