Skip to content

Commit

Permalink
refactor(rest): introduce RequestContext
Browse files Browse the repository at this point in the history
Rework all places where we were accepting a pair of
`(ParsedRequest, ServerResponse)` to use an context object instead.

Two context types are introduced:

`HandlerContext` is an interface describing objects required to
handle an incoming HTTP request. There are two properties for starter:
  - context.request
  - context.response

Low-level entities like Sequence Actions should be using this
interface to allow easy interoperability with potentially any
HTTP framework and more importantly, to keep the code easy to test.

`RequestContext` is a class extending our IoC `Context` and
implementing `HandlerContext` interface. This object is used
when invoking the sequence handler.

By combining both IoC and HTTP contexts into a single object,
there will be (hopefully) less confusion among LB4 users about
what shape a "context" object has in different places.

This is a breaking change affecting the following users:

- Custom sequence classes
- Custom sequence handlers registered via `app.handler`
- Custom implementations of the built-in sequence action `Reject`
  • Loading branch information
bajtos committed May 10, 2018
1 parent 4411c46 commit c4b0f71
Show file tree
Hide file tree
Showing 20 changed files with 179 additions and 136 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ packages/*/api-docs
packages/cli/generators/*/templates
packages/*/dist*
examples/*/dist*
sandbox/**/dist*
*.json
*.md
24 changes: 12 additions & 12 deletions docs/site/Sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ instances handle requests and responses. The `DefaultSequence` looks like this:

```ts
class DefaultSequence {
async handle(request: ParsedRequest, response: ServerResponse) {
async handle(context: RequestContext) {
try {
const route = this.findRoute(request);
const params = await this.parseParams(request, route);
const route = this.findRoute(context.request);
const params = await this.parseParams(context.request, route);
const result = await this.invoke(route, params);
await this.send(response, result);
} catch (err) {
await this.reject(response, err);
await this.send(context.response, result);
} catch (error) {
await this.reject(context, error);
}
}
}
Expand Down Expand Up @@ -61,15 +61,15 @@ Actions:

```ts
class MySequence extends DefaultSequence {
async handle(request: ParsedRequest, response: ServerResponse) {
async handle(context: RequestContext) {
// findRoute() produces an element
const route = this.findRoute(request);
const route = this.findRoute(context.request);
// parseParams() uses the route element and produces the params element
const params = await this.parseParams(request, route);
const params = await this.parseParams(context.request, route);
// invoke() uses both the route and params elements to produce the result (OperationRetVal) element
const result = await this.invoke(route, params);
// send() uses the result element
await this.send(response, result);
await this.send(context.response, result);
}
}
```
Expand All @@ -91,9 +91,9 @@ class MySequence extends DefaultSequence {
log(msg: string) {
console.log(msg);
}
async handle(request: ParsedRequest, response: ServerResponse) {
async handle(context: RequestContext) {
this.log('before request');
await super.handle(request, response);
await super.handle(context);
this.log('after request');
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/hello-world/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class HelloWorldApplication extends RestApplication {
// returns the same HTTP response: Hello World!
// Learn more about the concept of Sequence in our docs:
// http://loopback.io/doc/en/lb4/Sequence.html
this.handler((sequence, request, response) => {
this.handler(({response}, sequence) => {
sequence.send(response, 'Hello World!');
});
}
Expand Down
24 changes: 12 additions & 12 deletions examples/log-extension/test/acceptance/log.extension.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
InvokeMethod,
Send,
Reject,
ParsedRequest,
ServerResponse,
RequestContext,
} from '@loopback/rest';
import {get, param} from '@loopback/openapi-v3';
import {
Expand All @@ -31,7 +30,7 @@ import {
createClientForHandler,
expect,
} from '@loopback/testlab';
import {Context, inject} from '@loopback/context';
import {inject} from '@loopback/context';
import chalk from 'chalk';

const SequenceActions = RestBindings.SequenceActions;
Expand Down Expand Up @@ -204,7 +203,6 @@ describe('log extension acceptance test', () => {
function createSequence() {
class LogSequence implements SequenceHandler {
constructor(
@inject(RestBindings.Http.CONTEXT) public ctx: Context,
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS)
protected parseParams: ParseParams,
Expand All @@ -214,23 +212,25 @@ describe('log extension acceptance test', () => {
@inject(EXAMPLE_LOG_BINDINGS.LOG_ACTION) protected logger: LogFn,
) {}

async handle(req: ParsedRequest, res: ServerResponse) {
async handle(context: RequestContext): Promise<void> {
const {request, response} = context;

// tslint:disable-next-line:no-any
let args: any = [];
// tslint:disable-next-line:no-any
let result: any;

try {
const route = this.findRoute(req);
args = await this.parseParams(req, route);
const route = this.findRoute(request);
args = await this.parseParams(request, route);
result = await this.invoke(route, args);
this.send(res, result);
} catch (err) {
this.reject(res, req, err);
result = err;
this.send(response, result);
} catch (error) {
this.reject(context, error);
result = error;
}

await this.logger(req, args, result);
await this.logger(request, args, result);
}
}

Expand Down
16 changes: 8 additions & 8 deletions examples/todo/src/sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import {Context, inject} from '@loopback/context';
import {
FindRoute,
InvokeMethod,
ParsedRequest,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
} from '@loopback/rest';
import {ServerResponse} from 'http';

const SequenceActions = RestBindings.SequenceActions;

Expand All @@ -28,14 +27,15 @@ export class MySequence implements SequenceHandler {
@inject(SequenceActions.REJECT) public reject: Reject,
) {}

async handle(req: ParsedRequest, res: ServerResponse) {
async handle(context: RequestContext) {
try {
const route = this.findRoute(req);
const args = await this.parseParams(req, route);
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(res, result);
} catch (err) {
this.reject(res, req, err);
this.send(response, result);
} catch (error) {
this.reject(context, error);
}
}
}
18 changes: 9 additions & 9 deletions packages/authentication/test/acceptance/basic-auth.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import {Application} from '@loopback/core';
import {
RestBindings,
ServerResponse,
ParsedRequest,
ParseParams,
FindRoute,
InvokeMethod,
Expand All @@ -16,6 +14,7 @@ import {
SequenceHandler,
RestServer,
RestComponent,
RequestContext,
} from '@loopback/rest';
import {api, get} from '@loopback/openapi-v3';
import {Client, createClientForHandler} from '@loopback/testlab';
Expand Down Expand Up @@ -137,19 +136,20 @@ describe('Basic Authentication', () => {
protected authenticateRequest: AuthenticateFn,
) {}

async handle(req: ParsedRequest, res: ServerResponse) {
async handle(context: RequestContext) {
try {
const route = this.findRoute(req);
const {request, response} = context;
const route = this.findRoute(request);

// Authenticate
await this.authenticateRequest(req);
await this.authenticateRequest(request);

// Authentication successful, proceed to invoke controller
const args = await this.parseParams(req, route);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(res, result);
} catch (err) {
this.reject(res, req, err);
this.send(response, result);
} catch (error) {
this.reject(context, error);
return;
}
}
Expand Down
15 changes: 7 additions & 8 deletions packages/cli/generators/app/templates/src/sequence.ts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,34 @@ import {Context, inject} from '@loopback/context';
import {
FindRoute,
InvokeMethod,
ParsedRequest,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
} from '@loopback/rest';
import {ServerResponse} from 'http';

const SequenceActions = RestBindings.SequenceActions;

export class MySequence implements SequenceHandler {
constructor(
@inject(RestBindings.Http.CONTEXT) public ctx: Context,
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
) {}

async handle(req: ParsedRequest, res: ServerResponse) {
async handle(context: RequestContext) {
try {
const route = this.findRoute(req);
const args = await this.parseParams(req, route);
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(res, result);
this.send(response, result);
} catch (err) {
this.reject(res, req, err);
this.reject(context, err);
}
}
}
2 changes: 1 addition & 1 deletion packages/rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Here's a basic "Hello World" application using `@loopback/rest`:
import {RestApplication, RestServer} from '@loopback/rest';

const app = new RestApplication();
app.handler((sequence, request, response) => {
app.handler(({request, response}, sequence) => {
sequence.send(response, 'hello world');
});

Expand Down
20 changes: 7 additions & 13 deletions packages/rest/src/http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {ParsedRequest} from './types';

import {RestBindings} from './keys';
import {RequestContext} from './request-context';

export class HttpHandler {
protected _routes: RoutingTable = new RoutingTable();
Expand Down Expand Up @@ -67,22 +68,15 @@ export class HttpHandler {
response: ServerResponse,
): Promise<void> {
const parsedRequest: ParsedRequest = parseRequestUrl(request);
const requestContext = this._createRequestContext(parsedRequest, response);
const requestContext = new RequestContext(
parsedRequest,
response,
this._rootContext,
);

const sequence = await requestContext.get<SequenceHandler>(
RestBindings.SEQUENCE,
);
await sequence.handle(parsedRequest, response);
}

protected _createRequestContext(
req: ParsedRequest,
res: ServerResponse,
): Context {
const requestContext = new Context(this._rootContext);
requestContext.bind(RestBindings.Http.REQUEST).to(req);
requestContext.bind(RestBindings.Http.RESPONSE).to(res);
requestContext.bind(RestBindings.Http.CONTEXT).to(requestContext);
return requestContext;
await sequence.handle(requestContext);
}
}
1 change: 1 addition & 0 deletions packages/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {writeResultToResponse} from './writer';
export {HttpErrors};

export * from './http-handler';
export * from './request-context';
export * from './types';
export * from './keys';
export * from './rest.application';
Expand Down
7 changes: 3 additions & 4 deletions packages/rest/src/providers/reject.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {LogError, Reject} from '../types';
import {LogError, Reject, HandlerContext} from '../types';
import {inject, Provider} from '@loopback/context';
import {ServerResponse, ServerRequest} from 'http';
import {HttpError} from 'http-errors';
import {writeErrorToResponse} from '../writer';
import {RestBindings} from '../keys';
Expand All @@ -17,10 +16,10 @@ export class RejectProvider implements Provider<Reject> {
) {}

value(): Reject {
return (response, request, error) => this.action(response, request, error);
return (context, error) => this.action(context, error);
}

action(response: ServerResponse, request: ServerRequest, error: Error) {
action({request, response}: HandlerContext, error: Error) {
const err = <HttpError>error;
const statusCode = err.statusCode || err.status || 500;
writeErrorToResponse(response, err);
Expand Down
39 changes: 39 additions & 0 deletions packages/rest/src/request-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Context} from '@loopback/context';
import {ServerResponse} from 'http';
import {HandlerContext, ParsedRequest} from './types';
import {RestBindings} from './keys';

/**
* A per-request Context combining an IoC container with handler context
* (request, response, etc.).
*/
export class RequestContext extends Context implements HandlerContext {
constructor(
public readonly request: ParsedRequest,
public readonly response: ServerResponse,
parent: Context,
name?: string,
) {
super(parent, name);
this._setupBindings(request, response);
}

private _setupBindings(request: ParsedRequest, response: ServerResponse) {
this.bind(RestBindings.Http.REQUEST)
.to(request)
.lock();

this.bind(RestBindings.Http.RESPONSE)
.to(response)
.lock();

this.bind(RestBindings.Http.CONTEXT)
.to(this)
.lock();
}
}
Loading

0 comments on commit c4b0f71

Please sign in to comment.