feature(@nestjs) enhance execution context, create host

This commit is contained in:
Kamil Myśliwiec
2018-03-24 17:19:47 +01:00
parent 0eb3354344
commit fdbf364087
34 changed files with 283 additions and 94 deletions

View File

@@ -84,12 +84,12 @@ gulp.task('move', function() {
const getDirs = (base) => getFolders(base)
.map((path) => `${base}/${path}`);
const examplesDirs = getDirs('examples');
const examplesDirs = getDirs('sample');
const integrationDirs = getDirs('integration');
const directories = examplesDirs.concat(integrationDirs);
let stream = gulp
.packages(['node_modules/@nestjs/**/*']);
.src(['node_modules/@nestjs/**/*']);
directories.forEach((dir) => {
stream = stream.pipe(gulp.dest(dir + '/node_modules/@nestjs'));

View File

@@ -8,23 +8,23 @@ import { ApplicationModule } from './../src/app.module';
import { FastifyAdapter } from '@nestjs/core/adapters/fastify-adapter';
import { ExpressAdapter } from '@nestjs/core/adapters/express-adapter';
import { HelloService } from '../src/hello/hello.service';
import { INestFastifyApplication } from '@nestjs/common/interfaces/nest-fastify-application.interface';
describe('Hello world (fastify adapter)', () => {
let server;
let app: INestApplication;
let app: INestApplication & INestFastifyApplication
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [ApplicationModule],
}).compile();
server = fastify();
app = module.createNestApplication(new FastifyAdapter(server));
app = module.createNestApplication(new FastifyAdapter());
await app.init();
});
it(`/GET`, () => {
return server
return app
.inject({
method: 'GET',
url: '/hello',
@@ -33,7 +33,7 @@ describe('Hello world (fastify adapter)', () => {
});
it(`/GET (Promise/async)`, () => {
return server
return app
.inject({
method: 'GET',
url: '/hello/async',
@@ -42,7 +42,7 @@ describe('Hello world (fastify adapter)', () => {
});
it(`/GET (Observable stream)`, () => {
return server
return app
.inject({
method: 'GET',
url: '/hello/stream',

View File

@@ -10,14 +10,14 @@ const RETURN_VALUE = 'test';
@Injectable()
export class OverrideInterceptor {
intercept(data, context, stream) {
intercept(context, stream) {
return of(RETURN_VALUE);
}
}
@Injectable()
export class TransformInterceptor {
intercept(data, context, stream) {
intercept(context, stream) {
return stream.pipe(map(data => ({ data })));
}
}

View File

@@ -30,6 +30,9 @@ export {
INestApplicationContext,
HttpServer,
HttpServerFactory,
ArgumentsHost,
INestExpressApplication,
INestFastifyApplication,
} from './interfaces';
export * from './interceptors';
export * from './services/logger.service';

View File

@@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable';
import { MulterOptions } from '../interfaces/external/multer-options.interface';
import { mixin } from '../decorators/core/component.decorator';
import { transformException } from './multer/multer.utils';
import { ExecutionContext } from './../interfaces';
export function FileInterceptor(fieldName: string, options?: MulterOptions) {
return mixin(
@@ -11,18 +12,23 @@ export function FileInterceptor(fieldName: string, options?: MulterOptions) {
readonly upload = multer(options);
async intercept(
request,
context,
context: ExecutionContext,
stream$: Observable<any>,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
await new Promise((resolve, reject) =>
this.upload.single(fieldName)(request, request.res, err => {
if (err) {
const error = transformException(err);
return reject(error);
}
resolve();
}),
this.upload.single(fieldName)(
ctx.getRequest(),
ctx.getResponse(),
err => {
if (err) {
const error = transformException(err);
return reject(error);
}
resolve();
},
),
);
return stream$;
}

View File

@@ -3,6 +3,7 @@ import { NestInterceptor } from './../interfaces/features/nest-interceptor.inter
import { Observable } from 'rxjs/Observable';
import { MulterOptions } from '../interfaces/external/multer-options.interface';
import { transformException } from './multer/multer.utils';
import { ExecutionContext } from './../interfaces';
export function FilesInterceptor(
fieldName: string,
@@ -13,18 +14,23 @@ export function FilesInterceptor(
readonly upload = multer(options);
async intercept(
request,
context,
context: ExecutionContext,
stream$: Observable<any>,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
await new Promise((resolve, reject) =>
this.upload.array(fieldName, maxCount)(request, request.res, err => {
if (err) {
const error = transformException(err);
return reject(error);
}
resolve();
}),
this.upload.array(fieldName, maxCount)(
ctx.getRequest(),
ctx.getResponse(),
err => {
if (err) {
const error = transformException(err);
return reject(error);
}
resolve();
},
),
);
return stream$;
}

View File

@@ -1,3 +1,5 @@
export interface ExceptionFilter<T = any, R = any> {
catch(exception: T, response: R);
import { ArgumentsHost } from './../features/arguments-host.interface';
export interface ExceptionFilter<T = any> {
catch(exception: T, host: ArgumentsHost);
}

View File

@@ -1,5 +1,6 @@
import { Observable } from 'rxjs/Observable';
import { ArgumentsHost } from './../features/arguments-host.interface';
export interface RpcExceptionFilter<T = any, R = any> {
catch(exception: T): Observable<R>;
catch(exception: T, host: ArgumentsHost): Observable<R>;
}

View File

@@ -1,3 +1,6 @@
export interface WsExceptionFilter<T = any, R = any> {
catch(exception: T, client: R);
import { ArgumentsHost } from './../features/arguments-host.interface';
export interface WsExceptionFilter<T = any> {
catch(exception: T, host: ArgumentsHost);
}

View File

@@ -0,0 +1,22 @@
export interface HttpArgumentsHost {
getRequest<T = any>(): T;
getResponse<T = any>(): T;
}
export interface WsArgumentsHost {
getData<T = any>(): T;
getClient<T = any>(): T;
}
export interface RpcArgumentsHost {
getData<T = any>(): T;
}
export interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
}

View File

@@ -3,7 +3,6 @@ import { ExecutionContext } from './execution-context.interface';
export interface CanActivate {
canActivate(
request,
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean>;
}

View File

@@ -1,8 +1,7 @@
import { Type } from './../index';
import { ArgumentsHost } from './arguments-host.interface';
export interface ExecutionContext {
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
}

View File

@@ -3,7 +3,6 @@ import { ExecutionContext } from './execution-context.interface';
export interface NestInterceptor<T = any, R = any> {
intercept(
dataOrRequest,
context: ExecutionContext,
stream$: Observable<T>,
): Observable<R> | Promise<Observable<R>>;

View File

@@ -24,4 +24,7 @@ export * from './features/nest-interceptor.interface';
export * from './features/custom-route-param-factory.interface';
export * from './modules/dynamic-module.interface';
export * from './http/http-server.interface';
export * from './http/http-server-factory.interface';
export * from './http/http-server-factory.interface';
export * from './features/arguments-host.interface';
export * from './nest-express-application.interface';
export * from './nest-fastify-application.interface';

View File

@@ -14,10 +14,10 @@ export interface INestFastifyApplication {
* @returns this
*/
useStaticAssets(options: {
root: string,
prefix: string,
setHeaders: Function,
send: any,
root: string;
prefix: string;
setHeaders: Function;
send: any;
}): this;
/**
@@ -26,4 +26,52 @@ export interface INestFastifyApplication {
* @returns this
*/
setViewEngine(options: any): this;
}
/**
* A wrapper function around native `fastify.inject()` method.
* @returns void
*/
inject(opts: HTTPInjectOptions | string): Promise<HTTPInjectResponse>;
}
/** Reference: https://github.com/fastify/fastify */
export type HTTPMethod =
| 'DELETE'
| 'GET'
| 'HEAD'
| 'PATCH'
| 'POST'
| 'PUT'
| 'OPTIONS';
/**
* Fake http inject options
*/
export interface HTTPInjectOptions {
url: string;
method?: HTTPMethod;
authority?: string;
headers?: object;
remoteAddress?: string;
payload?: string | object | Buffer | any;
simulate?: {
end?: boolean;
split?: boolean;
error?: boolean;
close?: boolean;
};
validate?: boolean;
}
/**
* Fake http inject response
*/
export interface HTTPInjectResponse {
raw: any;
headers: object;
statusCode: number;
statusMessage: string;
payload: string;
rawPayload: Buffer;
trailers: object;
}

View File

@@ -12,4 +12,5 @@ export interface WebSocketAdapter<T = any> {
}[],
process: (data) => Observable<any>,
);
close(server);
}

View File

@@ -98,6 +98,10 @@ export class FastifyAdapter {
return this.instance.register(...args);
}
inject(...args) {
return this.instance.inject(...args);
}
close() {
return this.instance.close();
}

View File

@@ -4,6 +4,7 @@ import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions/ex
import { isEmpty, isObject } from '@nestjs/common/utils/shared.utils';
import { InvalidExceptionFilterException } from '../errors/exceptions/invalid-exception-filter.exception';
import { HttpException } from '@nestjs/common';
import { ArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface';
export class ExceptionsHandler {
private static readonly logger = new Logger(ExceptionsHandler.name);
@@ -11,8 +12,8 @@ export class ExceptionsHandler {
constructor(private readonly applicationRef: HttpServer) {}
public next(exception: Error | HttpException | any, response) {
if (this.invokeCustomFilters(exception, response)) return;
public next(exception: Error | HttpException | any, ctx: ArgumentsHost) {
if (this.invokeCustomFilters(exception, ctx)) return;
if (!(exception instanceof HttpException)) {
const body = {
@@ -20,7 +21,7 @@ export class ExceptionsHandler {
message: messages.UNKNOWN_EXCEPTION_MESSAGE,
};
const statusCode = 500;
this.applicationRef.reply(response, body, statusCode);
this.applicationRef.reply(ctx.getArgByIndex(1), body, statusCode);
if (this.isExceptionObject(exception)) {
return ExceptionsHandler.logger.error(
exception.message,
@@ -36,7 +37,11 @@ export class ExceptionsHandler {
statusCode: exception.getStatus(),
message: res,
};
this.applicationRef.reply(response, message, exception.getStatus());
this.applicationRef.reply(
ctx.getArgByIndex(1),
message,
exception.getStatus(),
);
}
public setCustomFilters(filters: ExceptionFilterMetadata[]) {

View File

@@ -7,23 +7,24 @@ import {
isEmpty,
} from '@nestjs/common/utils/shared.utils';
import { Controller } from '@nestjs/common/interfaces';
import { CanActivate, HttpStatus, ExecutionContext } from '@nestjs/common';
import { CanActivate, HttpStatus } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { FORBIDDEN_MESSAGE } from './constants';
import { ExecutionContextHost } from '../helpers/execution-context.host';
export class GuardsConsumer {
public async tryActivate(
guards: CanActivate[],
data,
args: any[],
instance: Controller,
callback: (...args) => any,
): Promise<boolean> {
if (!guards || isEmpty(guards)) {
return true;
}
const context = this.createContext(instance, callback);
const context = this.createContext(args, instance, callback);
for (const guard of guards) {
const result = guard.canActivate(data, context);
const result = guard.canActivate(context);
if (await this.pickResult(result)) {
continue;
}
@@ -33,13 +34,15 @@ export class GuardsConsumer {
}
public createContext(
args: any[],
instance: Controller,
callback: (...args) => any,
): ExecutionContext {
return {
parent: instance.constructor,
handler: callback,
};
): ExecutionContextHost {
return new ExecutionContextHost(
args,
instance.constructor as any,
callback,
);
}
public async pickResult(

View File

@@ -0,0 +1,51 @@
import { ExecutionContext } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces';
import {
RpcArgumentsHost,
WsArgumentsHost,
HttpArgumentsHost,
} from '@nestjs/common/interfaces/features/arguments-host.interface';
export class ExecutionContextHost implements ExecutionContext {
constructor(
private readonly args: any[],
private readonly constructorRef: Type<any> = null,
private readonly handler: Function = null,
) {}
getClass<T = any>(): Type<T> {
return this.constructorRef;
}
getHandler(): Function {
return this.handler;
}
getArgs<T extends Array<any> = any[]>(): T {
return this.args as T;
}
getArgByIndex<T = any>(index: number): T {
return this.args[index] as T;
}
switchToRpc(): RpcArgumentsHost {
return Object.assign(this, {
getData: () => this.getArgByIndex(0),
});
}
switchToHttp(): HttpArgumentsHost {
return Object.assign(this, {
getRequest: () => this.getArgByIndex(0),
getResponse: () => this.getArgByIndex(1),
});
}
switchToWs(): WsArgumentsHost {
return Object.assign(this, {
getClient: () => this.getArgByIndex(0),
getData: () => this.getArgByIndex(1),
});
}
}

View File

@@ -31,10 +31,9 @@ export class ExternalContextCreator {
module,
);
return async (...args) => {
const [req] = args;
const canActivate = await this.guardsConsumer.tryActivate(
guards,
req,
args,
instance,
callback,
);
@@ -44,7 +43,7 @@ export class ExternalContextCreator {
const handler = () => callback.apply(instance, args);
return await this.interceptorsConsumer.intercept(
interceptors,
req,
args,
instance,
callback,
handler,

View File

@@ -7,16 +7,17 @@ import {
isEmpty,
} from '@nestjs/common/utils/shared.utils';
import { Controller } from '@nestjs/common/interfaces';
import { HttpStatus, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { HttpStatus, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { defer } from 'rxjs/observable/defer';
import { fromPromise } from 'rxjs/observable/fromPromise';
import { take, switchMap } from 'rxjs/operators';
import { ExecutionContextHost } from '../helpers/execution-context.host';
export class InterceptorsConsumer {
public async intercept(
interceptors: NestInterceptor[],
dataOrRequest: any,
args: any[],
instance: Controller,
callback: (...args) => any,
next: () => Promise<any>,
@@ -24,24 +25,26 @@ export class InterceptorsConsumer {
if (!interceptors || isEmpty(interceptors)) {
return await await next();
}
const context = this.createContext(instance, callback);
const context = this.createContext(args, instance, callback);
const start$ = defer(() => this.transformDeffered(next));
const result$ = await interceptors.reduce(
async (stream$, interceptor) =>
await interceptor.intercept(dataOrRequest, context, await stream$),
await interceptor.intercept(context, await stream$),
Promise.resolve(start$),
);
return await result$.toPromise();
}
public createContext(
args: any[],
instance: Controller,
callback: (...args) => any,
): ExecutionContext {
return {
parent: instance.constructor,
handler: callback,
};
): ExecutionContextHost {
return new ExecutionContextHost(
args,
instance.constructor as any,
callback,
);
}
public transformDeffered(next: () => Promise<any>): Observable<any> {

View File

@@ -272,6 +272,11 @@ export class NestApplication extends NestApplicationContext
return this;
}
public inject(...args) {
const adapter = this.httpAdapter as FastifyAdapter;
return adapter.inject && adapter.inject(...args);
}
public enableCors(options?: CorsOptions): this {
this.httpAdapter.use(cors(options));
return this;

View File

@@ -94,7 +94,7 @@ export class RouterExecutionContext {
return async (req, res, next) => {
const args = this.createNullArray(argsLength);
fnCanActivate && (await fnCanActivate(req));
fnCanActivate && (await fnCanActivate([res, res]));
const handler = async () => {
fnApplyPipes && (await fnApplyPipes(args, req, res, next));
@@ -102,7 +102,7 @@ export class RouterExecutionContext {
};
const result = await this.interceptorsConsumer.intercept(
interceptors,
req,
[req, res],
instance,
callback,
handler,
@@ -213,10 +213,10 @@ export class RouterExecutionContext {
instance: Controller,
callback: (...args) => any,
) {
const canActivateFn = async req => {
const canActivateFn = async (args: any[]) => {
const canActivate = await this.guardsConsumer.tryActivate(
guards,
req,
args,
instance,
callback,
);

View File

@@ -1,4 +1,5 @@
import { ExceptionsHandler } from '../exceptions/exceptions-handler';
import { ExecutionContextHost } from '../helpers/execution-context.host';
export type RouterProxyCallback = (req?, res?, next?) => void;
@@ -8,12 +9,13 @@ export class RouterProxy {
exceptionsHandler: ExceptionsHandler,
) {
return (req, res, next) => {
const host = new ExecutionContextHost([req, res]);
try {
Promise.resolve(targetCallback(req, res, next)).catch(e => {
exceptionsHandler.next(e, res);
exceptionsHandler.next(e, host);
});
} catch (e) {
exceptionsHandler.next(e, res);
exceptionsHandler.next(e, host);
}
};
}
@@ -23,12 +25,13 @@ export class RouterProxy {
exceptionsHandler: ExceptionsHandler,
) {
return (err, req, res, next) => {
const host = new ExecutionContextHost([req, res]);
try {
Promise.resolve(targetCallback(err, req, res, next)).catch(e => {
exceptionsHandler.next(e, res);
exceptionsHandler.next(e, host);
});
} catch (e) {
exceptionsHandler.next(e, res);
exceptionsHandler.next(e, host);
}
};
}

View File

@@ -47,7 +47,7 @@ export class RpcContextCreator {
const [data, ...params] = args;
const canActivate = await this.guardsConsumer.tryActivate(
guards,
data,
args,
instance,
callback,
);
@@ -65,7 +65,7 @@ export class RpcContextCreator {
return await this.interceptorsConsumer.intercept(
interceptors,
data,
args,
instance,
callback,
handler,

View File

@@ -1,5 +1,6 @@
import { Observable } from 'rxjs/Observable';
import { RpcExceptionsHandler } from '../exceptions/rpc-exceptions-handler';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
export class RpcProxy {
public create(
@@ -7,10 +8,11 @@ export class RpcProxy {
exceptionsHandler: RpcExceptionsHandler,
): (...args) => Promise<Observable<any>> {
return async (...args) => {
const host = new ExecutionContextHost(args);
try {
return await targetCallback(...args);
} catch (e) {
return exceptionsHandler.handle(e);
return exceptionsHandler.handle(e, host);
}
};
}

View File

@@ -6,13 +6,17 @@ import { Observable } from 'rxjs/Observable';
import { RpcException } from './rpc-exception';
import { RpcExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions';
import { _throw } from 'rxjs/observable/throw';
import { ArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface';
export class RpcExceptionsHandler {
private static readonly logger = new Logger(RpcExceptionsHandler.name);
private filters: RpcExceptionFilterMetadata[] = [];
public handle(exception: Error | RpcException | any): Observable<any> {
const filterResult$ = this.invokeCustomFilters(exception);
public handle(
exception: Error | RpcException | any,
host: ArgumentsHost,
): Observable<any> {
const filterResult$ = this.invokeCustomFilters(exception, host);
if (filterResult$) {
return filterResult$;
}
@@ -41,7 +45,7 @@ export class RpcExceptionsHandler {
this.filters = filters;
}
public invokeCustomFilters(exception): Observable<any> | null {
public invokeCustomFilters(exception, host: ArgumentsHost): Observable<any> | null {
if (isEmpty(this.filters)) return null;
const filter = this.filters.find(({ exceptionMetatypes, func }) => {
@@ -52,6 +56,6 @@ export class RpcExceptionsHandler {
);
return hasMetatype;
});
return filter ? filter.func(exception) : null;
return filter ? filter.func(exception, host) : null;
}
}

View File

@@ -56,4 +56,8 @@ export class IoAdapter implements WebSocketAdapter {
public bindMiddleware(server, middleware: (socket, next) => void) {
server.use(middleware);
}
public close(server) {
isFunction(server.close) && server.close();
}
}

View File

@@ -67,4 +67,8 @@ export class WsAdapter implements WebSocketAdapter {
const { callback } = messageHandler;
return process(callback(message.data));
}
public close(server) {
isFunction(server.close) && server.close();
}
}

View File

@@ -46,7 +46,7 @@ export class WsContextCreator {
return this.wsProxy.create(async (client, data) => {
const canActivate = await this.guardsConsumer.tryActivate(
guards,
data,
[client, data],
instance,
callback,
);
@@ -63,7 +63,7 @@ export class WsContextCreator {
};
return await this.interceptorsConsumer.intercept(
interceptors,
data,
[client, data],
instance,
callback,
handler,

View File

@@ -1,4 +1,5 @@
import { WsExceptionsHandler } from './../exceptions/ws-exceptions-handler';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
export class WsProxy {
public create(
@@ -6,10 +7,11 @@ export class WsProxy {
exceptionsHandler: WsExceptionsHandler,
): (client, data) => Promise<void> {
return async (client, data) => {
const host = new ExecutionContextHost([client, data]);
try {
return await targetCallback(client, data);
} catch (e) {
exceptionsHandler.handle(e, client);
exceptionsHandler.handle(e, host);
}
};
}

View File

@@ -4,12 +4,14 @@ import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions/ex
import { isEmpty, isObject } from '@nestjs/common/utils/shared.utils';
import { InvalidExceptionFilterException } from '@nestjs/core/errors/exceptions/invalid-exception-filter.exception';
import { WsException } from '../exceptions/ws-exception';
import { ArgumentsHost } from '@nestjs/common';
export class WsExceptionsHandler {
private filters: ExceptionFilterMetadata[] = [];
public handle(exception: Error | WsException | any, client) {
if (this.invokeCustomFilters(exception, client) || !client.emit) return;
public handle(exception: Error | WsException | any, args: ArgumentsHost) {
const client = args.switchToWs().getClient();
if (this.invokeCustomFilters(exception, args) || !client.emit) return;
const status = 'error';
if (!(exception instanceof WsException)) {
@@ -33,7 +35,7 @@ export class WsExceptionsHandler {
this.filters = filters;
}
public invokeCustomFilters(exception, client): boolean {
public invokeCustomFilters(exception, args: ArgumentsHost): boolean {
if (isEmpty(this.filters)) return false;
const filter = this.filters.find(({ exceptionMetatypes, func }) => {
@@ -44,7 +46,7 @@ export class WsExceptionsHandler {
);
return hasMetatype;
});
filter && filter.func(exception, client);
filter && filter.func(exception, args);
return !!filter;
}
}

View File

@@ -21,10 +21,12 @@ import { InterceptorsContextCreator } from '@nestjs/core/interceptors/intercepto
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
export class SocketModule {
private socketsContainer = new SocketsContainer();
private readonly socketsContainer = new SocketsContainer();
private applicationConfig: ApplicationConfig;
private webSocketsController: WebSocketsController;
public register(container, config) {
this.applicationConfig = config;
this.webSocketsController = new WebSocketsController(
new SocketServerProvider(this.socketsContainer, config),
container,
@@ -65,9 +67,13 @@ export class SocketModule {
);
}
public close() {
public async close() {
if (!this.applicationConfig) {
return void 0;
}
const adapter = this.applicationConfig.getIoAdapter();
const servers = this.socketsContainer.getAllServers();
servers.forEach(({ server }) => server && server.close && server.close());
servers.forEach(({ server }) => server && adapter.close(server));
this.socketsContainer.clear();
}