Skip to content

Commit 12a3ccf

Browse files
Robert Austinelasticmachine
andauthored
Use HTTP request schemas to create types, use those types in the client (#59340)
* wip * wip * wip * will this work? * wip but it works * pedro * remove thing * remove TODOs * fix type issue * add tests to check that alert index api works * Revert "add tests to check that alert index api works" This reverts commit 5d40ca18337cf8deb63a0291150780ec094db016. * Moved schema * undoing my evils * fix comments. fix incorrect import Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 6e5e8c8 commit 12a3ccf

File tree

18 files changed

+192
-77
lines changed

18 files changed

+192
-77
lines changed

src/core/public/http/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,12 @@ export interface HttpRequestInit {
205205

206206
/** @public */
207207
export interface HttpFetchQuery {
208-
[key: string]: string | number | boolean | undefined;
208+
[key: string]:
209+
| string
210+
| number
211+
| boolean
212+
| undefined
213+
| Array<string | number | boolean | undefined>;
209214
}
210215

211216
/**

src/core/public/public.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ export interface HttpFetchOptionsWithPath extends HttpFetchOptions {
610610
// @public (undocumented)
611611
export interface HttpFetchQuery {
612612
// (undocumented)
613-
[key: string]: string | number | boolean | undefined;
613+
[key: string]: string | number | boolean | undefined | Array<string | number | boolean | undefined>;
614614
}
615615

616616
// @public
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Schemas
2+
3+
These schemas are used to validate, coerce, and provide types for the comms between the client, server, and ES.
4+
5+
# Future work
6+
In the future, we may be able to locate these under 'server'.

x-pack/plugins/endpoint/server/routes/alerts/list/schemas.ts renamed to x-pack/plugins/endpoint/common/schema/alert_index.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
import { decode } from 'rison-node';
6+
7+
import { schema, Type } from '@kbn/config-schema';
78
import { i18n } from '@kbn/i18n';
8-
import { schema } from '@kbn/config-schema';
9-
import { esKuery } from '../../../../../../../src/plugins/data/server';
10-
import { EndpointAppConstants } from '../../../../common/types';
9+
import { decode } from 'rison-node';
10+
import { fromKueryExpression } from '../../../../../src/plugins/data/common';
11+
import { EndpointAppConstants } from '../types';
1112

12-
export const alertListReqSchema = schema.object(
13+
/**
14+
* Used to validate GET requests against the index of the alerting APIs.
15+
*/
16+
export const alertingIndexGetQuerySchema = schema.object(
1317
{
1418
page_size: schema.maybe(
1519
schema.number({
@@ -26,31 +30,21 @@ export const alertListReqSchema = schema.object(
2630
schema.arrayOf(schema.string(), {
2731
minSize: 2,
2832
maxSize: 2,
29-
})
33+
}) as Type<[string, string]> // Cast this to a string tuple. `@kbn/config-schema` doesn't do this automatically
3034
),
3135
before: schema.maybe(
3236
schema.arrayOf(schema.string(), {
3337
minSize: 2,
3438
maxSize: 2,
35-
})
39+
}) as Type<[string, string]> // Cast this to a string tuple. `@kbn/config-schema` doesn't do this automatically
3640
),
3741
sort: schema.maybe(schema.string()),
38-
order: schema.maybe(
39-
schema.string({
40-
validate(value) {
41-
if (value !== 'asc' && value !== 'desc') {
42-
return i18n.translate('xpack.endpoint.alerts.errors.bad_sort_direction', {
43-
defaultMessage: 'must be `asc` or `desc`',
44-
});
45-
}
46-
},
47-
})
48-
),
42+
order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])),
4943
query: schema.maybe(
5044
schema.string({
5145
validate(value) {
5246
try {
53-
esKuery.fromKueryExpression(value);
47+
fromKueryExpression(value);
5448
} catch (err) {
5549
return i18n.translate('xpack.endpoint.alerts.errors.bad_kql', {
5650
defaultMessage: 'must be valid KQL',

x-pack/plugins/endpoint/common/types.ts

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*/
66

77
import { SearchResponse } from 'elasticsearch';
8+
import { TypeOf } from '@kbn/config-schema';
9+
import * as kbnConfigSchemaTypes from '@kbn/config-schema/target/types/types';
10+
import { alertingIndexGetQuerySchema } from './schema/alert_index';
811

912
/**
1013
* A deep readonly type that will make all children of a given object readonly recursively
@@ -24,10 +27,7 @@ export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
2427
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
2528
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
2629

27-
export enum Direction {
28-
asc = 'asc',
29-
desc = 'desc',
30-
}
30+
export type Direction = 'asc' | 'desc';
3131

3232
export class EndpointAppConstants {
3333
static BASE_API_URL = '/api/endpoint';
@@ -45,7 +45,6 @@ export class EndpointAppConstants {
4545
**/
4646
static ALERT_LIST_DEFAULT_PAGE_SIZE = 10;
4747
static ALERT_LIST_DEFAULT_SORT = '@timestamp';
48-
static ALERT_LIST_DEFAULT_ORDER = Direction.desc;
4948
}
5049

5150
export interface AlertResultList {
@@ -336,3 +335,72 @@ export type ResolverEvent = EndpointEvent | LegacyEndpointEvent;
336335
* The PageId type is used for the payload when firing userNavigatedToPage actions
337336
*/
338337
export type PageId = 'alertsPage' | 'managementPage' | 'policyListPage';
338+
339+
/**
340+
* Takes a @kbn/config-schema 'schema' type and returns a type that represents valid inputs.
341+
* Similar to `TypeOf`, but allows strings as input for `schema.number()` (which is inline
342+
* with the behavior of the validator.) Also, for `schema.object`, when a value is a `schema.maybe`
343+
* the key will be marked optional (via `?`) so that you can omit keys for optional values.
344+
*
345+
* Use this when creating a value that will be passed to the schema.
346+
* e.g.
347+
* ```ts
348+
* const input: KbnConfigSchemaInputTypeOf<typeof schema> = value
349+
* schema.validate(input) // should be valid
350+
* ```
351+
*/
352+
type KbnConfigSchemaInputTypeOf<
353+
T extends kbnConfigSchemaTypes.Type<unknown>
354+
> = T extends kbnConfigSchemaTypes.ObjectType
355+
? KbnConfigSchemaInputObjectTypeOf<
356+
T
357+
> /** `schema.number()` accepts strings, so this type should accept them as well. */
358+
: kbnConfigSchemaTypes.Type<number> extends T
359+
? TypeOf<T> | string
360+
: TypeOf<T>;
361+
362+
/**
363+
* Works like ObjectResultType, except that 'maybe' schema will create an optional key.
364+
* This allows us to avoid passing 'maybeKey: undefined' when constructing such an object.
365+
*
366+
* Instead of using this directly, use `InputTypeOf`.
367+
*/
368+
type KbnConfigSchemaInputObjectTypeOf<
369+
T extends kbnConfigSchemaTypes.ObjectType
370+
> = T extends kbnConfigSchemaTypes.ObjectType<infer P>
371+
? {
372+
/** Use ? to make the field optional if the prop accepts undefined.
373+
* This allows us to avoid writing `field: undefined` for optional fields.
374+
*/
375+
[K in Exclude<
376+
keyof P,
377+
keyof KbnConfigSchemaNonOptionalProps<P>
378+
>]?: KbnConfigSchemaInputTypeOf<P[K]>;
379+
} &
380+
{ [K in keyof KbnConfigSchemaNonOptionalProps<P>]: KbnConfigSchemaInputTypeOf<P[K]> }
381+
: never;
382+
383+
/**
384+
* Takes the props of a schema.object type, and returns a version that excludes
385+
* optional values. Used by `InputObjectTypeOf`.
386+
*
387+
* Instead of using this directly, use `InputTypeOf`.
388+
*/
389+
type KbnConfigSchemaNonOptionalProps<Props extends kbnConfigSchemaTypes.Props> = Pick<
390+
Props,
391+
{
392+
[Key in keyof Props]: undefined extends TypeOf<Props[Key]> ? never : Key;
393+
}[keyof Props]
394+
>;
395+
396+
/**
397+
* Query params to pass to the alert API when fetching new data.
398+
*/
399+
export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf<
400+
typeof alertingIndexGetQuerySchema
401+
>;
402+
403+
/**
404+
* Result of the validated query params when handling alert index requests.
405+
*/
406+
export type AlertingIndexGetQueryResult = TypeOf<typeof alertingIndexGetQuerySchema>;

x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { AlertResultList, AlertData } from '../../../../../common/types';
88
import { AppAction } from '../action';
99
import { MiddlewareFactory, AlertListState } from '../../types';
1010
import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors';
11+
import { cloneHttpFetchQuery } from '../../../../common/clone_http_fetch_query';
1112

1213
export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreStart => {
1314
return api => next => async (action: AppAction) => {
1415
next(action);
1516
const state = api.getState();
1617
if (action.type === 'userChangedUrl' && isOnAlertPage(state)) {
1718
const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, {
18-
query: apiQueryParams(state),
19+
query: cloneHttpFetchQuery(apiQueryParams(state)),
1920
});
2021
api.dispatch({ type: 'serverReturnedAlertsData', payload: response });
2122
}

x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,11 @@ import {
99
createSelector,
1010
createStructuredSelector as createStructuredSelectorWithBadType,
1111
} from 'reselect';
12-
import {
13-
AlertListState,
14-
AlertingIndexUIQueryParams,
15-
AlertsAPIQueryParams,
16-
CreateStructuredSelector,
17-
} from '../../types';
18-
import { Immutable } from '../../../../../common/types';
12+
import { AlertListState, AlertingIndexUIQueryParams, CreateStructuredSelector } from '../../types';
13+
import { Immutable, AlertingIndexGetQueryInput } from '../../../../../common/types';
1914

2015
const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType;
16+
2117
/**
2218
* Returns the Alert Data array from state
2319
*/
@@ -82,14 +78,18 @@ export const uiQueryParams: (
8278
*/
8379
export const apiQueryParams: (
8480
state: AlertListState
85-
) => Immutable<AlertsAPIQueryParams> = createSelector(
81+
) => Immutable<AlertingIndexGetQueryInput> = createSelector(
8682
uiQueryParams,
8783
({ page_size, page_index }) => ({
8884
page_size,
8985
page_index,
9086
})
9187
);
9288

89+
/**
90+
* True if the user has selected an alert to see details about.
91+
* Populated via the browsers query params.
92+
*/
9393
export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector(
9494
uiQueryParams,
9595
({ selected_alert: selectedAlert }) => selectedAlert !== undefined

x-pack/plugins/endpoint/public/applications/endpoint/types.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import { Dispatch, MiddlewareAPI } from 'redux';
8-
import { CoreStart } from 'kibana/public';
98
import {
109
EndpointMetadata,
1110
AlertData,
@@ -14,6 +13,7 @@ import {
1413
ImmutableArray,
1514
} from '../../../common/types';
1615
import { AppAction } from './store/action';
16+
import { CoreStart } from '../../../../../../src/core/public';
1717

1818
export { AppAction };
1919
export type MiddlewareFactory<S = GlobalState> = (
@@ -140,17 +140,3 @@ export interface AlertingIndexUIQueryParams {
140140
*/
141141
selected_alert?: string;
142142
}
143-
144-
/**
145-
* Query params to pass to the alert API when fetching new data.
146-
*/
147-
export interface AlertsAPIQueryParams {
148-
/**
149-
* Number of results to return.
150-
*/
151-
page_size?: string;
152-
/**
153-
* 0-based index of 'page' to return.
154-
*/
155-
page_index?: string;
156-
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { cloneHttpFetchQuery } from './clone_http_fetch_query';
8+
import { Immutable } from '../../common/types';
9+
import { HttpFetchQuery } from '../../../../../src/core/public';
10+
11+
describe('cloneHttpFetchQuery', () => {
12+
it('can clone complex queries', () => {
13+
const query: Immutable<HttpFetchQuery> = {
14+
a: 'a',
15+
'1': 1,
16+
undefined,
17+
array: [1, 2, undefined],
18+
};
19+
expect(cloneHttpFetchQuery(query)).toMatchInlineSnapshot(`
20+
Object {
21+
"1": 1,
22+
"a": "a",
23+
"array": Array [
24+
1,
25+
2,
26+
undefined,
27+
],
28+
"undefined": undefined,
29+
}
30+
`);
31+
});
32+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { Immutable } from '../../common/types';
8+
9+
import { HttpFetchQuery } from '../../../../../src/core/public';
10+
11+
export function cloneHttpFetchQuery(query: Immutable<HttpFetchQuery>): HttpFetchQuery {
12+
const clone: HttpFetchQuery = {};
13+
for (const [key, value] of Object.entries(query)) {
14+
if (Array.isArray(value)) {
15+
clone[key] = [...value];
16+
} else {
17+
// Array.isArray is not removing ImmutableArray from the union.
18+
clone[key] = value as string | number | boolean;
19+
}
20+
}
21+
return clone;
22+
}

0 commit comments

Comments
 (0)