Skip to content
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

add stats api #3

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kbn-securitysolution-es-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './src/set_index_template';
export * from './src/set_policy';
export * from './src/set_template';
export * from './src/transform_error';
export * from './src/build_response';
66 changes: 66 additions & 0 deletions packages/kbn-securitysolution-es-utils/src/build_response/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { CustomHttpResponseOptions, KibanaResponseFactory } from '@kbn/core/server';

const statusToErrorMessage = (
statusCode: number
):
| 'Bad Request'
| 'Unauthorized'
| 'Forbidden'
| 'Not Found'
| 'Conflict'
| 'Internal Error'
| '(unknown error)' => {
switch (statusCode) {
case 400:
return 'Bad Request';
case 401:
return 'Unauthorized';
case 403:
return 'Forbidden';
case 404:
return 'Not Found';
case 409:
return 'Conflict';
case 500:
return 'Internal Error';
default:
return '(unknown error)';
}
};

export class ResponseFactory {
constructor(private response: KibanaResponseFactory) {}

error<T>({ statusCode, body, headers }: CustomHttpResponseOptions<T>) {
// KibanaResponse is not exported so we cannot use a return type here and that is why the linter is turned off above
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider removing this comment if it's no longer applicable

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when running the following command:

node scripts/type_check.js --project x-pack/plugins/data_quality/tsconfig.json

the following type error is reported:

[bazel] packages/kbn-securitysolution-es-utils/src/build_response/index.ts:9:71 - error TS2307: Cannot find module '@kbn/core/server' or its corresponding type declarations.
[bazel]
[bazel] 9 import type { CustomHttpResponseOptions, KibanaResponseFactory } from '@kbn/core/server';
[bazel]                                                                         ~~~~~~~~~~~~~~~~~~
[bazel]
[bazel] Found 1 error in packages/kbn-securitysolution-es-utils/src/build_response/index.ts:9

const contentType: CustomHttpResponseOptions<T>['headers'] = {
'content-type': 'application/json',
};
const defaultedHeaders: CustomHttpResponseOptions<T>['headers'] = {
...contentType,
...(headers ?? {}),
};

return this.response.custom({
body: Buffer.from(
JSON.stringify({
message: body ?? statusToErrorMessage(statusCode),
status_code: statusCode,
})
),
headers: defaultedHeaders,
statusCode,
});
}
}

export const buildResponse = (response: KibanaResponseFactory): ResponseFactory =>
new ResponseFactory(response);
13 changes: 13 additions & 0 deletions x-pack/plugins/data_quality/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const PLUGIN_ID = 'dataQuality';
export const PLUGIN_NAME = 'dataQuality';

export const BASE_PATH = '/internal/data_quality';
export const GET_INDEX_STATS = `${BASE_PATH}/stats/{index_name}`;
export const GET_INDEX_MAPPINGS = `${BASE_PATH}/mappings/{index_name}`;
11 changes: 11 additions & 0 deletions x-pack/plugins/data_quality/server/__mocks__/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { httpServerMock } from '@kbn/core/server/mocks';

export const requestMock = {
create: httpServerMock.createKibanaRequest,
};
56 changes: 56 additions & 0 deletions x-pack/plugins/data_quality/server/__mocks__/request_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { coreMock } from '@kbn/core/server/mocks';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';

export const createMockClients = () => {
const core = coreMock.createRequestHandlerContext();
const license = licensingMock.createLicenseMock();

return {
core,
clusterClient: core.elasticsearch.client,
savedObjectsClient: core.savedObjects.client,

licensing: {
...licensingMock.createRequestHandlerContext({ license }),
license,
},

config: createMockConfig(),
appClient: createAppClientMock(),
};
};

type MockClients = ReturnType<typeof createMockClients>;

const convertRequestContextMock = <T>(context: T) => {
return coreMock.createCustomRequestHandlerContext(context);
};

const createMockConfig = () => ({});

const createAppClientMock = () => ({});

const createRequestContextMock = (clients: MockClients = createMockClients()) => {
return {
core: clients.core,
};
};

const createTools = () => {
const clients = createMockClients();
const context = createRequestContextMock(clients);

return { clients, context };
};

export const requestContextMock = {
create: createRequestContextMock,
convertContext: convertRequestContextMock,
createTools,
};
12 changes: 12 additions & 0 deletions x-pack/plugins/data_quality/server/__mocks__/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { httpServerMock } from '@kbn/core/server/mocks';

export const responseMock = {
create: httpServerMock.createResponseFactory,
};
94 changes: 94 additions & 0 deletions x-pack/plugins/data_quality/server/__mocks__/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { httpServiceMock } from '@kbn/core/server/mocks';
import type { RequestHandler, RouteConfig, KibanaRequest } from '@kbn/core/server';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';

import { requestMock } from './request';
import { responseMock as responseFactoryMock } from './response';
import { requestContextMock } from './request_context';
import { responseAdapter } from './test_adapters';

interface Route {
config: RouteConfig<unknown, unknown, unknown, 'get' | 'post' | 'delete' | 'patch' | 'put'>;
handler: RequestHandler;
}

const getRoute = (routerMock: MockServer['router']): Route => {
const routeCalls = [
...routerMock.get.mock.calls,
...routerMock.post.mock.calls,
...routerMock.put.mock.calls,
...routerMock.patch.mock.calls,
...routerMock.delete.mock.calls,
];

const [route] = routeCalls;
if (!route) {
throw new Error('No route registered!');
}

const [config, handler] = route;
return { config, handler };
};

const buildResultMock = () => ({ ok: jest.fn((x) => x), badRequest: jest.fn((x) => x) });

class MockServer {
constructor(
public readonly router = httpServiceMock.createRouter(),
private responseMock = responseFactoryMock.create(),
private contextMock = requestContextMock.convertContext(requestContextMock.create()),
private resultMock = buildResultMock()
) {}

public validate(request: KibanaRequest) {
this.validateRequest(request);
return this.resultMock;
}

public async inject(request: KibanaRequest, context: RequestHandlerContext = this.contextMock) {
const validatedRequest = this.validateRequest(request);
const [rejection] = this.resultMock.badRequest.mock.calls;
if (rejection) {
throw new Error(`Request was rejected with message: '${rejection}'`);
}

await this.getRoute().handler(context, validatedRequest, this.responseMock);
return responseAdapter(this.responseMock);
}

private getRoute(): Route {
return getRoute(this.router);
}

private maybeValidate(part: any, validator?: any): any {
return typeof validator === 'function' ? validator(part, this.resultMock) : part;
}

private validateRequest(request: KibanaRequest): KibanaRequest {
const validations = this.getRoute().config.validate;
if (!validations) {
return request;
}

const validatedRequest = requestMock.create({
path: request.route.path,
method: request.route.method,
body: this.maybeValidate(request.body, validations.body),
query: this.maybeValidate(request.query, validations.query),
params: this.maybeValidate(request.params, validations.params),
});

return validatedRequest;
}
}
const createMockServer = () => new MockServer();

export const serverMock = {
create: createMockServer,
};
64 changes: 64 additions & 0 deletions x-pack/plugins/data_quality/server/__mocks__/test_adapters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { responseMock } from './response';

type ResponseMock = ReturnType<typeof responseMock.create>;
type Method = keyof ResponseMock;

type MockCall = any; // eslint-disable-line @typescript-eslint/no-explicit-any

interface ResponseCall {
body: any; // eslint-disable-line @typescript-eslint/no-explicit-any
status: number;
}

/**
* @internal
*/
export interface Response extends ResponseCall {
calls: ResponseCall[];
}

const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => {
if (!calls.length) return [];

switch (method) {
case 'ok':
return calls.map(([call]) => ({ status: 200, body: call.body }));
case 'custom':
return calls.map(([call]) => ({
status: call.statusCode,
body: JSON.parse(call.body),
}));
case 'customError':
return calls.map(([call]) => ({
status: call.statusCode,
body: call.body,
}));
default:
throw new Error(`Encountered unexpected call to response.${method}`);
}
};

export const responseAdapter = (response: ResponseMock): Response => {
const methods = Object.keys(response) as Method[];
const calls = methods
.reduce<Response['calls']>((responses, method) => {
const methodMock = response[method];
return [...responses, ...buildResponses(method, methodMock.mock.calls)];
}, [])
.sort((call, other) => other.status - call.status);

const [{ body, status }] = calls;

return {
body,
status,
calls,
};
};
18 changes: 18 additions & 0 deletions x-pack/plugins/data_quality/server/lib/fetch_stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types';
import type { IScopedClusterClient } from '@kbn/core/server';

export const fetchStats = async (
client: IScopedClusterClient,
indexName: string
): Promise<IndicesStatsResponse> =>
await client.asCurrentUser.indices.stats({
expand_wildcards: ['open'],
index: indexName,
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
* 2.0.
*/

export const PLUGIN_ID = 'dataQuality';
export const PLUGIN_NAME = 'dataQuality';
export * from './fetch_mappings';
export * from './fetch_stats';
6 changes: 3 additions & 3 deletions x-pack/plugins/data_quality/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';

import { DataQualityPluginSetup, DataQualityPluginStart } from './types';
import { defineRoutes } from './routes';
import { getIndexMappingsRoute, getIndexStatsRoute } from './routes';

export class DataQualityPlugin implements Plugin<DataQualityPluginSetup, DataQualityPluginStart> {
private readonly logger: Logger;
Expand All @@ -22,8 +22,8 @@ export class DataQualityPlugin implements Plugin<DataQualityPluginSetup, DataQua
const router = core.http.createRouter();

// Register server side APIs
defineRoutes(router);

getIndexMappingsRoute(router);
getIndexStatsRoute(router);
return {};
}

Expand Down
Loading