replaces valdiation typing
This commit is contained in:
parent
cff6041b6c
commit
711c098e9b
|
@ -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(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
}
|
||||
|
|
46
x-pack/plugins/event_log/server/lib/date_from_string.test.ts
Normal file
46
x-pack/plugins/event_log/server/lib/date_from_string.test.ts
Normal 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);
|
||||
});
|
||||
});
|
46
x-pack/plugins/event_log/server/lib/date_from_string.ts
Normal file
46
x-pack/plugins/event_log/server/lib/date_from_string.ts
Normal 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}`
|
||||
);
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue