Typescript some of src/legacy/server/index_patterns (#44993) (#46349)

* TypeScriptify errors.js

* Fixed type errors in errors.ts

* Prettierize resolve_time_pattern.test.js

* Prettierize resolve_time_pattern.js

* TypeScriptify resolve_time_pattern.js

* Prettierize mixin.js

* Fixed errors.ts payload error.

* TypeScriptify and Prettierize mixin.ts

* Prettierize query_params.js

* TypeScriptified routes and used core.http features and fixed related tests.

* Fixed type errors.

* Removed anys

* Prettierize params.js

* Prettierize response.js

* Accepts string array as query param.

* Returns 404 when there's no matching pattern for fields_for_wildcard.

* Simplified schema.

* Fixed types.
This commit is contained in:
Stacey Gammon 2019-09-23 16:03:29 -04:00 committed by GitHub
parent 90bf444703
commit 817877dc8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 273 additions and 257 deletions

View file

@ -1,24 +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 { IndexPatternsService } from './service';
export type IndexPatternsServiceFactory = (args: {
callCluster: (endpoint: string, clientParams: any, options: any) => Promise<any>;
}) => IndexPatternsService;

View file

@ -18,26 +18,12 @@
*/
import { IndexPatternsService } from './service';
import KbnServer from '../kbn_server';
import { APICaller } from '../../../core/server';
import { Legacy } from '../../../../kibana';
import { registerRoutes } from './routes';
import {
createFieldsForWildcardRoute,
createFieldsForTimePatternRoute,
} from './routes';
export function indexPatternsMixin(kbnServer, server) {
const pre = {
/**
* Create an instance of the `indexPatterns` service
* @type {Hapi.Pre}
*/
getIndexPatternsService: {
assign: 'indexPatterns',
method(request) {
return request.getIndexPatternsService();
}
}
};
export function indexPatternsMixin(kbnServer: KbnServer, server: Legacy.Server) {
/**
* Create an instance of the IndexPatternsService
*
@ -55,12 +41,16 @@ export function indexPatternsMixin(kbnServer, server) {
* @method request.getIndexPatternsService
* @type {IndexPatternsService}
*/
server.addMemoizedFactoryToRequest('getIndexPatternsService', request => {
server.addMemoizedFactoryToRequest('getIndexPatternsService', (request: Legacy.Request) => {
const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
const callCluster = (...args) => callWithRequest(request, ...args);
const callCluster: APICaller = (endpoint, params, options) =>
callWithRequest(request, endpoint, params, options);
return server.indexPatternsServiceFactory({ callCluster });
});
server.route(createFieldsForWildcardRoute(pre));
server.route(createFieldsForTimePatternRoute(pre));
registerRoutes(kbnServer.newPlatform.setup.core);
}
export type IndexPatternsServiceFactory = (args: {
callCluster: (endpoint: string, clientParams: any, options: any) => Promise<any>;
}) => IndexPatternsService;

View file

@ -0,0 +1,134 @@
/*
* 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 { first } from 'rxjs/operators';
import { schema } from '@kbn/config-schema';
import {
InternalCoreSetup,
KibanaRequest,
RequestHandlerContext,
APICaller,
} from '../../../core/server';
import { IndexPatternsService } from './service';
export function registerRoutes(core: InternalCoreSetup) {
const getIndexPatternsService = async (request: KibanaRequest): Promise<IndexPatternsService> => {
const client = await core.elasticsearch.dataClient$.pipe(first()).toPromise();
const callCluster: APICaller = (endpoint, params, options) =>
client.asScoped(request).callAsCurrentUser(endpoint, params, options);
return new Promise(resolve => resolve(new IndexPatternsService(callCluster)));
};
const parseMetaFields = (metaFields: string | string[]) => {
let parsedFields: string[] = [];
if (typeof metaFields === 'string') {
parsedFields = JSON.parse(metaFields);
} else {
parsedFields = metaFields;
}
return parsedFields;
};
const router = core.http.createRouter('/api/index_patterns');
router.get(
{
path: '/_fields_for_wildcard',
validate: {
query: schema.object({
pattern: schema.string(),
meta_fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
defaultValue: [],
}),
}),
},
},
async (context: RequestHandlerContext, request: any, response: any) => {
const indexPatterns = await getIndexPatternsService(request);
const { pattern, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
try {
parsedFields = parseMetaFields(metaFields);
} catch (error) {
return response.badRequest();
}
try {
const fields = await indexPatterns.getFieldsForWildcard({
pattern,
metaFields: parsedFields,
});
return response.ok({
body: { fields },
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return response.notFound();
}
}
);
router.get(
{
path: '/_fields_for_time_pattern',
validate: {
query: schema.object({
pattern: schema.string(),
interval: schema.maybe(schema.string()),
look_back: schema.number({ min: 1 }),
meta_fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
defaultValue: [],
}),
}),
},
},
async (context: RequestHandlerContext, request: any, response: any) => {
const indexPatterns = await getIndexPatternsService(request);
const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query;
let parsedFields: string[] = [];
try {
parsedFields = parseMetaFields(metaFields);
} catch (error) {
return response.badRequest();
}
try {
const fields = await indexPatterns.getFieldsForTimePattern({
pattern,
interval: interval ? interval : '',
lookBack,
metaFields: parsedFields,
});
return response.ok({
body: { fields },
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return response.notFound();
}
}
);
}

View file

@ -1,53 +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 Joi from 'joi';
export const createFieldsForTimePatternRoute = pre => ({
path: '/api/index_patterns/_fields_for_time_pattern',
method: 'GET',
config: {
pre: [pre.getIndexPatternsService],
validate: {
query: Joi.object().keys({
pattern: Joi.string().required(),
look_back: Joi.number().min(1).required(),
meta_fields: Joi.array().items(Joi.string()).default([]),
}).default()
},
async handler(req) {
const { indexPatterns } = req.pre;
const {
pattern,
interval,
look_back: lookBack,
meta_fields: metaFields,
} = req.query;
const fields = await indexPatterns.getFieldsForTimePattern({
pattern,
interval,
lookBack,
metaFields
});
return { fields };
}
}
});

View file

@ -1,48 +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 Joi from 'joi';
export const createFieldsForWildcardRoute = pre => ({
path: '/api/index_patterns/_fields_for_wildcard',
method: 'GET',
config: {
pre: [pre.getIndexPatternsService],
validate: {
query: Joi.object().keys({
pattern: Joi.string().required(),
meta_fields: Joi.array().items(Joi.string()).default([]),
}).default()
},
async handler(req) {
const { indexPatterns } = req.pre;
const {
pattern,
meta_fields: metaFields,
} = req.query;
const fields = await indexPatterns.getFieldsForWildcard({
pattern,
metaFields
});
return { fields };
}
}
});

View file

@ -1,21 +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.
*/
export { createFieldsForWildcardRoute } from './fields_for_wildcard_route';
export { createFieldsForTimePatternRoute } from './fields_for_time_pattern_route';

View file

@ -29,7 +29,7 @@ const ERR_NO_MATCHING_INDICES = 'no_matching_indices';
* @param {Any} err
* @return {Boolean}
*/
export function isEsIndexNotFoundError(err) {
export function isEsIndexNotFoundError(err: any) {
return get(err, ['body', 'error', 'type']) === ERR_ES_INDEX_NOT_FOUND;
}
@ -39,9 +39,9 @@ export function isEsIndexNotFoundError(err) {
* @param {String} pattern the pattern which indexes were supposed to match
* @return {Boom}
*/
export function createNoMatchingIndicesError(pattern) {
export function createNoMatchingIndicesError(pattern: string[] | string) {
const err = Boom.notFound(`No indices match pattern "${pattern}"`);
err.output.payload.code = ERR_NO_MATCHING_INDICES;
(err.output.payload as any).code = ERR_NO_MATCHING_INDICES;
return err;
}
@ -51,17 +51,17 @@ export function createNoMatchingIndicesError(pattern) {
* @param {Any} err
* @return {Boolean}
*/
export function isNoMatchingIndicesError(err) {
export function isNoMatchingIndicesError(err: any) {
return get(err, ['output', 'payload', 'code']) === ERR_NO_MATCHING_INDICES;
}
/**
* Wrap "index_not_found_exception" errors in custom Boom errors
* automatically
* @param {[type]} indices [description]
* @return {[type]} [description]
* @param {Array<String>|String} indices
* @return {Boom}
*/
export function convertEsError(indices, error) {
export function convertEsError(indices: string[] | string, error: any) {
if (isEsIndexNotFoundError(error)) {
return createNoMatchingIndicesError(indices);
}

View file

@ -22,6 +22,16 @@ import { APICaller } from 'src/core/server';
import { convertEsError } from './errors';
import { FieldCapsResponse } from './field_capabilities';
export interface IndicesAliasResponse {
[index: string]: IndexAliasResponse;
}
export interface IndexAliasResponse {
aliases: {
[aliasName: string]: Record<string, any>;
};
}
/**
* Call the index.getAlias API for a list of indices.
*
@ -36,13 +46,16 @@ import { FieldCapsResponse } from './field_capabilities';
* @param {Array<String>|String} indices
* @return {Promise<IndexAliasResponse>}
*/
export async function callIndexAliasApi(callCluster: APICaller, indices: string[] | string) {
export async function callIndexAliasApi(
callCluster: APICaller,
indices: string[] | string
): Promise<IndicesAliasResponse> {
try {
return await callCluster('indices.getAlias', {
return (await callCluster('indices.getAlias', {
index: indices,
ignoreUnavailable: true,
allowNoIndices: false,
});
})) as Promise<IndicesAliasResponse>;
} catch (error) {
throw convertEsError(indices, error);
}

View file

@ -32,7 +32,7 @@ const TIME_PATTERN = '[logs-]dddd-YYYY.w';
describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
let sandbox;
beforeEach(() => sandbox = sinon.createSandbox());
beforeEach(() => (sandbox = sinon.createSandbox()));
afterEach(() => sandbox.restore());
describe('resolveTimePattern()', () => {
@ -47,8 +47,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
const timePattern = {};
const wildcard = {};
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard')
.returns(wildcard);
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard);
await resolveTimePattern(noop, timePattern);
sinon.assert.calledOnce(timePatternToWildcard);
@ -60,8 +59,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
const wildcard = {};
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({});
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard')
.returns(wildcard);
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard);
await resolveTimePattern(noop, timePattern);
sinon.assert.calledOnce(callIndexAliasApi);
@ -110,7 +108,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
expect(resp.matches).toEqual([
'logs-Saturday-2017.1',
'logs-Friday-2017.1',
'logs-Sunday-2017.1'
'logs-Sunday-2017.1',
]);
});
});

View file

@ -20,8 +20,10 @@
import { chain } from 'lodash';
import moment from 'moment';
import { APICaller } from 'src/core/server';
import { timePatternToWildcard } from './time_pattern_to_wildcard';
import { callIndexAliasApi } from './es_api';
import { callIndexAliasApi, IndicesAliasResponse } from './es_api';
/**
* Convert a time pattern into a list of indexes it could
@ -34,15 +36,16 @@ import { callIndexAliasApi } from './es_api';
* and the indices that actually match the time
* pattern (matches);
*/
export async function resolveTimePattern(callCluster, timePattern) {
export async function resolveTimePattern(callCluster: APICaller, timePattern: string) {
const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern));
const allIndexDetails = chain(aliases)
.reduce((acc, index, indexName) => acc.concat(
indexName,
Object.keys(index.aliases || {})
), [])
.sort()
const allIndexDetails = chain<IndicesAliasResponse>(aliases)
.reduce(
(acc: string[], index: any, indexName: string) =>
acc.concat(indexName, Object.keys(index.aliases || {})),
[]
)
.sortBy((indexName: string) => indexName)
.uniq(true)
.map(indexName => {
const parsed = moment(indexName, timePattern, true);
@ -51,7 +54,7 @@ export async function resolveTimePattern(callCluster, timePattern) {
valid: false,
indexName,
order: indexName,
isMatch: false
isMatch: false,
};
}
@ -59,18 +62,15 @@ export async function resolveTimePattern(callCluster, timePattern) {
valid: true,
indexName,
order: parsed,
isMatch: indexName === parsed.format(timePattern)
isMatch: indexName === parsed.format(timePattern),
};
})
.sortByOrder(['valid', 'order'], ['desc', 'desc'])
.value();
return {
all: allIndexDetails
.map(details => details.indexName),
all: allIndexDetails.map(details => details.indexName),
matches: allIndexDetails
.filter(details => details.isMatch)
.map(details => details.indexName),
matches: allIndexDetails.filter(details => details.isMatch).map(details => details.indexName),
};
}

View file

@ -73,6 +73,10 @@ declare module 'hapi' {
savedObjectsManagement(): SavedObjectsManagement;
getInjectedUiAppVars: (pluginName: string) => { [key: string]: any };
getUiNavLinks(): Array<{ _id: string }>;
addMemoizedFactoryToRequest: (
name: string,
factoryFn: (request: Request) => Record<string, any>
) => void;
}
interface Request {

View file

@ -27,34 +27,49 @@ export default function ({ getService }) {
before(() => esArchiver.load('index_patterns/daily_index'));
after(() => esArchiver.unload('index_patterns/daily_index'));
it('requires `pattern` and `look_back` query params', () => (
it('requires `pattern` query param', () =>
supertest
.get('/api/index_patterns/_fields_for_time_pattern')
.query({ pattern: null })
.query({ look_back: 1 })
.expect(400)
.then(resp => {
expect(resp.body.validation).to.eql({
keys: [
'pattern',
'look_back'
],
source: 'query'
});
})
));
expect(resp.body.message).to.contain(
'[request query.pattern]: expected value of type [string] but got [undefined]'
);
}));
it('supports `meta_fields` query param', () => (
it('requires `look_back` query param', () =>
supertest
.get('/api/index_patterns/_fields_for_time_pattern')
.query({ pattern: 'pattern-*' })
.expect(400)
.then(resp => {
expect(resp.body.message).to.contain(
'[request query.look_back]: expected value of type [number] but got [undefined]'
);
}));
it('supports `meta_fields` query param in JSON format', () =>
supertest
.get('/api/index_patterns/_fields_for_time_pattern')
.query({
pattern: '[logs-]YYYY.MM.DD',
look_back: 1,
meta_fields: JSON.stringify(['a'])
meta_fields: JSON.stringify(['a']),
})
.expect(200)
));
.expect(200));
it('requires `look_back` to be a number', () => (
it('supports `meta_fields` query param in string array format', () =>
supertest
.get('/api/index_patterns/_fields_for_time_pattern')
.query({
pattern: '[logs-]YYYY.MM.DD',
look_back: 1,
meta_fields: ['a', 'b'],
})
.expect(200));
it('requires `look_back` to be a number', () =>
supertest
.get('/api/index_patterns/_fields_for_time_pattern')
.query({
@ -63,11 +78,12 @@ export default function ({ getService }) {
})
.expect(400)
.then(resp => {
expect(resp.body.message).to.contain('"look_back" must be a number');
})
));
expect(resp.body.message).to.contain(
'[request query.look_back]: expected value of type [number] but got [string]'
);
}));
it('requires `look_back` to be greater than one', () => (
it('requires `look_back` to be greater than one', () =>
supertest
.get('/api/index_patterns/_fields_for_time_pattern')
.query({
@ -76,8 +92,9 @@ export default function ({ getService }) {
})
.expect(400)
.then(resp => {
expect(resp.body.message).to.contain('"look_back" must be larger than or equal to 1');
})
));
expect(resp.body.message).to.contain(
'[request query.look_back]: Value is [0] but it must be equal to or greater than [1].'
);
}));
});
}

View file

@ -26,41 +26,46 @@ export default function ({ getService }) {
before(() => esArchiver.load('index_patterns/basic_index'));
after(() => esArchiver.unload('index_patterns/basic_index'));
it('requires a pattern query param', () => (
it('requires a pattern query param', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({})
.expect(400)
));
.expect(400));
it('accepts a JSON formatted meta_fields query param', () => (
it('accepts a JSON formatted meta_fields query param', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({
pattern: '*',
meta_fields: JSON.stringify(['meta'])
meta_fields: JSON.stringify(['meta']),
})
.expect(200)
));
.expect(200));
it('rejects a comma-separated list of meta_fields', () => (
it('accepts meta_fields query param in string array', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({
pattern: '*',
meta_fields: 'foo,bar'
meta_fields: ['_id', 'meta'],
})
.expect(400)
));
.expect(200));
it('rejects unexpected query params', () => (
it('rejects a comma-separated list of meta_fields', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({
pattern: '*',
meta_fields: 'foo,bar',
})
.expect(400));
it('rejects unexpected query params', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({
pattern: chance.word(),
[chance.word()]: chance.word(),
})
.expect(400)
));
.expect(400));
});
}

View file

@ -25,8 +25,7 @@ export default function ({ getService }) {
const supertest = getService('supertest');
const ensureFieldsAreSorted = resp => {
expect(resp.body.fields)
.to.eql(sortBy(resp.body.fields, 'name'));
expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name'));
};
describe('response', () => {
@ -45,7 +44,7 @@ export default function ({ getService }) {
searchable: true,
aggregatable: true,
name: 'bar',
readFromDocValues: true
readFromDocValues: true,
},
{
type: 'string',
@ -53,7 +52,7 @@ export default function ({ getService }) {
searchable: true,
aggregatable: false,
name: 'baz',
readFromDocValues: false
readFromDocValues: false,
},
{
type: 'string',
@ -71,23 +70,18 @@ export default function ({ getService }) {
searchable: true,
aggregatable: true,
name: 'foo',
readFromDocValues: true
}
]
readFromDocValues: true,
},
],
})
.then(ensureFieldsAreSorted)
);
.then(ensureFieldsAreSorted));
it('always returns a field for all passed meta fields', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({
pattern: 'basic_index',
meta_fields: JSON.stringify([
'_id',
'_source',
'crazy_meta_field'
])
meta_fields: JSON.stringify(['_id', '_source', 'crazy_meta_field']),
})
.expect(200, {
fields: [
@ -113,7 +107,7 @@ export default function ({ getService }) {
searchable: true,
aggregatable: true,
name: 'bar',
readFromDocValues: true
readFromDocValues: true,
},
{
aggregatable: false,
@ -146,11 +140,18 @@ export default function ({ getService }) {
searchable: true,
aggregatable: true,
name: 'foo',
readFromDocValues: true
}
]
readFromDocValues: true,
},
],
})
.then(ensureFieldsAreSorted)
);
.then(ensureFieldsAreSorted));
it('returns 404 when the pattern does not exist', () =>
supertest
.get('/api/index_patterns/_fields_for_wildcard')
.query({
pattern: '[non-existing-pattern]its-invalid-*',
})
.expect(404));
});
}