diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 475de028ecb52a1..94db92431a4184d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -20,10 +20,6 @@ import styled from 'styled-components'; import { idx } from '@kbn/elastic-idx'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { useFetcher } from '../../../hooks/useFetcher'; -import { - loadErrorDistribution, - loadErrorGroupDetails -} from '../../../services/rest/apm/error_groups'; import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; import { DetailView } from './DetailView'; @@ -31,6 +27,7 @@ import { ErrorDistribution } from './Distribution'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; +import { calls } from '../../../services/rest/callApi'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -68,24 +65,28 @@ export function ErrorGroupDetails() { const { data: errorGroupData } = useFetcher(() => { if (serviceName && start && end && errorGroupId) { - return loadErrorGroupDetails({ - serviceName, - start, - end, - errorGroupId, - uiFilters + return calls.getErrorsByGroupId({ + query: { + serviceName, + groupId: errorGroupId, + start, + end, + uiFilters: JSON.stringify(uiFilters) + } }); } }, [serviceName, start, end, errorGroupId, uiFilters]); const { data: errorDistributionData } = useFetcher(() => { if (serviceName && start && end && errorGroupId) { - return loadErrorDistribution({ - serviceName, - start, - end, - errorGroupId, - uiFilters + return calls.getErrorsDistribution({ + query: { + serviceName, + start, + end, + groupId: errorGroupId, + uiFilters: JSON.stringify(uiFilters) + } }); } }, [serviceName, start, end, errorGroupId, uiFilters]); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 4d5a87faa46a6b1..111aa41d90e30ed 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -14,14 +14,11 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { useFetcher } from '../../../hooks/useFetcher'; -import { - loadErrorDistribution, - loadErrorGroupList -} from '../../../services/rest/apm/error_groups'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; +import { calls } from '../../../services/rest/callApi'; const ErrorGroupOverview: React.SFC = () => { const { @@ -31,24 +28,28 @@ const ErrorGroupOverview: React.SFC = () => { const { data: errorDistributionData } = useFetcher(() => { if (serviceName && start && end) { - return loadErrorDistribution({ - serviceName, - start, - end, - uiFilters + return calls.getErrorsDistribution({ + query: { + serviceName, + start, + end, + uiFilters: JSON.stringify(uiFilters) + } }); } }, [serviceName, start, end, uiFilters]); const { data: errorGroupListData } = useFetcher(() => { if (serviceName && start && end) { - return loadErrorGroupList({ - serviceName, - start, - end, - sortField, - sortDirection, - uiFilters + return calls.getErrorGroupList({ + query: { + serviceName, + start, + end, + sortField, + sortDirection, + uiFilters: JSON.stringify(uiFilters) + } }); } }, [serviceName, start, end, sortField, sortDirection, uiFilters]); diff --git a/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts b/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts deleted file mode 100644 index 8632b12e2695f89..000000000000000 --- a/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callApmApi } from '../callApi'; -import { UIFilters } from '../../../../typings/ui-filters'; - -export async function loadErrorGroupList({ - serviceName, - start, - end, - uiFilters, - sortField, - sortDirection -}: { - serviceName: string; - start: string; - end: string; - uiFilters: UIFilters; - sortField?: string; - sortDirection?: string; -}) { - return callApmApi({ - pathname: `/api/apm/services/{serviceName}/errors`, - method: 'GET', - params: { - path: { - serviceName - }, - query: { - start, - end, - sortField, - sortDirection, - uiFilters: JSON.stringify(uiFilters) - } - } - }); -} - -export async function loadErrorGroupDetails({ - serviceName, - start, - end, - uiFilters, - errorGroupId -}: { - serviceName: string; - start: string; - end: string; - errorGroupId: string; - uiFilters: UIFilters; -}) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/{groupId}', - method: 'GET', - params: { - path: { - serviceName, - groupId: errorGroupId - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); -} - -export async function loadErrorDistribution({ - serviceName, - start, - end, - uiFilters, - errorGroupId -}: { - serviceName: string; - start: string; - end: string; - uiFilters: UIFilters; - errorGroupId?: string; -}) { - return callApmApi({ - pathname: `/api/apm/services/{serviceName}/errors/distribution`, - method: 'GET', - params: { - path: { - serviceName - }, - query: { - start, - end, - groupId: errorGroupId, - uiFilters: JSON.stringify(uiFilters) - } - } - }); -} diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index a95d540902e05c5..a00956e5d26a522 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -10,9 +10,8 @@ import LRU from 'lru-cache'; import hash from 'object-hash'; import { kfetch, KFetchOptions } from 'ui/kfetch'; import { KFetchKibanaOptions } from 'ui/kfetch/kfetch'; -import { idx } from '@kbn/elastic-idx'; -import { APICall, RouteParams } from '../../../server/routes/typings'; -import { api } from '../../../server/routes'; +import { APMAPI } from '../../../server/routes/create_apm_api'; +import { Client, HttpMethod } from '../../../server/routes/typings'; function fetchOptionsWithDebug(fetchOptions: KFetchOptions) { const debugEnabled = @@ -58,33 +57,29 @@ export async function callApi( return res; } -export const callApmApi: APICall = ({ - method, - pathname, - params -}: { - method: any; - pathname: string; - params?: { - [key in keyof RouteParams]: any; - }; -}) => { - const path = idx(params, _ => _.path) || {}; - const query = idx(params, _ => _.query) || {}; - const body = idx(params, _ => _.body) || {}; - - const formattedPathname = pathname - .split('/') - .map(part => (part.startsWith(':') ? path[part.substr(1)] : part)) - .join('/'); - - return callApi({ - method, - pathname: formattedPathname, - query, - body: body ? JSON.stringify(body) : undefined - }) as any; -}; +export const calls: Client = new Proxy( + {}, + { + get: (obj, prop) => { + return ({ + query = {}, + body = {}, + method = 'GET' + }: { + query?: any; + body?: any; + method?: HttpMethod; + }) => { + return callApi({ + method, + pathname: `/api/apm/${prop.toString()}`, + query, + body: body ? JSON.stringify(body) : undefined + }) as any; + }; + } + } +) as Client; // only cache items that has a time range with `start` and `end` params, // and where `end` is not a timestamp in the future diff --git a/x-pack/legacy/plugins/apm/public/utils/createTypedObjectBuilder.ts b/x-pack/legacy/plugins/apm/public/utils/createTypedObjectBuilder.ts new file mode 100644 index 000000000000000..d9f815ca6134d74 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/createTypedObjectBuilder.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +type ExtraKeys = Exclude; + +type Exact = ExtraKeys extends never + ? U + : 'Unknown keys specified:' & { [key in ExtraKeys]: U[key] }; + +export function createTypedObjectBuilder() { + return function build(obj: U): Exact { + return obj as any; + }; +} diff --git a/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts b/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts index 460c2ffa0db3b3b..f6ad49c6027d0da 100644 --- a/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts +++ b/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts @@ -13,7 +13,7 @@ import { initTracesApi } from '../routes/traces'; import { initTransactionGroupsApi } from '../routes/transaction_groups'; import { initUIFiltersApi } from '../routes/ui_filters'; import { initSettingsApi } from '../routes/settings'; -import { api } from '../routes'; +import { createApmApi } from '../routes/create_apm_api'; export class Plugin { public setup(core: InternalCoreSetup) { @@ -23,7 +23,7 @@ export class Plugin { initServicesApi(core); initSettingsApi(core); initMetricsApi(core); - api.init(core); + createApmApi().init(core); makeApmUsageCollector(core as CoreSetupWithUsageCollector); } diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_api.ts index 11d704122091618..c6c297533201aeb 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api.ts @@ -9,36 +9,48 @@ import { InternalCoreSetup } from 'src/core/server'; import { Request } from 'hapi'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { ServerAPI, RouteFactoryFunction, RouteParams } from './typings'; -import { removeUndefinedProps } from '../../public/context/UrlParamsContext/helpers'; +import { + ServerAPI, + RouteFactoryFn, + HttpMethod, + Route, + QueryRT, + BodyRT, + RouteState +} from './typings'; +import { createTypedObjectBuilder } from '../../public/utils/createTypedObjectBuilder'; -type GenericRouteFactoryFunction = RouteFactoryFunction< - any, - any, - { [key in keyof RouteParams]?: RouteParams[key] }, - any ->; - -export function createApi() { - const factoryFns: GenericRouteFactoryFunction[] = []; - const router = { - add(fn: GenericRouteFactoryFunction) { +export function createApi() { + const factoryFns: Array> = []; + const api = createTypedObjectBuilder>()({ + _S: {} as T, + add(fn) { factoryFns.push(fn); - return router; + return this as any; }, - init: (core: InternalCoreSetup) => { + init(core: InternalCoreSetup) { const { server } = core.http; factoryFns.forEach(fn => { - const { params = {}, ...route } = fn(core); + const { body, query, name, ...route } = fn(core) as Route< + string, + HttpMethod, + QueryRT, + BodyRT, + any + >; + const params = { body, query }; + server.route( merge( { options: { tags: ['access:apm'] - } + }, + method: 'GET' }, route, { + path: `/api/apm/${name}`, handler: async (request: Request) => { const paramMap = { path: request.params, @@ -53,7 +65,8 @@ export function createApi() { let codec = params[key]; if (!codec) return acc; - if ('_tag' in codec) { + // Use exact props where possible (only possible for types with props) + if ('props' in codec) { codec = t.exact(codec); } @@ -69,17 +82,14 @@ export function createApi() { {} as Record ); - return route.handler( - request, - removeUndefinedProps(parsedParams) - ); + return route.handler(request, parsedParams); } } ) ); }); } - }; + }); - return router as ServerAPI<{}>; + return api as ServerAPI; } diff --git a/x-pack/legacy/plugins/apm/server/routes/index.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts similarity index 61% rename from x-pack/legacy/plugins/apm/server/routes/index.ts rename to x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index 1baad657f62f4d3..13e824f8f3084aa 100644 --- a/x-pack/legacy/plugins/apm/server/routes/index.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -11,10 +11,16 @@ import { } from './errors'; import { createApi } from './create_api'; -const api = createApi() - .add(indexPatternRoute) - .add(errorDistributionRoute) - .add(errorGroupsRoute) - .add(errorsRoute); +const createApmApi = () => { + const api = createApi() + .add(indexPatternRoute) + .add(errorDistributionRoute) + .add(errorGroupsRoute) + .add(errorsRoute); -export { api }; + return api; +}; + +export type APMAPI = ReturnType; + +export { createApmApi }; diff --git a/x-pack/legacy/plugins/apm/server/routes/create_route.ts b/x-pack/legacy/plugins/apm/server/routes/create_route.ts index 4eb9adeca72c338..8a9fbcd30853fae 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_route.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_route.ts @@ -3,33 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; -import { - RouteFactoryFunction, - HttpMethod, - RouteParams, - RouteRequestHandler -} from './typings'; +import { RouteFactoryFn, HttpMethod, QueryRT, BodyRT } from './typings'; export function createRoute< - T extends string, - U extends HttpMethod, - V extends RouteParams, - W, - X extends RouteRequestHandler ->(cb: RouteFactoryFunction) { - return cb; + TName extends string, + TMethod extends HttpMethod | undefined, + TQuery extends QueryRT | undefined, + TBody extends BodyRT | undefined, + TReturn +>(fn: RouteFactoryFn) { + return fn; } - -createRoute(core => ({ - path: '/foo', - method: 'GET' as const, - params: { - query: t.type({ - serviceName: t.string - }) - }, - handler: async (req, params) => { - return null; - } -})); diff --git a/x-pack/legacy/plugins/apm/server/routes/errors.ts b/x-pack/legacy/plugins/apm/server/routes/errors.ts index a86c0720b1d4f85..7e7b3c001981fb5 100644 --- a/x-pack/legacy/plugins/apm/server/routes/errors.ts +++ b/x-pack/legacy/plugins/apm/server/routes/errors.ts @@ -13,24 +13,22 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { defaultApiTypes } from './default_api_types'; export const errorsRoute = createRoute(core => ({ - path: `/api/apm/services/{serviceName}/errors`, - method: 'GET', - params: { - path: t.type({ + name: 'getErrorGroupList', + query: t.intersection([ + t.type({ serviceName: t.string }), - query: t.intersection([ + t.intersection([ t.partial({ sortField: t.string, sortDirection: t.string }), defaultApiTypes ]) - }, - handler: async (req, params) => { + ]), + handler: async (req, { query }) => { const setup = await setupRequest(req); - const { serviceName } = params.path; - const { sortField, sortDirection } = params.query; + const { sortField, sortDirection, serviceName } = query; return getErrorGroups({ serviceName, @@ -42,40 +40,35 @@ export const errorsRoute = createRoute(core => ({ })); export const errorGroupsRoute = createRoute(() => ({ - method: 'GET', - path: `/api/apm/services/{serviceName}/errors/{groupId}`, - params: { - path: t.type({ + name: 'getErrorsByGroupId', + query: t.intersection([ + t.type({ serviceName: t.string, groupId: t.string }), - query: defaultApiTypes - }, - handler: async (req, params) => { + defaultApiTypes + ]), + handler: async (req, { query }) => { const setup = await setupRequest(req); - const { serviceName, groupId } = params.path; + const { serviceName, groupId } = query; return getErrorGroup({ serviceName, groupId, setup }); } })); export const errorDistributionRoute = createRoute(() => ({ - path: `/api/apm/services/{serviceName}/errors/distribution`, - method: 'GET', - params: { - path: t.type({ + name: 'getErrorsDistribution', + query: t.intersection([ + t.type({ serviceName: t.string }), - query: t.intersection([ - t.partial({ - groupId: t.string - }), - defaultApiTypes - ]) - }, + t.partial({ + groupId: t.string + }), + defaultApiTypes + ]), handler: async (req, params) => { const setup = await setupRequest(req); - const { serviceName } = params.path; - const { groupId } = params.query; + const { groupId, serviceName } = params.query; return getErrorDistribution({ serviceName, groupId, setup }); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts index ab0e52e5f350f86..b81b424be5e5c61 100644 --- a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts @@ -8,8 +8,7 @@ import { getAPMIndexPattern } from '../lib/index_pattern'; import { createRoute } from './create_route'; export const indexPatternRoute = createRoute(core => ({ - method: 'GET', - path: '/api/apm/index_pattern', + name: 'getIndexPattern', handler: async () => { const { server } = core.http; return await getAPMIndexPattern(server); diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index 8bc59cdd77018e3..a3aec070650f38f 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -4,107 +4,84 @@ * you may not use this file except in compliance with the Elastic License. */ -import t, { Type, HasProps, TypeOf } from 'io-ts'; +import t from 'io-ts'; import { Request } from 'hapi'; import { InternalCoreSetup } from 'src/core/server'; -type ExtractTType = T extends Type ? TypeOf : {}; - -export type ExtractTTypeFromParams = { - [key in keyof T]: ExtractTType; -}; - -interface RouteType< - T extends Partial>>, - U extends any -> { - params: T; - returnType: U; -} - -export interface RouteDescriptionTree { - [key: string]: { - [method in HttpMethod]: RouteType; - }; -} +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; -export interface RouteParams { - path?: HasProps; - query?: HasProps; - body?: HasProps | Type; -} +export type QueryRT = t.HasProps; -export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; +export type BodyRT = t.Type; -export type RouteRequestHandler = ( - req: Request, - params: ExtractTTypeFromParams -) => Promise; +type DecodeParams< + TQuery extends QueryRT | undefined, + TBody extends BodyRT | undefined +> = (TQuery extends QueryRT ? { query: t.TypeOf } : {}) & + (TBody extends BodyRT ? { body: t.TypeOf } : {}); -export interface RouteDescription< - T extends string, - U extends HttpMethod, - V extends RouteParams, - W +export interface Route< + TName extends string, + TMethod extends HttpMethod | undefined, + TQuery extends QueryRT | undefined, + TBody extends BodyRT | undefined, + TReturn > { - path: T; - method: U; - params?: V; - handler: RouteRequestHandler; + name: TName; + method?: TMethod; + query?: TQuery; + body?: TBody; + handler: ( + req: Request, + params: DecodeParams + ) => Promise; } -export type RouteFactoryFunction< - T extends string, - U extends HttpMethod, - V extends RouteParams, - W -> = (core: InternalCoreSetup) => RouteDescription; +export type RouteFactoryFn< + TName extends string, + TMethod extends HttpMethod | undefined, + TQuery extends QueryRT | undefined, + TBody extends BodyRT | undefined, + TReturn +> = (core: InternalCoreSetup) => Route; -// eslint-disable-next-line @typescript-eslint/prefer-interface -export type ServerAPI = { - add( - fn: RouteFactoryFunction +export type RouteState = Record< + string, + Route +>; + +export interface ServerAPI { + _S: TRouteState; + add< + TName extends string, + TMethod extends HttpMethod | undefined, + TQuery extends QueryRT | undefined, + TBody extends BodyRT | undefined, + TReturn + >( + factoryFn: RouteFactoryFn ): ServerAPI< - T & + TRouteState & { - [key in U]: { - [key in V]: RouteType, X>; - }; + [key in TName]: Route; } >; - call(options: { - pathname: U; - method: V; - params: W extends { params: { [key in keyof RouteParams]: any } } - ? { - [key in keyof W['params']]: W['params'][key]; - } - : {}; - }): W extends { returnType: any } ? Promise : never; init: (core: InternalCoreSetup) => void; -}; - -export type APICall> = T['call']; - -const api: ServerAPI<{}> = {} as any; +} -const call = api.add(core => ({ - path: '/foo', - method: 'GET', - params: { - query: t.partial({ - foo: t.string - }) - }, - handler: async () => { - return null; - } -})).call; +type ClientMethod = TRoute extends Route< + string, + infer TMethod, + infer TQuery, + infer TBody, + infer TReturn +> + ? ( + options: DecodeParams & + (TMethod extends Exclude ? { method: TMethod } : {}) + ) => Promise + : never; -call({ - pathname: '/foo', - method: 'GET', - params: { - query: {} - } -}); +export type Client> = { + [key in keyof TServerAPI['_S']]: ClientMethod; +};