Skip to content
This repository has been archived by the owner on May 8, 2020. It is now read-only.

Commit

Permalink
feat(bootstrap): Refactor the bootstrap into a function in the core t…
Browse files Browse the repository at this point in the history
…o reduce implementation boilerplate.

Implement BeforeAll and AfterAll middleware decorators
  • Loading branch information
zakhenry committed Jun 16, 2016
1 parent 133cc7b commit 20f71a5
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 58 deletions.
6 changes: 4 additions & 2 deletions docs/guide/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ If a middleware has simple requirements like basic request or response manipulat
Here is an example of a basic middleware that defines a header to copy from the request to the response:
```typescript
function forwardHeader(headerName: string): IsolatedMiddlewareFactory {
return () => (request: Request, response: Response): Response => {
//use a named function here so the call stack can easily be debugged to show the called middleware
return () => function forwardHeader(request: Request, response: Response): Response {
response.header(headerName, request.headers().get(headerName));
return response;
}
Expand Down Expand Up @@ -80,7 +81,8 @@ export class AuthorizationMiddleware implements InjectableMiddleware {

public middlewareFactory(claim:string): Middleware {

return (request: Request, response: Response): Response => {
//use a named function here so the call stack can easily be debugged to show the called middleware
return function authorize(request: Request, response: Response): Response {
if (!this.auth.check(request, claim)) {
throw new UnauthorizedException('Forbidden');
}
Expand Down
54 changes: 35 additions & 19 deletions src/server/controllers/abstract.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { InjectableMiddlewareFactory, MiddlewareFactory } from '../middleware/in
import { PromiseFactory } from '../../common/util/serialPromise';
import { Response } from './response';
import { Request } from './request';
import { initializeMiddlewareRegister } from '../middleware/middleware.decorator';

export type ActionType = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

Expand All @@ -31,7 +32,10 @@ export type MiddlewareLocation = 'before' | 'after';
export abstract class AbstractController {

protected actionMethods: Map<string, MethodDefinition>;
protected registeredMiddleware: Map<string, MiddlewareRegistry>;
public registeredMiddleware: {
methods: Map<string, MiddlewareRegistry>
all: MiddlewareRegistry
};

protected routeBase: string;
protected logger: Logger;
Expand Down Expand Up @@ -73,23 +77,28 @@ export abstract class AbstractController {
this.actionMethods.set(methodSignature, methodDefinition);
}

public registerMiddleware(methodSignature: string, location: MiddlewareLocation, middlewareFactories: InjectableMiddlewareFactory[]): void {
if (!this.registeredMiddleware) {
this.registeredMiddleware = new Map<string, MiddlewareRegistry>();
}
public registerMiddleware(location: MiddlewareLocation, middlewareFactories: InjectableMiddlewareFactory[], methodSignature?: string): this {

initializeMiddlewareRegister(this);

let current: MiddlewareRegistry = this.registeredMiddleware.get(methodSignature);
if (methodSignature){
let current: MiddlewareRegistry = this.registeredMiddleware.methods.get(methodSignature);

if (!current) {
current = {
before: [],
after: []
};
if (!current) {
current = {
before: [],
after: []
};

this.registeredMiddleware.set(methodSignature, current);
this.registeredMiddleware.methods.set(methodSignature, current);
}

current[location].push(...middlewareFactories);
} else { // not method signature, apply to all
this.registeredMiddleware.all[location].push(...middlewareFactories);
}

current[location].push(...middlewareFactories);
return this;

}

Expand All @@ -101,20 +110,27 @@ export abstract class AbstractController {

this.actionMethods.forEach((methodDefinition: MethodDefinition, methodSignature: string) => {

const middlewareFactories = this.registeredMiddleware && this.registeredMiddleware.get(methodSignature);

let callStack: PromiseFactory<Response>[] = [];

callStack.push(this[methodSignature]);
if (this.registeredMiddleware){
const methodMiddlewareFactories = this.registeredMiddleware.methods.get(methodSignature);
//wrap method registered factories with the class defined ones
methodMiddlewareFactories.before.unshift(...this.registeredMiddleware.all.before);
methodMiddlewareFactories.after.push(...this.registeredMiddleware.all.after);

callStack.push(this[methodSignature]);

if (middlewareFactories) {
callStack.unshift(...middlewareFactories.before.map((middleware: MiddlewareFactory) => middleware(this.injector)));
callStack.push(...middlewareFactories.after.map((middleware: MiddlewareFactory) => middleware(this.injector)));
if (methodMiddlewareFactories) {
callStack.unshift(...methodMiddlewareFactories.before.map((middleware: MiddlewareFactory) => middleware(this.injector)));
callStack.push(...methodMiddlewareFactories.after.map((middleware: MiddlewareFactory) => middleware(this.injector)));
}
}


this.server.register({
method: methodDefinition.method,
path: `/api/${this.routeBase}${methodDefinition.route}`,
callStack: callStack,
callStackHandler: (request: Request, response: Response): Promise<Response> => {
return callStack.reduce((current: Promise<Response>, next: PromiseFactory<Response>): Promise<Response> => {

Expand Down
69 changes: 67 additions & 2 deletions src/server/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import 'core-js';
import 'reflect-metadata';
import { ReflectiveInjector, provide } from '@angular/core';
import {
ReflectiveInjector,
Provider,
Type,
provide,
ResolvedReflectiveProvider
} from '@angular/core';
import { Server } from './servers/abstract.server';
import { HapiServer } from './servers/hapi.server';
import { AbstractController } from './controllers/abstract.controller';
Expand All @@ -9,7 +15,7 @@ import { RemoteCli } from './services/remoteCli.service';
import { Logger } from '../common/services/logger.service';
import { ConsoleLogger } from '../common/services/consoleLogger.service';
import { DebugLogMiddleware } from './middleware/debugLog.middleware';

export {provide} from '@angular/core';
/**
* The core injector is exported so implementations can pick up already registered injectables
* without having to register them themselves.
Expand All @@ -24,3 +30,62 @@ export const coreInjector = ReflectiveInjector.resolveAndCreate([
provide(Logger, {useClass: ConsoleLogger}),
]);

export type ProviderDefinition = Type | Provider | {
[k: string]: any;
} | any[];

export interface BootstrapResponse {
injector: ReflectiveInjector;
server: Server;
logger: Logger;
}

export interface ControllerDictionary<T extends AbstractController> {
[key:string]: T;
}

export function bootstrap(controllers: ControllerDictionary<any>, providers: ProviderDefinition[] = []): BootstrapResponse {

let logger: Logger;
try {

let controllerArray = Object.keys(controllers).map(key => controllers[key]);

// resolve all controllers
let resolvedControllerProviders = ReflectiveInjector.resolve(controllerArray);

// resolve all other user classes
const resolvedProviders:ResolvedReflectiveProvider[] = ReflectiveInjector.resolve(providers)
.concat(resolvedControllerProviders);

// get an injector from the resolutions, using the core injector as parent
const injector = ReflectiveInjector.fromResolvedProviders(resolvedProviders, coreInjector);

// assign logger instance as soon as possible so the error handler might use it
logger = injector.get(Logger).source('bootstrap');

// iterate over the controller providers, instantiating them to register their routes
resolvedControllerProviders.forEach((resolvedControllerProvider: ResolvedReflectiveProvider) => {
logger.info(`initializing ${resolvedControllerProvider.key.displayName}`);
injector.instantiateResolved(resolvedControllerProvider)
.registerInjector(injector)
.registerRoutes();
});

// get vars for the bootstrapper
const server: Server = injector.get(Server);

return {injector, server, logger};

} catch (e) {
if (logger){
logger.critical(e);
} else {
console.error('Failed to initialize Logger, falling back to console');
console.error(e);
}
process.exit(1);
}

}

2 changes: 1 addition & 1 deletion src/server/middleware/debugLog.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class DebugLogMiddleware implements InjectableMiddleware {

public middlewareFactory(messages: string[]): Middleware {

return (request: Request, response: Response): Response => {
return function debugLog(request: Request, response: Response): Response {
this.logger.debug(...messages);
return response;
}
Expand Down
55 changes: 47 additions & 8 deletions src/server/middleware/middleware.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
import { AbstractController } from '../controllers/abstract.controller';
import { InjectableMiddlewareFactory } from './index';
import { AbstractController, MiddlewareRegistry } from '../controllers/abstract.controller';
import { MiddlewareFactory } from './index';
import { initializeRelationMap } from '../../common/relations/index';

/**
* Decorator for assigning middleware method in a controller
* Decorator for assigning before middleware method in a controller
* @returns {function(any, string, TypedPropertyDescriptor<T>): undefined}
* @constructor
* @param middlewareFactories
*/
export function Before<T>(...middlewareFactories:InjectableMiddlewareFactory[]): MethodDecorator {
export function Before<T>(...middlewareFactories: MiddlewareFactory[]): MethodDecorator {

return function (target: AbstractController, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {

target.registerMiddleware(propertyKey, 'before', middlewareFactories);
target.registerMiddleware('before', middlewareFactories, propertyKey);
};
}

/**
* Decorator for assigning middleware method in a controller
* Decorator for assigning after middleware method in a controller
* @returns {function(any, string, TypedPropertyDescriptor<T>): undefined}
* @constructor
* @param middlewareFactories
*/
export function After<T>(...middlewareFactories:InjectableMiddlewareFactory[]): MethodDecorator {
export function After<T>(...middlewareFactories: MiddlewareFactory[]): MethodDecorator {

return function (target: AbstractController, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {

target.registerMiddleware(propertyKey, 'after', middlewareFactories);
target.registerMiddleware('after', middlewareFactories, propertyKey);
};
}

export function initializeMiddlewareRegister(target: AbstractController): void {
if (!target.registeredMiddleware) {
target.registeredMiddleware = {
methods: new Map<string, MiddlewareRegistry>(),
all: {
before: [],
after: []
}
};
}
}

/**
* Decorator for assigning before middleware to all methods in a controller
* @param middlewareFactories
* @returns {function(AbstractController): void}
* @constructor
*/
export function BeforeAll(...middlewareFactories: MiddlewareFactory[]): ClassDecorator {
return function (target: Function): void {
initializeMiddlewareRegister(target.prototype);
target.prototype.registeredMiddleware.all.before.push(...middlewareFactories);
}
}

/**
* Decorator for assigning after middleware to all methods in a controller
* @param middlewareFactories
* @returns {function(AbstractController): void}
* @constructor
*/
export function AfterAll(...middlewareFactories: MiddlewareFactory[]): ClassDecorator {
return function (target: Function): void {
initializeMiddlewareRegister(target.prototype);
target.prototype.registeredMiddleware.all.after.push(...middlewareFactories);
}
}
26 changes: 16 additions & 10 deletions src/server/servers/abstract.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,29 @@ import { Logger } from '../../common/services/logger.service';
import { Server as Hapi } from 'hapi';
import { Response } from '../controllers/response';
import { Request } from '../controllers/request';
import { PromiseFactory } from '../../common/util/serialPromise';

export type HttpMethod = 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE';

export interface RouteInfo{
method:HttpMethod;
path:string;
}

export interface RouteConfig {
path: string;
method: HttpMethod;
callStack: PromiseFactory<Response>[];
callStackHandler: (request: Request, response: Response) => Promise<Response>;
}

@Injectable()
export abstract class Server {

public configuredRoutes: RouteConfig[] = [];
/**
* Logger instance for the class, initialized with `server` source
*/
protected logger: Logger;
protected logger: Logger;
/**
* The implementation of the underlying engine, could be hapi, koa, express etc
*/
protected engine: Hapi|any;
protected engine: Hapi|any;

constructor(loggerBase: Logger, remoteCli: RemoteCli) {

Expand All @@ -43,7 +41,13 @@ export abstract class Server {
* Registration function for routes
* @param config
*/
public abstract register(config: RouteConfig): void;
public register(config: RouteConfig): this {

this.configuredRoutes.push(config);
return this.registerRouteWithEngine(config);
};

protected abstract registerRouteWithEngine(config: RouteConfig): this;

/**
* Initialization function, called before start is called
Expand All @@ -62,7 +66,9 @@ export abstract class Server {
public getEngine(): any {
return this.engine;
}

public abstract getRoutes():RouteInfo[];

public getRoutes(): RouteConfig[] {
return this.configuredRoutes;
}

}
Loading

0 comments on commit 20f71a5

Please sign in to comment.