Skip to content

Commit

Permalink
add TSDoc
Browse files Browse the repository at this point in the history
  • Loading branch information
mshustov committed Jul 30, 2019
1 parent 179ab37 commit 1728522
Show file tree
Hide file tree
Showing 16 changed files with 477 additions and 89 deletions.
158 changes: 158 additions & 0 deletions src/core/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,164 @@ class MyPlugin {
}
```

### Handle HTTP request with New Platform HTTP Service
Kibana HTTP Service provides own abstraction for work with HTTP stack.
Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover,
you shouldn't rely on the fact that HTTP Service uses one or another library under the hood, there are high chances that we can switch to another stack at all. If HTTP Service
lacks needed functionality we happy to discuss and support your needs.

To handle an incoming request in your plugin you should:
- create Router instance. Use `plugin id` as a prefix path segment for your routes.
```ts
import { Router } from 'src/core/server';
const router = new Router('my-app');
```
- use `@kbn/config-schema` package to create a schema to validate `request params`, `query`, `body` if an incoming request used them to pass additional details. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error.
To opt out of validating the request, specify `false`.
```ts
import { schema, TypeOf } from '@kbn/config-schema';
const validate = {
params: schema.object({
id: schema.string(),
}),
};
```
- declare a function to respond to incoming request.
The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`.
And `createResponse` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status.
Unlike, `hapi` route handler in the Legacy platform, any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation.
```ts
const handler = async (request: KibanaRequest, createResponse: ResponseFactory) => {
const data = await findObject(request.params.id);
// creates a command to respond with 'not found' error
if (!data) return createResponse.notFound();
// creates a command to send found data to the client and set response headers
return createResponse.ok(data, {
headers: {
'content-type': 'application/json'
}
});
}
```
- register route handler for GET request to 'my-app/path/{id}' path
```ts
import { schema, TypeOf } from '@kbn/config-schema';
import { Router } from 'src/core/server';
const router = new Router('my-app');

const validate = {
params: schema.object({
id: schema.string(),
}),
};

router.get(
{
path: 'path/{id}',
validate
},
async (request, createResponse) => {
const data = await findObject(request.params.id);
if (!data) return createResponse.notFound();
return createResponse.ok(data, {
headers: {
'content-type': 'application/json'
}
});
}
);
```
#### What data I can send back with a response?
You have several options to create a response utilizing `createResponse`:
1. create a successful response. Supported types of response body are:
- `undefined`, no content to send.
- `string`, send text
- `JSON`, send JSON object, HTTP server will throw if given object is not valid (has circular references, for example)
- `Stream` send data stream
- `Buffer` send binary stream
```js
return response.ok(undefined);
return response.ok('ack');
return response.ok({ id: '1' });
return response.ok(Buffer.from(...););

const stream = new Stream.PassThrough();
fs.createReadStream('./file').pipe(stream);
return res.ok(stream);
```
HTTP headers are configurable via response factory parameter `options`.
```js
return response.ok({ id: '1' }, {
headers: {
'content-type': 'application/json'
}
});
```
2. create redirect response. Redirection URL is configures via 'Location' header.
```js
return response.redirected('The document has moved', {
headers: {
location: '/new-url',
},
});
```
3. create error response. You may pass an error message to the client, where error message can be:
- `string` send message text
- `Error` send the message text of given Error object.
- `{ message: string | Error, meta: {data: Record<string, any>, ...} }` - send message text and attach additional error metadata.
```js
return response.unauthorized('User has no access to the requested resource.', {
headers: {
'WWW-Authenticate': 'challenge',
}
})
return response.badRequest();
return response.badRequest('validation error');

try {
// ...
} catch(error){
return response.badRequest(error);
}

return response.badRequest({
message: 'validation error',
meta: {
data: {
requestBody: request.body,
failedFields: validationResult
},
}
});

try {
// ...
} catch(error){
return response.badRequest({
message: error,
meta: {
data: {
requestBody: request.body,
},
}
});
}

```
4. create a custom response. It might happen that `response factory` doesn't cover your use case and you want to specify HTTP response status code as well.
```js
return response.custom('ok', {
statusCode: 201,
headers: {
location: '/created-url'
}
})
```
### Mock core services in tests
Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts.
```typescript
Expand Down
5 changes: 1 addition & 4 deletions src/core/server/elasticsearch/cluster_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
*/
import { Client } from 'elasticsearch';
import { get } from 'lodash';
import { Request } from 'hapi';

import { ElasticsearchErrorHelpers } from './errors';
import { GetAuthHeaders, isRealRequest } from '../http';
import { GetAuthHeaders, isRealRequest, LegacyRequest } from '../http';
import { filterHeaders, Headers, KibanaRequest, ensureRawRequest } from '../http/router';
import { Logger } from '../logging';
import {
Expand All @@ -36,8 +35,6 @@ import { ScopedClusterClient } from './scoped_cluster_client';
* @public
*/

export type LegacyRequest = Request;

const noop = () => undefined;
/**
* The set of options that defines how API call should be made and result be
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/elasticsearch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

export { ElasticsearchServiceSetup, ElasticsearchService } from './elasticsearch_service';
export { CallAPIOptions, ClusterClient, FakeRequest, LegacyRequest } from './cluster_client';
export { CallAPIOptions, ClusterClient, FakeRequest } from './cluster_client';
export { ScopedClusterClient, Headers, APICaller } from './scoped_cluster_client';
export { ElasticsearchClientConfig } from './elasticsearch_client_config';
export { config } from './elasticsearch_config';
Expand Down
12 changes: 7 additions & 5 deletions src/core/server/http/auth_headers_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Request } from 'hapi';
import { KibanaRequest, ensureRawRequest } from './router';
import { KibanaRequest, ensureRawRequest, LegacyRequest } from './router';
import { AuthHeaders } from './lifecycle/auth';

/**
* Get headers to authenticate a user against Elasticsearch.
* @param request {@link KibanaRequest} - an incoming request.
* @return authentication headers {@link AuthHeaders} for - an incoming request.
* @public
* */
export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined;
export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined;

/** @internal */
export class AuthHeadersStorage {
private authHeadersCache = new WeakMap<Request, AuthHeaders>();
public set = (request: KibanaRequest | Request, headers: AuthHeaders) => {
private authHeadersCache = new WeakMap<LegacyRequest, AuthHeaders>();
public set = (request: KibanaRequest | LegacyRequest, headers: AuthHeaders) => {
this.authHeadersCache.set(ensureRawRequest(request), headers);
};
public get: GetAuthHeaders = request => {
Expand Down
41 changes: 35 additions & 6 deletions src/core/server/http/auth_state_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,51 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Request } from 'hapi';
import { KibanaRequest, ensureRawRequest } from './router';
import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router';

/**
* Status indicating an outcome of the authentication.
* @public
*/
export enum AuthStatus {
/**
* `auth` interceptor successfully authenticated a user
*/
authenticated = 'authenticated',
/**
* `auth` interceptor failed user authentication
*/
unauthenticated = 'unauthenticated',
/**
* `auth` interceptor has not been registered
*/
unknown = 'unknown',
}

/**
* Get authentication state for a request. Returned by `auth` interceptor.
* @param request {@link KibanaRequest} - an incoming request.
* @public
*/
export type GetAuthState = (
request: KibanaRequest | LegacyRequest
) => { status: AuthStatus; state: unknown };

/**
* Return authentication status for a request.
* @param request {@link KibanaRequest} - an incoming request.
* @public
*/
export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean;

/** @internal */
export class AuthStateStorage {
private readonly storage = new WeakMap<Request, unknown>();
private readonly storage = new WeakMap<LegacyRequest, unknown>();
constructor(private readonly canBeAuthenticated: () => boolean) {}
public set = (request: KibanaRequest | Request, state: unknown) => {
public set = (request: KibanaRequest | LegacyRequest, state: unknown) => {
this.storage.set(ensureRawRequest(request), state);
};
public get = (request: KibanaRequest | Request) => {
public get: GetAuthState = request => {
const key = ensureRawRequest(request);
const state = this.storage.get(key);
const status: AuthStatus = this.storage.has(key)
Expand All @@ -42,7 +71,7 @@ export class AuthStateStorage {

return { status, state };
};
public isAuthenticated = (request: KibanaRequest | Request) => {
public isAuthenticated: IsAuthenticated = request => {
return this.get(request).status === AuthStatus.authenticated;
};
}
9 changes: 4 additions & 5 deletions src/core/server/http/base_path_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Request } from 'hapi';
import { KibanaRequest, ensureRawRequest } from './router';
import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router';

import { modifyUrl } from '../../utils';

export class BasePath {
private readonly basePathCache = new WeakMap<Request, string>();
private readonly basePathCache = new WeakMap<LegacyRequest, string>();

constructor(private readonly serverBasePath?: string) {}

public get = (request: KibanaRequest | Request) => {
public get = (request: KibanaRequest | LegacyRequest) => {
const requestScopePath = this.basePathCache.get(ensureRawRequest(request)) || '';
const serverBasePath = this.serverBasePath || '';
return `${serverBasePath}${requestScopePath}`;
};

// should work only for KibanaRequest as soon as spaces migrate to NP
public set = (request: KibanaRequest | Request, requestSpecificBasePath: string) => {
public set = (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => {
const rawRequest = ensureRawRequest(request);

if (this.basePathCache.has(rawRequest)) {
Expand Down
16 changes: 16 additions & 0 deletions src/core/server/http/cookie_session_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,26 @@ import { KibanaRequest, ensureRawRequest } from './router';
import { SessionStorageFactory, SessionStorage } from './session_storage';
import { Logger } from '..';

/**
* Configuration used to create HTTP session storage based on top of cookie mechanism.
* @public
*/
export interface SessionStorageCookieOptions<T> {
/**
* Name of the session cookie.
*/
name: string;
/**
* A key used to encrypt a cookie value. Should be at least 32 characters long.
*/
encryptionKey: string;
/**
* Function called to validate a cookie content.
*/
validate: (sessionValue: T) => boolean | Promise<boolean>;
/**
* Flag indicating whether the cookie should be sent only via a secure connection.
*/
isSecure: boolean;
}

Expand Down
Loading

0 comments on commit 1728522

Please sign in to comment.