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:
parent
ee6135feaa
commit
fb2f0569e2
3
src/server/saved_objects/index.d.ts
vendored
3
src/server/saved_objects/index.d.ts
vendored
|
@ -18,8 +18,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
MigrationVersion,
|
||||||
SavedObject,
|
SavedObject,
|
||||||
|
SavedObjectAttributes,
|
||||||
SavedObjectsClient,
|
SavedObjectsClient,
|
||||||
SavedObjectsClientWrapperFactory,
|
SavedObjectsClientWrapperFactory,
|
||||||
|
SavedObjectReference,
|
||||||
SavedObjectsService,
|
SavedObjectsService,
|
||||||
} from './service';
|
} 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 { SavedObjectsService } from './create_saved_objects_service';
|
||||||
export { SavedObjectsClientWrapperFactory } from './lib';
|
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 {
|
export interface CreateOptions extends BaseOptions {
|
||||||
id?: string;
|
id?: string;
|
||||||
override?: boolean;
|
override?: boolean;
|
||||||
|
references?: SavedObjectReference[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BulkCreateObject<T extends SavedObjectAttributes = any> {
|
export interface BulkCreateObject<T extends SavedObjectAttributes = any> {
|
||||||
|
@ -48,6 +49,7 @@ export interface FindOptions extends BaseOptions {
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
search?: string;
|
search?: string;
|
||||||
searchFields?: string[];
|
searchFields?: string[];
|
||||||
|
hasReference?: { type: string; id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FindResponse<T extends SavedObjectAttributes = any> {
|
export interface FindResponse<T extends SavedObjectAttributes = any> {
|
||||||
|
@ -71,6 +73,10 @@ export interface BulkGetResponse<T extends SavedObjectAttributes = any> {
|
||||||
saved_objects: Array<SavedObject<T>>;
|
saved_objects: Array<SavedObject<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MigrationVersion {
|
||||||
|
[pluginName: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SavedObjectAttributes {
|
export interface SavedObjectAttributes {
|
||||||
[key: string]: SavedObjectAttributes | string | number | boolean | null;
|
[key: string]: SavedObjectAttributes | string | number | boolean | null;
|
||||||
}
|
}
|
||||||
|
@ -85,6 +91,7 @@ export interface SavedObject<T extends SavedObjectAttributes = any> {
|
||||||
};
|
};
|
||||||
attributes: T;
|
attributes: T;
|
||||||
references: SavedObjectReference[];
|
references: SavedObjectReference[];
|
||||||
|
migrationVersion?: MigrationVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedObjectReference {
|
export interface SavedObjectReference {
|
||||||
|
@ -93,6 +100,10 @@ export interface SavedObjectReference {
|
||||||
id: string;
|
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 {
|
export declare class SavedObjectsClient {
|
||||||
public static errors: typeof errors;
|
public static errors: typeof errors;
|
||||||
public errors: typeof errors;
|
public errors: typeof errors;
|
||||||
|
@ -103,7 +114,7 @@ export declare class SavedObjectsClient {
|
||||||
type: string,
|
type: string,
|
||||||
attributes: T,
|
attributes: T,
|
||||||
options?: CreateOptions
|
options?: CreateOptions
|
||||||
): Promise<SavedObject<T>>;
|
): Promise<CreateResponse<T>>;
|
||||||
public bulkCreate<T extends SavedObjectAttributes = any>(
|
public bulkCreate<T extends SavedObjectAttributes = any>(
|
||||||
objects: Array<BulkCreateObject<T>>,
|
objects: Array<BulkCreateObject<T>>,
|
||||||
options?: CreateOptions
|
options?: CreateOptions
|
||||||
|
@ -120,11 +131,11 @@ export declare class SavedObjectsClient {
|
||||||
type: string,
|
type: string,
|
||||||
id: string,
|
id: string,
|
||||||
options?: BaseOptions
|
options?: BaseOptions
|
||||||
): Promise<SavedObject<T>>;
|
): Promise<GetResponse<T>>;
|
||||||
public update<T extends SavedObjectAttributes = any>(
|
public update<T extends SavedObjectAttributes = any>(
|
||||||
type: string,
|
type: string,
|
||||||
id: string,
|
id: string,
|
||||||
attributes: Partial<T>,
|
attributes: Partial<T>,
|
||||||
options?: UpdateOptions
|
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 { Brand } from '../../../core/public/chrome';
|
||||||
|
import { SavedObjectsClient } from '../saved_objects';
|
||||||
import { BreadcrumbsApi } from './api/breadcrumbs';
|
import { BreadcrumbsApi } from './api/breadcrumbs';
|
||||||
import { HelpExtensionApi } from './api/help_extension';
|
import { HelpExtensionApi } from './api/help_extension';
|
||||||
import { ChromeNavLinks } from './api/nav';
|
import { ChromeNavLinks } from './api/nav';
|
||||||
|
@ -34,6 +35,7 @@ declare interface Chrome extends ChromeNavLinks {
|
||||||
getBasePath(): string;
|
getBasePath(): string;
|
||||||
getXsrfToken(): string;
|
getXsrfToken(): string;
|
||||||
getKibanaVersion(): string;
|
getKibanaVersion(): string;
|
||||||
|
getSavedObjectsClient(): SavedObjectsClient;
|
||||||
getUiSettingsClient(): any;
|
getUiSettingsClient(): any;
|
||||||
setVisible(visible: boolean): any;
|
setVisible(visible: boolean): any;
|
||||||
getInjected(key: string, defaultValue?: any): any;
|
getInjected(key: string, defaultValue?: any): any;
|
||||||
|
@ -43,7 +45,6 @@ declare interface Chrome extends ChromeNavLinks {
|
||||||
addApplicationClass(classNames: string | string[]): this;
|
addApplicationClass(classNames: string | string[]): this;
|
||||||
removeApplicationClass(classNames: string | string[]): this;
|
removeApplicationClass(classNames: string | string[]): this;
|
||||||
getApplicationClasses(): string;
|
getApplicationClasses(): string;
|
||||||
getSavedObjectsClient(): any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const chrome: Chrome;
|
declare const chrome: Chrome;
|
||||||
|
|
|
@ -24,16 +24,20 @@ import uiRoutes from '../routes';
|
||||||
|
|
||||||
import template from './error_auto_create_index.html';
|
import template from './error_auto_create_index.html';
|
||||||
|
|
||||||
uiRoutes
|
uiRoutes.when('/error/action.auto_create_index', {
|
||||||
.when('/error/action.auto_create_index', {
|
template,
|
||||||
template,
|
k7Breadcrumbs: () => [
|
||||||
k7Breadcrumbs: () => [{ text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { defaultMessage: 'Error' }) }],
|
{
|
||||||
});
|
text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', {
|
||||||
|
defaultMessage: 'Error',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export function isAutoCreateIndexError(error) {
|
export function isAutoCreateIndexError(error: object) {
|
||||||
return (
|
return (
|
||||||
get(error, 'res.status') === 503 &&
|
get(error, 'res.status') === 503 && get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
|
||||||
get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,5 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { kfetch, addInterceptor, KFetchOptions } from './kfetch';
|
export { kfetch, addInterceptor, KFetchOptions, KFetchQuery } from './kfetch';
|
||||||
export { kfetchAbortable } from './kfetch_abortable';
|
export { kfetchAbortable } from './kfetch_abortable';
|
||||||
|
|
|
@ -24,7 +24,7 @@ import url from 'url';
|
||||||
import chrome from '../chrome';
|
import chrome from '../chrome';
|
||||||
import { KFetchError } from './kfetch_error';
|
import { KFetchError } from './kfetch_error';
|
||||||
|
|
||||||
interface KFetchQuery {
|
export interface KFetchQuery {
|
||||||
[key: string]: string | number | boolean | undefined;
|
[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 { 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
|
* Returns an object matching a given title
|
||||||
|
@ -27,22 +30,30 @@ import { find } from 'lodash';
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @returns {Promise<SavedObject|undefined>}
|
* @returns {Promise<SavedObject|undefined>}
|
||||||
*/
|
*/
|
||||||
export function findObjectByTitle(savedObjectsClient, type, title) {
|
export function findObjectByTitle<T extends SavedObjectAttributes>(
|
||||||
if (!title) return Promise.resolve();
|
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
|
// 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.
|
// first, and so we shouldn't need to request everything. Using 10 just to be on the safe side.
|
||||||
return savedObjectsClient.find({
|
return savedObjectsClient
|
||||||
type,
|
.find<T>({
|
||||||
perPage: 10,
|
type,
|
||||||
search: `"${title}"`,
|
perPage: 10,
|
||||||
searchFields: ['title'],
|
search: `"${title}"`,
|
||||||
fields: ['title']
|
searchFields: ['title'],
|
||||||
}).then(response => {
|
fields: ['title'],
|
||||||
const match = find(response.savedObjects, (obj) => {
|
})
|
||||||
return obj.get('title').toLowerCase() === title.toLowerCase();
|
.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({
|
export const SavedObjectRegistryProvider = uiRegistry({
|
||||||
name: 'savedObjects',
|
name: 'savedObjects',
|
||||||
index: ['loaderProperties.name'],
|
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 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
|
// 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
|
// 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.
|
// and get savedObjectClient directly from chrome.
|
||||||
export function SavedObjectsClientProvider(Promise) {
|
export function SavedObjectsClientProvider(Promise: PromiseService) {
|
||||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: (...args) => {
|
create: (...args: Args<SavedObjectsClient['create']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.create(...args));
|
return Promise.resolve(savedObjectsClient.create(...args));
|
||||||
},
|
},
|
||||||
bulkCreate: (...args) => {
|
bulkCreate: (...args: Args<SavedObjectsClient['bulkCreate']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.bulkCreate(...args));
|
return Promise.resolve(savedObjectsClient.bulkCreate(...args));
|
||||||
},
|
},
|
||||||
delete: (...args) => {
|
delete: (...args: Args<SavedObjectsClient['delete']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.delete(...args));
|
return Promise.resolve(savedObjectsClient.delete(...args));
|
||||||
},
|
},
|
||||||
find: (...args) => {
|
find: (...args: Args<SavedObjectsClient['find']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.find(...args));
|
return Promise.resolve(savedObjectsClient.find(...args));
|
||||||
},
|
},
|
||||||
get: (...args) => {
|
get: (...args: Args<SavedObjectsClient['get']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.get(...args));
|
return Promise.resolve(savedObjectsClient.get(...args));
|
||||||
},
|
},
|
||||||
bulkGet: (...args) => {
|
bulkGet: (...args: Args<SavedObjectsClient['bulkGet']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.bulkGet(...args));
|
return Promise.resolve(savedObjectsClient.bulkGet(...args));
|
||||||
},
|
},
|
||||||
update: (...args) => {
|
update: (...args: Args<SavedObjectsClient['update']>) => {
|
||||||
return Promise.resolve(savedObjectsClient.update(...args));
|
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",
|
"extends": "../tsconfig.json",
|
||||||
"include": [
|
"include": [
|
||||||
|
"typings/**/*",
|
||||||
"common/**/*",
|
"common/**/*",
|
||||||
"server/**/*",
|
"server/**/*",
|
||||||
"plugins/**/*",
|
"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