-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use HTTP request schemas to create types, use those types in the client #59340
Changes from all commits
3142dc7
56cccba
5f4914e
372ed2c
7932058
287a2a5
52026d2
a5a6e79
814e311
c965270
21a84e6
07fceb7
1163eb3
a25663f
513dbe0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -610,7 +610,7 @@ export interface HttpFetchOptionsWithPath extends HttpFetchOptions { | |
// @public (undocumented) | ||
export interface HttpFetchQuery { | ||
// (undocumented) | ||
[key: string]: string | number | boolean | undefined; | ||
[key: string]: string | number | boolean | undefined | Array<string | number | boolean | undefined>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. generated docs |
||
} | ||
|
||
// @public | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Schemas | ||
|
||
These schemas are used to validate, coerce, and provide types for the comms between the client, server, and ES. | ||
|
||
# Future work | ||
In the future, we may be able to locate these under 'server'. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,9 @@ | |
*/ | ||
|
||
import { SearchResponse } from 'elasticsearch'; | ||
import { TypeOf } from '@kbn/config-schema'; | ||
import * as kbnConfigSchemaTypes from '@kbn/config-schema/target/types/types'; | ||
import { alertingIndexGetQuerySchema } from './schema/alert_index'; | ||
|
||
/** | ||
* 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>>; | |
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>; | ||
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }; | ||
|
||
export enum Direction { | ||
asc = 'asc', | ||
desc = 'desc', | ||
} | ||
export type Direction = 'asc' | 'desc'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @oatkiller Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other plugins have used enum for this... for example, siem. So just curious what the advantages/disadvantages are. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't have a strong reason for changing this, was just getting around some quick type errors. Let me try and put it back. The tl;dr is: enum's end up in the runtime. I think they're good for giving names to magic numbers that you use for data interchange, e.g. binary header sentinels, status codes, etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so i tried to go back and make everything the enum, but it turns out we have a whole lot of places in the app writing 'asc' and 'desc' as literals. I don't honestly know what the best practice is, but for the sake of keeping the (runtime) enum out of the validation that will be used on FE and BE, I'm going to keep this as a type for now. I'm aware that this may not be right, I just don't have quite enough experience w/ this yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, that's fine. I imagine that could be difficult to work into the schema validation. Let's come back to it later. |
||
|
||
export class EndpointAppConstants { | ||
static BASE_API_URL = '/api/endpoint'; | ||
|
@@ -45,7 +45,6 @@ export class EndpointAppConstants { | |
**/ | ||
static ALERT_LIST_DEFAULT_PAGE_SIZE = 10; | ||
static ALERT_LIST_DEFAULT_SORT = '@timestamp'; | ||
static ALERT_LIST_DEFAULT_ORDER = Direction.desc; | ||
} | ||
|
||
export interface AlertResultList { | ||
|
@@ -336,3 +335,72 @@ export type ResolverEvent = EndpointEvent | LegacyEndpointEvent; | |
* The PageId type is used for the payload when firing userNavigatedToPage actions | ||
*/ | ||
export type PageId = 'alertsPage' | 'managementPage' | 'policyListPage'; | ||
|
||
/** | ||
* Takes a @kbn/config-schema 'schema' type and returns a type that represents valid inputs. | ||
* Similar to `TypeOf`, but allows strings as input for `schema.number()` (which is inline | ||
* with the behavior of the validator.) Also, for `schema.object`, when a value is a `schema.maybe` | ||
* the key will be marked optional (via `?`) so that you can omit keys for optional values. | ||
* | ||
* Use this when creating a value that will be passed to the schema. | ||
* e.g. | ||
* ```ts | ||
* const input: KbnConfigSchemaInputTypeOf<typeof schema> = value | ||
* schema.validate(input) // should be valid | ||
* ``` | ||
*/ | ||
type KbnConfigSchemaInputTypeOf< | ||
T extends kbnConfigSchemaTypes.Type<unknown> | ||
> = T extends kbnConfigSchemaTypes.ObjectType | ||
? KbnConfigSchemaInputObjectTypeOf< | ||
T | ||
> /** `schema.number()` accepts strings, so this type should accept them as well. */ | ||
: kbnConfigSchemaTypes.Type<number> extends T | ||
? TypeOf<T> | string | ||
: TypeOf<T>; | ||
|
||
/** | ||
* Works like ObjectResultType, except that 'maybe' schema will create an optional key. | ||
* This allows us to avoid passing 'maybeKey: undefined' when constructing such an object. | ||
* | ||
* Instead of using this directly, use `InputTypeOf`. | ||
*/ | ||
type KbnConfigSchemaInputObjectTypeOf< | ||
T extends kbnConfigSchemaTypes.ObjectType | ||
> = T extends kbnConfigSchemaTypes.ObjectType<infer P> | ||
? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This maps the properties of a schema object. It does it twice, once for values that accept 'undefined', and once for keys that do not. If a value accepts undefined, they key is marked optional (?) |
||
/** Use ? to make the field optional if the prop accepts undefined. | ||
* This allows us to avoid writing `field: undefined` for optional fields. | ||
*/ | ||
[K in Exclude< | ||
keyof P, | ||
keyof KbnConfigSchemaNonOptionalProps<P> | ||
>]?: KbnConfigSchemaInputTypeOf<P[K]>; | ||
} & | ||
{ [K in keyof KbnConfigSchemaNonOptionalProps<P>]: KbnConfigSchemaInputTypeOf<P[K]> } | ||
: never; | ||
|
||
/** | ||
* Takes the props of a schema.object type, and returns a version that excludes | ||
* optional values. Used by `InputObjectTypeOf`. | ||
* | ||
* Instead of using this directly, use `InputTypeOf`. | ||
*/ | ||
type KbnConfigSchemaNonOptionalProps<Props extends kbnConfigSchemaTypes.Props> = Pick< | ||
Props, | ||
{ | ||
[Key in keyof Props]: undefined extends TypeOf<Props[Key]> ? never : Key; | ||
}[keyof Props] | ||
>; | ||
|
||
/** | ||
* Query params to pass to the alert API when fetching new data. | ||
*/ | ||
export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👏 👏 👏 |
||
typeof alertingIndexGetQuerySchema | ||
>; | ||
|
||
/** | ||
* Result of the validated query params when handling alert index requests. | ||
*/ | ||
export type AlertingIndexGetQueryResult = TypeOf<typeof alertingIndexGetQuerySchema>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,14 +8,15 @@ import { AlertResultList, AlertData } from '../../../../../common/types'; | |
import { AppAction } from '../action'; | ||
import { MiddlewareFactory, AlertListState } from '../../types'; | ||
import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; | ||
import { cloneHttpFetchQuery } from '../../../../common/clone_http_fetch_query'; | ||
|
||
export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreStart => { | ||
return api => next => async (action: AppAction) => { | ||
next(action); | ||
const state = api.getState(); | ||
if (action.type === 'userChangedUrl' && isOnAlertPage(state)) { | ||
const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, { | ||
query: apiQueryParams(state), | ||
query: cloneHttpFetchQuery(apiQueryParams(state)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems okay to me. |
||
}); | ||
api.dispatch({ type: 'serverReturnedAlertsData', payload: response }); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* 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 { cloneHttpFetchQuery } from './clone_http_fetch_query'; | ||
import { Immutable } from '../../common/types'; | ||
import { HttpFetchQuery } from '../../../../../src/core/public'; | ||
|
||
describe('cloneHttpFetchQuery', () => { | ||
it('can clone complex queries', () => { | ||
const query: Immutable<HttpFetchQuery> = { | ||
a: 'a', | ||
'1': 1, | ||
undefined, | ||
array: [1, 2, undefined], | ||
}; | ||
expect(cloneHttpFetchQuery(query)).toMatchInlineSnapshot(` | ||
Object { | ||
"1": 1, | ||
"a": "a", | ||
"array": Array [ | ||
1, | ||
2, | ||
undefined, | ||
], | ||
"undefined": undefined, | ||
} | ||
`); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* 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 { Immutable } from '../../common/types'; | ||
|
||
import { HttpFetchQuery } from '../../../../../src/core/public'; | ||
|
||
export function cloneHttpFetchQuery(query: Immutable<HttpFetchQuery>): HttpFetchQuery { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. simply clone a query object. |
||
const clone: HttpFetchQuery = {}; | ||
for (const [key, value] of Object.entries(query)) { | ||
if (Array.isArray(value)) { | ||
clone[key] = [...value]; | ||
} else { | ||
// Array.isArray is not removing ImmutableArray from the union. | ||
clone[key] = value as string | number | boolean; | ||
} | ||
} | ||
return clone; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The underlying code supports this already. This change loosens up the type