replaces valdiation typing

This commit is contained in:
Gidi Meir Morris 2020-04-02 19:42:57 +01:00
parent cff6041b6c
commit 711c098e9b
9 changed files with 154 additions and 87 deletions

View file

@ -208,7 +208,7 @@ describe('queryEventsBySavedObject', () => {
'index-name',
'saved-object-type',
'saved-object-id',
{ page: 10, per_page: 10, start: undefined, end: undefined }
{ page: 10, per_page: 10 }
);
expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('search', {
index: 'index-name',
@ -240,7 +240,7 @@ describe('queryEventsBySavedObject', () => {
const start = moment()
.subtract(1, 'days')
.toISOString();
.toDate();
await clusterClientAdapter.queryEventsBySavedObject(
'index-name',
@ -265,7 +265,7 @@ describe('queryEventsBySavedObject', () => {
{
range: {
'event.start': {
gte: start,
gte: start.toISOString(),
},
},
},
@ -285,10 +285,10 @@ describe('queryEventsBySavedObject', () => {
const start = moment()
.subtract(1, 'days')
.toISOString();
.toDate();
const end = moment()
.add(1, 'days')
.toISOString();
.toDate();
await clusterClientAdapter.queryEventsBySavedObject(
'index-name',
@ -313,14 +313,14 @@ describe('queryEventsBySavedObject', () => {
{
range: {
'event.start': {
gte: start,
gte: start.toISOString(),
},
},
},
{
range: {
'event.end': {
lte: end,
lte: end.toISOString(),
},
},
},

View file

@ -114,7 +114,7 @@ export class ClusterClientAdapter {
index: string,
type: string,
id: string,
{ page, per_page: size, start, end }: Partial<FindOptionsType>
{ page, per_page: size, start, end }: FindOptionsType
): Promise<any[]> {
try {
const {
@ -125,6 +125,7 @@ export class ClusterClientAdapter {
...(size && page
? {
size,
// `page` count is a positive number, `from` is zero based index
from: (page - 1) * size,
}
: {}),
@ -141,14 +142,14 @@ export class ClusterClientAdapter {
start && {
range: {
'event.start': {
gte: start,
gte: start.toISOString(),
},
},
},
end && {
range: {
'event.end': {
lte: end,
lte: end.toISOString(),
},
},
},

View file

@ -160,10 +160,10 @@ describe('EventLogStart', () => {
const start = moment()
.subtract(1, 'days')
.toISOString();
.toDate();
const end = moment()
.add(1, 'days')
.toISOString();
.toDate();
expect(
await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', {
@ -184,54 +184,6 @@ describe('EventLogStart', () => {
}
);
});
test('validates that the start date is valid', async () => {
const esContext = contextMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
const eventLogClient = new EventLogClient({
esContext,
savedObjectsClient,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: 'saved-object-id',
type: 'saved-object-type',
attributes: {},
references: [],
});
esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue([]);
expect(
eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', {
start: 'not a date string',
})
).rejects.toMatchInlineSnapshot(`[Error: [start]: Invalid Date]`);
});
test('validates that the end date is valid', async () => {
const esContext = contextMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
const eventLogClient = new EventLogClient({
esContext,
savedObjectsClient,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: 'saved-object-id',
type: 'saved-object-type',
attributes: {},
references: [],
});
esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue([]);
expect(
eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', {
end: 'not a date string',
})
).rejects.toMatchInlineSnapshot(`[Error: [end]: Invalid Date]`);
});
});
});

View file

@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Observable } from 'rxjs';
import { ClusterClient, SavedObjectsClientContract } from 'src/core/server';
import { schema, TypeOf } from '@kbn/config-schema';
import { defaults } from 'lodash';
import { EsContext } from './es';
import { IEventLogClient, IEvent } from './types';
import { DateFromString, PositiveNumberFromString } from './lib/date_from_string';
export type PluginClusterClient = Pick<ClusterClient, 'callAsInternalUser' | 'asScoped'>;
export type AdminClusterClient$ = Observable<PluginClusterClient>;
@ -18,23 +20,13 @@ interface EventLogServiceCtorParams {
savedObjectsClient: SavedObjectsClientContract;
}
const optionalDateFieldSchema = schema.maybe(
schema.string({
validate(value) {
if (isNaN(Date.parse(value))) {
return 'Invalid Date';
}
},
})
);
export const findOptionsSchema = schema.object({
per_page: schema.number({ defaultValue: 10, min: 0 }),
page: schema.number({ defaultValue: 1, min: 1 }),
start: optionalDateFieldSchema,
end: optionalDateFieldSchema,
export const FindOptionsSchema = t.partial({
per_page: PositiveNumberFromString,
page: PositiveNumberFromString,
start: DateFromString,
end: DateFromString,
});
export type FindOptionsType = TypeOf<typeof findOptionsSchema>;
export type FindOptionsType = t.TypeOf<typeof FindOptionsSchema>;
// note that clusterClient may be null, indicating we can't write to ES
export class EventLogClient implements IEventLogClient {
@ -49,14 +41,14 @@ export class EventLogClient implements IEventLogClient {
async findEventsBySavedObject(
type: string,
id: string,
options?: Partial<FindOptionsType>
options: FindOptionsType = {}
): Promise<IEvent[]> {
await this.savedObjectsClient.get(type, id);
return (await this.esContext.esAdapter.queryEventsBySavedObject(
this.esContext.esNames.alias,
type,
id,
findOptionsSchema.validate(options ?? {})
defaults(options, { page: 1, per_page: 10 })
)) as IEvent[];
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 { DateFromString, PositiveNumberFromString } from './date_from_string';
import { right, isLeft } from 'fp-ts/lib/Either';
describe('DateFromString', () => {
test('validated and parses a string into a Date', () => {
const date = new Date(1973, 10, 30);
expect(DateFromString.decode(date.toISOString())).toEqual(right(date));
});
test('validated and returns a failure for an actual Date', () => {
const date = new Date(1973, 10, 30);
expect(isLeft(DateFromString.decode(date))).toEqual(true);
});
test('validated and returns a failure for an invalid Date string', () => {
expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true);
});
test('validated and returns a failure for a null value', () => {
expect(isLeft(DateFromString.decode(null))).toEqual(true);
});
});
describe('PositiveNumberFromString', () => {
test('validated and parses a string into a positive number', () => {
expect(PositiveNumberFromString.decode('1')).toEqual(right(1));
});
test('validated and returns a failure for an invalid number', () => {
expect(isLeft(PositiveNumberFromString.decode('a23'))).toEqual(true);
});
test('validated and returns a failure for a negative number', () => {
expect(isLeft(PositiveNumberFromString.decode('-45'))).toEqual(true);
});
test('validated and returns a failure for a null value', () => {
expect(isLeft(PositiveNumberFromString.decode(null))).toEqual(true);
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 * as t from 'io-ts';
import { isNumber } from 'lodash';
import { either } from 'fp-ts/lib/Either';
// represents a Date from an ISO string
export const DateFromString = new t.Type<Date, string, unknown>(
'DateFromString',
// detect the type
(value): value is Date => value instanceof Date,
(valueToDecode, context) =>
either.chain(
// validate this is a string
t.string.validate(valueToDecode, context),
// decode
value => {
const decoded = new Date(value);
return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded);
}
),
valueToEncode => valueToEncode.toISOString()
);
export const PositiveNumberFromString = new t.Type<number, string, unknown>(
'PositiveNumberFromString',
// detect the type
(value): value is number => isNumber(value),
(valueToDecode, context) =>
either.chain(
// validate this is a string
t.string.validate(valueToDecode, context),
// decode
value => {
const decoded = parseInt(value, 10);
return isNaN(decoded) || decoded < 0
? t.failure(valueToDecode, context)
: t.success(decoded);
}
),
valueToEncode => `${valueToEncode}`
);

View file

@ -0,0 +1,25 @@
/*
* 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 * as t from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { RouteValidationResultFactory, RouteValidationFunction } from 'kibana/server';
import { Type } from 'io-ts';
export const routeValidatorByType = <T extends Type<any, any, any>>(type: T) => (
value: any,
{ ok, badRequest }: RouteValidationResultFactory
) => {
type TypeOf = t.TypeOf<typeof type>;
// const twemp = type.decode(value)
return pipe(
type.decode(value),
fold<t.Errors, TypeOf, ReturnType<RouteValidationFunction<TypeOf>>>(
(errors: t.Errors) => badRequest(errors.map(e => `${e.message ?? e.value}`).join('\n')),
(val: TypeOf) => ok(val)
)
);
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import * as t from 'io-ts';
import {
IRouter,
RequestHandlerContext,
@ -13,25 +13,27 @@ import {
KibanaResponseFactory,
} from 'kibana/server';
import { BASE_EVENT_LOG_API_PATH } from '../../common';
import { findOptionsSchema, FindOptionsType } from '../event_log_client';
import { FindOptionsSchema, FindOptionsType } from '../event_log_client';
import { routeValidatorByType } from '../lib/route_validator_by_type';
const paramSchema = schema.object({
type: schema.string(),
id: schema.string(),
const ParamsSchema = t.type({
type: t.string,
id: t.string,
});
type ParamsType = t.TypeOf<typeof ParamsSchema>;
export const findRoute = (router: IRouter) => {
router.get(
{
path: `${BASE_EVENT_LOG_API_PATH}/{type}/{id}/_find`,
validate: {
params: paramSchema,
query: findOptionsSchema,
params: routeValidatorByType(ParamsSchema),
query: routeValidatorByType(FindOptionsSchema),
},
},
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<TypeOf<typeof paramSchema>, FindOptionsType, any, any>,
req: KibanaRequest<ParamsType, FindOptionsType, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
if (!context.eventLog) {

View file

@ -48,6 +48,7 @@ export default function({ getService }: FtrProviderContext) {
await logTestEvent(id, firstExpectedEvent);
await Promise.all(expectedEvents.map(event => logTestEvent(id, event)));
log.debug(`Query with default pagination`);
await retry.try(async () => {
const { body: foundEvents } = await supertest
.get(`/api/event_log/event_log_test/${id}/_find`)
@ -62,6 +63,7 @@ export default function({ getService }: FtrProviderContext) {
3
);
log.debug(`Query with per_page pagination`);
const { body: firstPage } = await supertest
.get(`/api/event_log/event_log_test/${id}/_find?per_page=3`)
.set('kbn-xsrf', 'foo')
@ -70,6 +72,7 @@ export default function({ getService }: FtrProviderContext) {
expect(firstPage.length).to.be(3);
assertEventsFromApiMatchCreatedEvents(firstPage, expectedFirstPage);
log.debug(`Query with all pagination params`);
const { body: secondPage } = await supertest
.get(`/api/event_log/event_log_test/${id}/_find?per_page=3&page=2`)
.set('kbn-xsrf', 'foo')