* 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:
parent
4108aa2688
commit
cb86994923
23 changed files with 970 additions and 733 deletions
3
src/server/saved_objects/index.d.ts
vendored
3
src/server/saved_objects/index.d.ts
vendored
|
@ -18,8 +18,11 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
MigrationVersion,
|
||||
SavedObject,
|
||||
SavedObjectAttributes,
|
||||
SavedObjectsClient,
|
||||
SavedObjectsClientWrapperFactory,
|
||||
SavedObjectReference,
|
||||
SavedObjectsService,
|
||||
} from './service';
|
||||
|
|
12
src/server/saved_objects/service/index.d.ts
vendored
12
src/server/saved_objects/service/index.d.ts
vendored
|
@ -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';
|
||||
|
|
|
@ -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>>;
|
||||
}
|
||||
|
|
3
src/ui/public/chrome/index.d.ts
vendored
3
src/ui/public/chrome/index.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
@ -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
20
src/ui/public/promises/index.d.ts
vendored
Normal 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
25
src/ui/public/promises/promises.d.ts
vendored
Normal 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;
|
||||
}
|
|
@ -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 }
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
81
src/ui/public/saved_objects/saved_object.ts
Normal file
81
src/ui/public/saved_objects/saved_object.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -22,5 +22,5 @@ import { uiRegistry } from '../registry/_registry';
|
|||
export const SavedObjectRegistryProvider = uiRegistry({
|
||||
name: 'savedObjects',
|
||||
index: ['loaderProperties.name'],
|
||||
order: ['loaderProperties.name']
|
||||
order: ['loaderProperties.name'],
|
||||
});
|
|
@ -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) });
|
||||
}
|
||||
}
|
357
src/ui/public/saved_objects/saved_objects_client.test.ts
Normal file
357
src/ui/public/saved_objects/saved_objects_client.test.ts
Normal 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 },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
358
src/ui/public/saved_objects/saved_objects_client.ts
Normal file
358
src/ui/public/saved_objects/saved_objects_client.ts
Normal 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) });
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
},
|
||||
};
|
36
src/ui/public/utils/case_conversion.ts
Normal file
36
src/ui/public/utils/case_conversion.ts
Normal 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);
|
||||
});
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"typings/**/*",
|
||||
"common/**/*",
|
||||
"server/**/*",
|
||||
"plugins/**/*",
|
||||
|
|
10
x-pack/typings/index.d.ts
vendored
Normal file
10
x-pack/typings/index.d.ts
vendored
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue