chore() merge latest changes (resolve conflicts)

This commit is contained in:
Kamil Myśliwiec
2018-12-10 22:57:45 +01:00
84 changed files with 705 additions and 328 deletions

View File

@@ -73,6 +73,15 @@ describe('NATS transport', () => {
.expect(200, 'true');
});
it(`/GET (exception)`, () => {
return request(server)
.get('/exception')
.expect(200, {
message: 'test',
status: 'error',
});
});
afterEach(async () => {
await app.close();
});

View File

@@ -1,12 +1,13 @@
import { Body, Controller, HttpCode, Post, Query } from '@nestjs/common';
import { Body, Controller, Get, HttpCode, Post, Query } from '@nestjs/common';
import {
Client,
ClientProxy,
MessagePattern,
RpcException,
Transport,
} from '@nestjs/microservices';
import { from, Observable, of } from 'rxjs';
import { scan } from 'rxjs/operators';
import { from, Observable, of, throwError } from 'rxjs';
import { catchError, scan } from 'rxjs/operators';
@Controller()
export class NatsController {
@@ -71,4 +72,16 @@ export class NatsController {
streaming(data: number[]): Observable<number> {
return from(data);
}
@Get('exception')
async getError() {
return await this.client
.send<number>('exception', {})
.pipe(catchError(err => of(err)));
}
@MessagePattern('exception')
throwError(): Observable<number> {
return throwError(new RpcException('test'));
}
}

View File

@@ -0,0 +1,35 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as io from 'socket.io-client';
import { ErrorGateway } from '../src/error.gateway';
describe('ErrorGateway', () => {
let app: INestApplication;
beforeEach(async () => {
const testingModule = await Test.createTestingModule({
providers: [ErrorGateway],
}).compile();
app = await testingModule.createNestApplication();
await app.listenAsync(3000);
});
it(`should handle error`, async () => {
const ws = io.connect('http://localhost:8080');
ws.emit('push', {
test: 'test',
});
await new Promise(resolve =>
ws.on('exception', data => {
expect(data).to.be.eql({
status: 'error',
message: 'test',
});
resolve();
}),
);
});
afterEach(() => app.close());
});

View File

@@ -0,0 +1,14 @@
import {
SubscribeMessage,
WebSocketGateway,
WsException,
} from '@nestjs/websockets';
import { throwError } from 'rxjs';
@WebSocketGateway(8080)
export class ErrorGateway {
@SubscribeMessage('push')
onPush(client, data) {
return throwError(new WsException('test'));
}
}

View File

@@ -3,5 +3,5 @@
"packages": [
"packages/*"
],
"version": "5.4.1"
"version": "5.5.0"
}

12
package-lock.json generated
View File

@@ -4982,9 +4982,9 @@
},
"dependencies": {
"fastify-plugin": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-1.2.1.tgz",
"integrity": "sha512-TLmXpp3PEsD3MFSV3FBiH70hkalvgZ8Qmg3hr1FcJpZFaPwxoWUDoFdXZrguY+2gBVQgkFtrM47aWL5lNHqg+Q==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-1.3.0.tgz",
"integrity": "sha512-3Tv+3ptWUbxgd2dRbZ57c3wOPDqJf+X5cSSN20YTOG9KI9YL13/PEZhEu66E3Yd/YDPWx8uZtC+axfxI+24qLA==",
"requires": {
"semver": "^5.5.0"
}
@@ -15733,9 +15733,9 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.1.tgz",
"integrity": "sha512-jw7P2z/h6aPT4AENXDGjcfHTu5CSqzsbZc6YlUIebTyBAq8XaKp78x7VcSh30xwSCcsu5irZkYZUSFP1MrAMbg==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz",
"integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==",
"dev": true
},
"uglify-js": {

View File

@@ -5,30 +5,21 @@
"scripts": {
"coverage": "nyc report --reporter=text-lcov | coveralls",
"precommit": "lint-staged",
"test":
"nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec --require 'node_modules/reflect-metadata/Reflect.js'",
"integration-test":
"mocha integration/**/*.spec.ts --reporter spec --require ts-node/register --require 'node_modules/reflect-metadata/Reflect.js'",
"lint":
"tslint -p tsconfig.json -c tslint.json \"packages/**/*.ts\" -e \"*.spec.ts\"",
"format":
"prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status",
"test": "nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec --require 'node_modules/reflect-metadata/Reflect.js'",
"integration-test": "mocha integration/**/*.spec.ts --reporter spec --require ts-node/register --require 'node_modules/reflect-metadata/Reflect.js'",
"lint": "tslint -p tsconfig.json -c tslint.json \"packages/**/*.ts\" -e \"*.spec.ts\"",
"format": "prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status",
"clean": "gulp clean:bundle",
"build": "npm run clean && gulp build",
"prebuild:dev": "rm -rf node_modules/@nestjs",
"build:dev": "gulp build --dist node_modules/@nestjs && gulp move",
"postinstall": "opencollective",
"prerelease": "gulp copy-misc && gulp build --dist node_modules/@nestjs",
"publish":
"npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --force-publish --exact -m \"chore(@nestjs) publish %s release\"",
"publish:rc":
"npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --npm-tag=rc -m \"chore(@nestjs) publish %s release\"",
"publish:next":
"npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --npm-tag=next --skip-git -m \"chore(@nestjs) publish %s release\"",
"publish:beta":
"npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --npm-tag=beta -m \"chore(@nestjs) publish %s release\"",
"publish:test":
"npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --force-publish --npm-tag=test --skip-git -m \"chore(@nestjs) publish %s release\""
"publish": "npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --force-publish --exact -m \"chore(@nestjs) publish %s release\"",
"publish:rc": "npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --npm-tag=rc -m \"chore(@nestjs) publish %s release\"",
"publish:next": "npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --npm-tag=next --skip-git -m \"chore(@nestjs) publish %s release\"",
"publish:beta": "npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --npm-tag=beta -m \"chore(@nestjs) publish %s release\"",
"publish:test": "npm run prerelease && npm run build && ./node_modules/.bin/lerna publish --force-publish --npm-tag=test --skip-git -m \"chore(@nestjs) publish %s release\""
},
"engines": {
"node": ">= 8.9.0"
@@ -132,7 +123,7 @@
"supertest": "^3.0.0",
"ts-node": "^6.0.0",
"tslint": "^5.11.0",
"typescript": "^3.1.6"
"typescript": "^3.2.2"
},
"collective": {
"type": "opencollective",
@@ -142,7 +133,9 @@
}
},
"nyc": {
"include": ["packages/**/*.ts"],
"include": [
"packages/**/*.ts"
],
"exclude": [
"node_modules/",
"packages/**/*.spec.ts",
@@ -161,13 +154,23 @@
"packages/common/serializer/**/*",
"packages/common/services/logger.service.ts"
],
"extension": [".ts"],
"require": ["ts-node/register"],
"reporter": ["text-summary", "html"],
"extension": [
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text-summary",
"html"
],
"sourceMap": true,
"instrument": true
},
"lint-staged": {
"packages/**/*.{ts,json}": ["npm run format", "git add"]
"packages/**/*.{ts,json}": [
"npm run format",
"git add"
]
}
}

View File

@@ -24,7 +24,10 @@ export class CacheModule {
return {
module: CacheModule,
imports: options.imports,
providers: this.createAsyncProviders(options),
providers: [
...this.createAsyncProviders(options),
...(options.extraProviders || []),
],
};
}
@@ -55,7 +58,8 @@ export class CacheModule {
}
return {
provide: CACHE_MODULE_OPTIONS,
useFactory: async (optionsFactory: CacheOptionsFactory) => optionsFactory.createCacheOptions(),
useFactory: async (optionsFactory: CacheOptionsFactory) =>
optionsFactory.createCacheOptions(),
inject: [options.useExisting || options.useClass],
};
}

View File

@@ -2,6 +2,7 @@ import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Inject, Injectable, Optional } from '../../decorators';
import {
CallHandler,
ExecutionContext,
HttpServer,
NestInterceptor,
@@ -28,22 +29,22 @@ export class CacheInterceptor implements NestInterceptor {
async intercept(
context: ExecutionContext,
call$: Observable<any>,
next: CallHandler,
): Promise<Observable<any>> {
const key = this.trackBy(context);
if (!key) {
return call$;
return next.handle();
}
try {
const value = await this.cacheManager.get(key);
if (value) {
return of(value);
}
return call$.pipe(
tap((response: any) => this.cacheManager.set(key, response)),
);
return next
.handle()
.pipe(tap(response => this.cacheManager.set(key, response)));
} catch {
return call$;
return next.handle();
}
}

View File

@@ -1,4 +1,4 @@
import { ModuleMetadata, Type } from '../../interfaces';
import { ModuleMetadata, Provider, Type } from '../../interfaces';
import { CacheManagerOptions } from './cache-manager.interface';
export interface CacheModuleOptions extends CacheManagerOptions {
@@ -17,4 +17,5 @@ export interface CacheModuleAsyncOptions
...args: any[]
) => Promise<CacheModuleOptions> | CacheModuleOptions;
inject?: any[];
extraProviders?: Provider[];
}

View File

@@ -15,17 +15,17 @@ export function Inject<T = any>(token?: T) {
token && isFunction(token) ? ((token as any) as Function).name : token;
if (!isUndefined(index)) {
const dependencies =
let dependencies =
Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || [];
dependencies.push({ index, param: type });
dependencies = [...dependencies, { index, param: type }];
Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, dependencies, target);
return;
}
const properties =
let properties =
Reflect.getMetadata(PROPERTY_DEPS_METADATA, target.constructor) || [];
properties.push({ key, type });
properties = [...properties, { key, type }];
Reflect.defineMetadata(
PROPERTY_DEPS_METADATA,
properties,

View File

@@ -1,2 +1,3 @@
export const AXIOS_INSTANCE_TOKEN = 'AXIOS_INSTANCE_TOKEN';
export const HTTP_MODULE_ID = 'HTTP_MODULE_ID';
export const HTTP_MODULE_OPTIONS = 'HTTP_MODULE_OPTIONS';

View File

@@ -1,9 +1,18 @@
import Axios, { AxiosRequestConfig } from 'axios';
import Axios from 'axios';
import { Module } from '../decorators/modules/module.decorator';
import { DynamicModule } from '../interfaces';
import { DynamicModule, Provider } from '../interfaces';
import { randomStringGenerator } from '../utils/random-string-generator.util';
import { AXIOS_INSTANCE_TOKEN, HTTP_MODULE_ID } from './http.constants';
import {
AXIOS_INSTANCE_TOKEN,
HTTP_MODULE_ID,
HTTP_MODULE_OPTIONS,
} from './http.constants';
import { HttpService } from './http.service';
import {
HttpModuleAsyncOptions,
HttpModuleOptions,
HttpModuleOptionsFactory,
} from './interfaces';
@Module({
providers: [
@@ -16,7 +25,7 @@ import { HttpService } from './http.service';
exports: [HttpService],
})
export class HttpModule {
static register(config: AxiosRequestConfig): DynamicModule {
static register(config: HttpModuleOptions): DynamicModule {
return {
module: HttpModule,
providers: [
@@ -31,4 +40,52 @@ export class HttpModule {
],
};
}
static registerAsync(options: HttpModuleAsyncOptions): DynamicModule {
return {
module: HttpModule,
imports: options.imports,
providers: [
...this.createAsyncProviders(options),
{
provide: HTTP_MODULE_ID,
useValue: randomStringGenerator(),
},
...(options.extraProviders || []),
],
};
}
private static createAsyncProviders(
options: HttpModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}
private static createAsyncOptionsProvider(
options: HttpModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: HTTP_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: HTTP_MODULE_OPTIONS,
useFactory: async (optionsFactory: HttpModuleOptionsFactory) =>
optionsFactory.createHttpOptions(),
inject: [options.useExisting || options.useClass],
};
}
}

View File

@@ -1,2 +1,3 @@
export * from './http.module';
export * from './http.service';
export * from './interfaces';

View File

@@ -0,0 +1,19 @@
import { AxiosRequestConfig } from 'axios';
import { ModuleMetadata, Provider, Type } from '../../interfaces';
export interface HttpModuleOptions extends AxiosRequestConfig {}
export interface HttpModuleOptionsFactory {
createHttpOptions(): Promise<HttpModuleOptions> | HttpModuleOptions;
}
export interface HttpModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<HttpModuleOptions>;
useClass?: Type<HttpModuleOptions>;
useFactory?: (
...args: any[]
) => Promise<HttpModuleOptions> | HttpModuleOptions;
inject?: any[];
extraProviders?: Provider[];
}

View File

@@ -0,0 +1 @@
export * from './http-module.interface';

View File

@@ -14,6 +14,7 @@ export * from './http';
export {
ArgumentMetadata,
ArgumentsHost,
CallHandler,
CanActivate,
DynamicModule,
ExceptionFilter,
@@ -36,6 +37,7 @@ export {
Provider,
RpcExceptionFilter,
Type,
ValidationError,
WebSocketAdapter,
WsExceptionFilter,
} from './interfaces';

View File

@@ -0,0 +1,28 @@
/**
* Validation error description.
* @see https://github.com/typestack/class-validator
*/
export interface ValidationError {
/**
* Object that was validated.
*/
target: Object;
/**
* Object's property that haven't pass validation.
*/
property: string;
/**
* Value that haven't pass a validation.
*/
value: any;
/**
* Constraints that failed validation with error messages.
*/
constraints: {
[type: string]: string;
};
/**
* Contains all nested validation errors of the property.
*/
children: ValidationError[];
}

View File

@@ -1,9 +1,13 @@
import { Observable } from 'rxjs';
import { ExecutionContext } from './execution-context.interface';
export interface CallHandler<T = any> {
handle(): Observable<T>;
}
export interface NestInterceptor<T = any, R = any> {
intercept(
context: ExecutionContext,
call$: Observable<T>,
next: CallHandler<T>,
): Observable<R> | Promise<Observable<R>>;
}

View File

@@ -4,6 +4,7 @@ export * from './controllers/controller.interface';
export * from './exceptions/exception-filter.interface';
export * from './exceptions/rpc-exception-filter.interface';
export * from './exceptions/ws-exception-filter.interface';
export * from './external/validation-error.interface';
export * from './features/arguments-host.interface';
export * from './features/can-activate.interface';
export * from './features/custom-route-param-factory.interface';

View File

@@ -75,6 +75,7 @@ export interface NatsOptions {
reconnectTimeWait?: number;
servers?: string[];
tls?: any;
queue?: string;
};
}

View File

@@ -1,8 +1,7 @@
{
"name": "@nestjs/common",
"version": "5.4.1",
"description":
"Nest - modern, fast, powerful node.js web framework (@common)",
"version": "5.5.0",
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
"author": "Kamil Mysliwiec",
"repository": {
"type": "git",

View File

@@ -1,6 +1,10 @@
import { Optional } from '../decorators';
import { Injectable } from '../decorators/core';
import { ArgumentMetadata, BadRequestException } from '../index';
import {
ArgumentMetadata,
BadRequestException,
ValidationError,
} from '../index';
import { ValidatorOptions } from '../interfaces/external/validator-options.interface';
import { PipeTransform } from '../interfaces/features/pipe-transform.interface';
import { loadPackage } from '../utils/load-package.util';
@@ -9,6 +13,7 @@ import { isNil } from '../utils/shared.utils';
export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessages?: boolean;
exceptionFactory?: (errors: ValidationError[]) => any;
}
let classValidator: any = {};
@@ -19,6 +24,7 @@ export class ValidationPipe implements PipeTransform<any> {
protected isTransformEnabled: boolean;
protected isDetailedOutputDisabled?: boolean;
protected validatorOptions: ValidatorOptions;
protected exceptionFactory: (errors: ValidationError[]) => any;
constructor(@Optional() options?: ValidationPipeOptions) {
options = options || {};
@@ -26,6 +32,12 @@ export class ValidationPipe implements PipeTransform<any> {
this.isTransformEnabled = !!transform;
this.validatorOptions = validatorOptions;
this.isDetailedOutputDisabled = disableErrorMessages;
this.exceptionFactory =
options.exceptionFactory ||
(errors =>
new BadRequestException(
this.isDetailedOutputDisabled ? undefined : errors,
));
const loadPkg = (pkg: any) => loadPackage(pkg, 'ValidationPipe');
classValidator = loadPkg('class-validator');
@@ -43,9 +55,7 @@ export class ValidationPipe implements PipeTransform<any> {
);
const errors = await classValidator.validate(entity, this.validatorOptions);
if (errors.length > 0) {
throw new BadRequestException(
this.isDetailedOutputDisabled ? undefined : errors,
);
throw this.exceptionFactory(errors);
}
return this.isTransformEnabled
? entity

View File

@@ -4,7 +4,11 @@ import { Inject, Injectable } from '../decorators/core';
import { ClassTransformOptions } from '../interfaces/external/class-transform-options.interface';
import { loadPackage } from '../utils/load-package.util';
import { isObject } from '../utils/shared.utils';
import { ExecutionContext, NestInterceptor } from './../interfaces';
import {
CallHandler,
ExecutionContext,
NestInterceptor,
} from './../interfaces';
import { CLASS_SERIALIZER_OPTIONS } from './class-serializer.constants';
let classTransformer: any = {};
@@ -26,16 +30,15 @@ export class ClassSerializerInterceptor implements NestInterceptor {
classTransformer = loadPkg('class-transformer');
}
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const options = this.getContextOptions(context);
return call$.pipe(
map((res: PlainLiteralObject | Array<PlainLiteralObject>) =>
this.serialize(res, options),
),
);
return next
.handle()
.pipe(
map((res: PlainLiteralObject | Array<PlainLiteralObject>) =>
this.serialize(res, options),
),
);
}
serialize(

View File

@@ -86,8 +86,7 @@ export class Logger implements LoggerService {
if (Logger.contextEnvironment === NestEnvironment.TEST) {
return;
}
const output =
message && isObject(message) ? JSON.stringify(message, null, 2) : message;
const output = isObject(message) ? JSON.stringify(message, null, 2) : message;
process.stdout.write(color(`[Nest] ${process.pid} - `));
process.stdout.write(`${new Date(Date.now()).toLocaleString()} `);

View File

@@ -25,6 +25,7 @@ describe('Shared utils', () => {
});
it('should returns false when object is not function', () => {
expect(isFunction(null)).to.be.false;
expect(isFunction(undefined)).to.be.false;
});
});
describe('isObject', () => {
@@ -33,6 +34,8 @@ describe('Shared utils', () => {
});
it('should returns false when object is not object', () => {
expect(isObject(3)).to.be.false;
expect(isObject(null)).to.be.false;
expect(isObject(undefined)).to.be.false;
});
});
describe('isString', () => {
@@ -41,6 +44,8 @@ describe('Shared utils', () => {
});
it('should returns false when object is not string', () => {
expect(isString(false)).to.be.false;
expect(isString(null)).to.be.false;
expect(isString(undefined)).to.be.false;
});
});
describe('isConstructor', () => {
@@ -58,6 +63,11 @@ describe('Shared utils', () => {
it('should returns same path', () => {
expect(validatePath('/nope')).to.be.eql('/nope');
});
it('should returns empty path', () => {
expect(validatePath('')).to.be.eql('');
expect(validatePath(null)).to.be.eql('');
expect(validatePath(undefined)).to.be.eql('');
});
});
describe('isNil', () => {
it('should returns true when obj is undefined or null', () => {
@@ -72,6 +82,7 @@ describe('Shared utils', () => {
it('should returns true when array is empty or not exists', () => {
expect(isEmpty([])).to.be.true;
expect(isEmpty(null)).to.be.true;
expect(isEmpty(undefined)).to.be.true;
});
it('should returns false when array is not empty', () => {
expect(isEmpty([1, 2])).to.be.false;

View File

@@ -1,5 +1,12 @@
import { isObject } from './shared.utils';
export const createHttpExceptionBody = (
message: any,
message: object | string,
error: string,
statusCode: number,
) => (message ? { statusCode, error, message } : { statusCode, error });
) => {
if (!message) {
return { statusCode, error };
}
return isObject(message) ? message : { statusCode, error, message };
};

View File

@@ -1,11 +1,12 @@
export const isUndefined = (obj: any): obj is undefined =>
typeof obj === 'undefined';
export const isObject = (fn: any): fn is object =>
!isNil(fn) && typeof fn === 'object';
export const validatePath = (path?: string): string =>
path ? (path.charAt(0) !== '/' ? '/' + path : path) : '';
export const isFunction = (fn: any): boolean => typeof fn === 'function';
export const isObject = (fn: any): fn is object => typeof fn === 'object';
export const isString = (fn: any): fn is string => typeof fn === 'string';
export const isConstructor = (fn: any): boolean => fn === 'constructor';
export const validatePath = (path: string): string =>
path.charAt(0) !== '/' ? '/' + path : path;
export const isNil = (obj: any): boolean => isUndefined(obj) || obj === null;
export const isEmpty = (array: any): boolean => !(array && array.length > 0);
export const isSymbol = (fn: any): fn is symbol => typeof fn === 'symbol';

View File

@@ -11,7 +11,7 @@ import { Module } from '../injector/module';
* Returns the name of an instance
* @param instance The instance which should get the name from
*/
const getInstanceName = (instance: any) =>
const getInstanceName = (instance: unknown) =>
instance && (instance as Type<any>).name;
/**

View File

@@ -20,7 +20,7 @@ export class BaseExceptionFilterContext extends ContextCreator {
public createConcreteContext<T extends any[], R extends any[]>(
metadata: T,
): R {
if (isUndefined(metadata) || isEmpty(metadata)) {
if (isEmpty(metadata)) {
return [] as R;
}
return iterate(metadata)

View File

@@ -2,11 +2,7 @@ import { CanActivate } from '@nestjs/common';
import { GUARDS_METADATA } from '@nestjs/common/constants';
import { Controller } from '@nestjs/common/interfaces';
import { ConfigurationProvider } from '@nestjs/common/interfaces/configuration-provider.interface';
import {
isEmpty,
isFunction,
isUndefined,
} from '@nestjs/common/utils/shared.utils';
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils';
import iterate from 'iterare';
import { ContextCreator } from '../helpers/context-creator';
import { NestContainer } from '../injector/container';
@@ -33,7 +29,7 @@ export class GuardsContextCreator extends ContextCreator {
public createConcreteContext<T extends any[], R extends any[]>(
metadata: T,
): R {
if (isUndefined(metadata) || isEmpty(metadata)) {
if (isEmpty(metadata)) {
return [] as R;
}
return iterate(metadata)
@@ -65,7 +61,8 @@ export class GuardsContextCreator extends ContextCreator {
if (!module) {
return undefined;
}
return module.injectables.get(guard.name);
const injectables = module.injectables;
return injectables.get(guard.name);
}
public getGlobalMetadata<T extends any[]>(): T {

View File

@@ -1,7 +1,7 @@
import { ForbiddenException, ParamData } from '@nestjs/common';
import { CUSTOM_ROUTE_AGRS_METADATA } from '@nestjs/common/constants';
import { Controller, Transform } from '@nestjs/common/interfaces';
import { isFunction, isUndefined } from '@nestjs/common/utils/shared.utils';
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { FORBIDDEN_MESSAGE } from '../guards/constants';
import { GuardsConsumer } from '../guards/guards-consumer';
import { GuardsContextCreator } from '../guards/guards-context-creator';
@@ -169,7 +169,7 @@ export class ExternalContextCreator {
factory: (...args: any[]) => void,
data: any,
): (...args: any[]) => any {
return !isUndefined(factory) && isFunction(factory)
return isFunction(factory)
? (...args: any[]) => factory(data, args)
: () => null;
}

View File

@@ -1,8 +1,8 @@
import { NestInterceptor } from '@nestjs/common';
import { Controller } from '@nestjs/common/interfaces';
import { CallHandler, Controller } from '@nestjs/common/interfaces';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { defer, from as fromPromise, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { mergeAll, switchMap } from 'rxjs/operators';
import { ExecutionContextHost } from '../helpers/execution-context.host';
export class InterceptorsConsumer {
@@ -18,20 +18,17 @@ export class InterceptorsConsumer {
}
const context = this.createContext(args, instance, callback);
const start$ = defer(() => this.transformDeffered(next));
/***
const nextFn = (i: number) => async () => {
if (i <= interceptors.length) {
const nextFn = (i = 0) => async () => {
if (i >= interceptors.length) {
return start$;
}
return await interceptors[i].intercept(context, nextFn(i + 1) as any);
const handler: CallHandler = {
handle: () => fromPromise(nextFn(i + 1)()).pipe(mergeAll()),
};
return interceptors[i].intercept(context, handler);
};
*/
const result$ = await interceptors.reduce(
async (stream$, interceptor) =>
interceptor.intercept(context, await stream$),
Promise.resolve(start$),
);
return result$.toPromise();
return nextFn()();
}
public createContext(

View File

@@ -32,7 +32,7 @@ export class InterceptorsContextCreator extends ContextCreator {
public createConcreteContext<T extends any[], R extends any[]>(
metadata: T,
): R {
if (isUndefined(metadata) || isEmpty(metadata)) {
if (isEmpty(metadata)) {
return [] as R;
}
return iterate(metadata)

View File

@@ -198,7 +198,7 @@ export class MiddlewareModule {
) {
const proxy = this.routerProxy.createProxy(middleware, exceptionsHandler);
const prefix = this.config.getGlobalPrefix();
const basePath = prefix ? validatePath(prefix) : '';
const basePath = validatePath(prefix);
router(basePath + path, proxy);
}
}

View File

@@ -131,7 +131,7 @@ export class NestApplication extends NestApplicationContext
await this.registerMiddleware(this.httpAdapter);
const prefix = this.config.getGlobalPrefix();
const basePath = prefix ? validatePath(prefix) : '';
const basePath = validatePath(prefix);
this.routesResolver.resolve(this.httpAdapter, basePath);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/core",
"version": "5.4.1",
"version": "5.5.0",
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
"author": "Kamil Mysliwiec",
"license": "MIT",

View File

@@ -36,7 +36,7 @@ export class PipesContextCreator extends ContextCreator {
public createConcreteContext<T extends any[], R extends any[]>(
metadata: T,
): R {
if (isUndefined(metadata) || isEmpty(metadata)) {
if (isEmpty(metadata)) {
return [] as R;
}
return iterate(metadata)

View File

@@ -19,7 +19,6 @@ import {
isEmpty,
isFunction,
isString,
isUndefined,
} from '@nestjs/common/utils/shared.utils';
import { FORBIDDEN_MESSAGE } from '../guards/constants';
import { GuardsConsumer } from '../guards/guards-consumer';
@@ -191,7 +190,7 @@ export class RouterExecutionContext {
factory: (...args: any[]) => void,
data: any,
): (...args: any[]) => any {
return !isUndefined(factory) && isFunction(factory)
return isFunction(factory)
? (req, res, next) => factory(data, req)
: () => null;
}

View File

@@ -1,8 +1,7 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { Observable, of } from 'rxjs';
import { of } from 'rxjs';
import * as sinon from 'sinon';
import { InterceptorsConsumer } from '../../interceptors/interceptors-consumer';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
describe('InterceptorsConsumer', () => {
let consumer: InterceptorsConsumer;
@@ -11,10 +10,12 @@ describe('InterceptorsConsumer', () => {
consumer = new InterceptorsConsumer();
interceptors = [
{
intercept: sinon.stub().returns(of(true)),
intercept: sinon.stub().callsFake((ctx, handler) => handler.handle()),
},
{
intercept: sinon.stub().returns(of(true)),
intercept: sinon
.stub()
.callsFake(async (ctx, handler) => handler.handle()),
},
];
});
@@ -73,18 +74,18 @@ describe('InterceptorsConsumer', () => {
it('should return Observable', async () => {
const val = 3;
const next = async () => val;
expect(
await consumer.transformDeffered(next).toPromise(),
).to.be.eql(val);
expect(await consumer.transformDeffered(next).toPromise()).to.be.eql(
val,
);
});
});
describe('when next() result is Promise', () => {
it('should return Observable', async () => {
const val = 3;
const next = async () => val;
expect(
await consumer.transformDeffered(next).toPromise(),
).to.be.eql(val);
expect(await consumer.transformDeffered(next).toPromise()).to.be.eql(
val,
);
});
});
describe('when next() result is Observable', () => {

View File

@@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { Observable } from 'rxjs';
import { GRPC_DEFAULT_URL } from '../constants';
import { GRPC_DEFAULT_PROTO_LOADER, GRPC_DEFAULT_URL } from '../constants';
import { InvalidGrpcPackageException } from '../errors/invalid-grpc-package.exception';
import { InvalidGrpcServiceException } from '../errors/invalid-grpc-service.exception';
import { InvalidProtoDefinitionException } from '../errors/invalid-proto-definition.exception';
@@ -24,11 +24,12 @@ export class ClientGrpcProxy extends ClientProxy implements ClientGrpc {
this.url =
this.getOptionsProp<GrpcOptions>(options, 'url') || GRPC_DEFAULT_URL;
const protoLoader =
this.getOptionsProp<GrpcOptions>(options, 'protoLoader') ||
GRPC_DEFAULT_PROTO_LOADER;
grpcPackage = loadPackage('grpc', ClientGrpcProxy.name);
grpcProtoLoaderPackage = loadPackage(
'@grpc/proto-loader',
ClientGrpcProxy.name,
);
grpcProtoLoaderPackage = loadPackage(protoLoader, ClientGrpcProxy.name);
this.grpcClient = this.createClient();
}

View File

@@ -87,6 +87,6 @@ export abstract class ClientProxy {
}
protected normalizePattern<T = any>(pattern: T): string {
return pattern && isString(pattern) ? pattern : JSON.stringify(pattern);
return isString(pattern) ? pattern : JSON.stringify(pattern);
}
}

View File

@@ -25,3 +25,5 @@ export const RQM_DEFAULT_QUEUE = 'default';
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
export const GRPC_DEFAULT_PROTO_LOADER = '@grpc/proto-loader';

View File

@@ -1,5 +1,7 @@
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { RpcExceptionsHandler } from '../exceptions/rpc-exceptions-handler';
export class RpcProxy {
@@ -9,11 +11,30 @@ export class RpcProxy {
): (...args: any[]) => Promise<Observable<any>> {
return async (...args: any[]) => {
try {
return await targetCallback(...args);
} catch (e) {
const host = new ExecutionContextHost(args);
return exceptionsHandler.handle(e, host);
const result = await targetCallback(...args);
return !this.isObservable(result)
? result
: result.pipe(
catchError(error =>
this.handleError(exceptionsHandler, args, error),
),
);
} catch (error) {
return this.handleError(exceptionsHandler, args, error);
}
};
}
handleError<T>(
exceptionsHandler: RpcExceptionsHandler,
args: any[],
error: T,
): Observable<any> {
const host = new ExecutionContextHost(args);
return exceptionsHandler.handle(error, host);
}
isObservable(result: any): boolean {
return result && isFunction(result.subscribe);
}
}

View File

@@ -1,8 +1,8 @@
export * from './client-grpc.interface';
export * from './client-metadata.interface';
export * from './microservice-configuration.interface';
export * from './pattern-metadata.interface';
export * from './custom-transport-strategy.interface';
export * from './message-handlers.interface';
export * from './closeable.interface';
export * from './custom-transport-strategy.interface';
export * from './message-handler.interface';
export * from './microservice-configuration.interface';
export * from './packet.interface';
export * from './client-grpc.interface';
export * from './pattern-metadata.interface';

View File

@@ -0,0 +1,5 @@
import { Observable } from 'rxjs';
export type MessageHandler<TInput = any, TResult = any> = (
data: TInput,
) => Promise<Observable<TResult>>;

View File

@@ -24,6 +24,7 @@ export interface GrpcOptions {
credentials?: any;
protoPath: string;
package: string;
protoLoader?: string;
loader?: {
keepCase?: boolean;
alternateCommentMode?: boolean;

View File

@@ -10,6 +10,16 @@ import {
import { ClientOptions } from './interfaces/client-metadata.interface';
import { PatternMetadata } from './interfaces/pattern-metadata.interface';
export interface ClientProperties {
property: string;
metadata: ClientOptions;
}
export interface PatternProperties {
pattern: PatternMetadata;
targetCallback: (...args: any[]) => any;
}
export class ListenerMetadataExplorer {
constructor(private readonly metadataScanner: MetadataScanner) {}
@@ -63,13 +73,3 @@ export class ListenerMetadataExplorer {
}
}
}
export interface ClientProperties {
property: string;
metadata: ClientOptions;
}
export interface PatternProperties {
pattern: PatternMetadata;
targetCallback: (...args: any[]) => any;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/microservices",
"version": "5.4.1",
"version": "5.5.0",
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
"author": "Kamil Mysliwiec",
"license": "MIT",

View File

@@ -1,6 +1,10 @@
import { fromEvent } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { CANCEL_EVENT, GRPC_DEFAULT_URL } from '../constants';
import {
CANCEL_EVENT,
GRPC_DEFAULT_PROTO_LOADER,
GRPC_DEFAULT_URL,
} from '../constants';
import { InvalidGrpcPackageException } from '../errors/invalid-grpc-package.exception';
import { InvalidProtoDefinitionException } from '../errors/invalid-proto-definition.exception';
import { CustomTransportStrategy } from '../interfaces';
@@ -28,11 +32,12 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
this.url =
this.getOptionsProp<GrpcOptions>(options, 'url') || GRPC_DEFAULT_URL;
const protoLoader =
this.getOptionsProp<GrpcOptions>(options, 'protoLoader') ||
GRPC_DEFAULT_PROTO_LOADER;
grpcPackage = this.loadPackage('grpc', ServerGrpc.name);
grpcProtoLoaderPackage = this.loadPackage(
'@grpc/proto-loader',
ServerGrpc.name,
);
grpcProtoLoaderPackage = this.loadPackage(protoLoader, ServerGrpc.name);
}
public async listen(callback: () => void) {
@@ -75,9 +80,9 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
// tslint:disable-next-line:forin
for (const methodName in grpcService.prototype) {
const methodHandler = this.messageHandlers[
this.createPattern(name, methodName)
];
const methodHandler = this.getHandlerByPattern(
this.createPattern(name, methodName),
);
if (!methodHandler) {
continue;
}

View File

@@ -42,7 +42,7 @@ export class ServerMqtt extends Server implements CustomTransportStrategy {
public bindEvents(mqttClient: MqttClient) {
mqttClient.on(MESSAGE_EVENT, this.getMessageHandler(mqttClient).bind(this));
const registeredPatterns = Object.keys(this.messageHandlers);
const registeredPatterns = [...this.messageHandlers.keys()];
registeredPatterns.forEach(pattern =>
mqttClient.subscribe(this.getAckQueueName(pattern)),
);
@@ -56,7 +56,7 @@ export class ServerMqtt extends Server implements CustomTransportStrategy {
return mqttPackage.connect(this.url, this.options as MqttOptions);
}
public getMessageHandler(pub: MqttClient): any {
public getMessageHandler(pub: MqttClient): Function {
return async (channel: string, buffer: Buffer) =>
this.handleMessage(channel, buffer, pub);
}
@@ -69,12 +69,12 @@ export class ServerMqtt extends Server implements CustomTransportStrategy {
const packet = this.deserialize(buffer.toString());
const pattern = channel.replace(/_ack$/, '');
const publish = this.getPublisher(pub, pattern, packet.id);
const status = 'error';
const handler = this.getHandlerByPattern(pattern);
if (!this.messageHandlers[pattern]) {
if (!handler) {
const status = 'error';
return publish({ id: packet.id, status, err: NO_PATTERN_MESSAGE });
}
const handler = this.messageHandlers[pattern];
const response$ = this.transformToObservable(
await handler(packet.data),
) as Observable<any>;

View File

@@ -41,20 +41,20 @@ export class ServerNats extends Server implements CustomTransportStrategy {
public bindEvents(client: Client) {
const queue = this.getOptionsProp<NatsOptions>(this.options, 'queue');
const subscribe = (channel: string) => {
if (queue) {
return client.subscribe(
channel,
{ queue },
this.getMessageHandler(channel, client).bind(this),
);
}
client.subscribe(
channel,
this.getMessageHandler(channel, client).bind(this),
);
};
const registeredPatterns = Object.keys(this.messageHandlers);
const subscribe = queue
? (channel: string) =>
client.subscribe(
channel,
{ queue },
this.getMessageHandler(channel, client).bind(this),
)
: (channel: string) =>
client.subscribe(
channel,
this.getMessageHandler(channel, client).bind(this),
);
const registeredPatterns = [...this.messageHandlers.keys()];
registeredPatterns.forEach(channel => subscribe(channel));
}
@@ -72,7 +72,7 @@ export class ServerNats extends Server implements CustomTransportStrategy {
});
}
public getMessageHandler(channel: string, client: Client) {
public getMessageHandler(channel: string, client: Client): Function {
return async (buffer: ReadPacket & PacketId, replyTo: string) =>
this.handleMessage(channel, buffer, client, replyTo);
}
@@ -84,12 +84,11 @@ export class ServerNats extends Server implements CustomTransportStrategy {
replyTo: string,
) {
const publish = this.getPublisher(client, replyTo, message.id);
const status = 'error';
if (!this.messageHandlers[channel]) {
const handler = this.getHandlerByPattern(channel);
if (!handler) {
const status = 'error';
return publish({ id: message.id, status, err: NO_PATTERN_MESSAGE });
}
const handler = this.messageHandlers[channel];
const response$ = this.transformToObservable(
await handler(message.data),
) as Observable<any>;

View File

@@ -51,7 +51,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
public bindEvents(subClient: RedisClient, pubClient: RedisClient) {
subClient.on(MESSAGE_EVENT, this.getMessageHandler(pubClient).bind(this));
const subscribePatterns = Object.keys(this.messageHandlers);
const subscribePatterns = [...this.messageHandlers.keys()];
subscribePatterns.forEach(pattern =>
subClient.subscribe(this.getAckQueueName(pattern)),
);
@@ -83,12 +83,12 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
const packet = this.deserialize(buffer);
const pattern = channel.replace(/_ack$/, '');
const publish = this.getPublisher(pub, pattern, packet.id);
const status = 'error';
const handler = this.getHandlerByPattern(pattern);
if (!this.messageHandlers[pattern]) {
if (!handler) {
const status = 'error';
return publish({ id: packet.id, status, err: NO_PATTERN_MESSAGE });
}
const handler = this.messageHandlers[pattern];
const response$ = this.transformToObservable(
await handler(packet.data),
) as Observable<any>;

View File

@@ -53,16 +53,16 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
const pattern = !isString(packet.pattern)
? JSON.stringify(packet.pattern)
: packet.pattern;
const status = 'error';
const handler = this.getHandlerByPattern(pattern);
if (!this.messageHandlers[pattern]) {
if (!handler) {
const status = 'error';
return socket.sendMessage({
id: packet.id,
status,
err: NO_PATTERN_MESSAGE,
});
}
const handler = this.messageHandlers[pattern];
const response$ = this.transformToObservable(
await handler(packet.data),
) as Observable<any>;

View File

@@ -9,11 +9,14 @@ import {
Subscription,
} from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { MicroserviceOptions, WritePacket } from '../interfaces';
import { MessageHandlers } from '../interfaces/message-handlers.interface';
import {
MessageHandler,
MicroserviceOptions,
WritePacket,
} from '../interfaces';
export abstract class Server {
protected readonly messageHandlers: MessageHandlers = {};
protected readonly messageHandlers = new Map<string, MessageHandler>();
protected readonly logger = new Logger(Server.name);
public addHandler(
@@ -21,17 +24,17 @@ export abstract class Server {
callback: <T>(data: T) => Promise<Observable<any>>,
) {
const key = isString(pattern) ? pattern : JSON.stringify(pattern);
this.messageHandlers[key] = callback;
this.messageHandlers.set(key, callback);
}
public getHandlers(): MessageHandlers {
public getHandlers(): Map<string, MessageHandler> {
return this.messageHandlers;
}
public getHandlerByPattern(
pattern: string,
): <T>(data: T) => Promise<Observable<any>> | null {
return this.messageHandlers[pattern] ? this.messageHandlers[pattern] : null;
public getHandlerByPattern(pattern: string): MessageHandler | null {
return this.messageHandlers.has(pattern)
? this.messageHandlers.get(pattern)
: null;
}
public send(
@@ -70,7 +73,7 @@ export abstract class Server {
this.logger.error(error);
}
protected loadPackage(name: string, ctx: string) {
protected loadPackage<T = any>(name: string, ctx: string): T {
return loadPackage(name, ctx);
}
}

View File

@@ -1,9 +1,9 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { of, throwError } from 'rxjs';
import * as sinon from 'sinon';
import { RpcProxy } from '../../context/rpc-proxy';
import { RpcExceptionsHandler } from '../../exceptions/rpc-exceptions-handler';
import { RpcException } from '../../exceptions/rpc-exception';
import { Observable, of } from 'rxjs';
import { RpcExceptionsHandler } from '../../exceptions/rpc-exceptions-handler';
describe('RpcProxy', () => {
let routerProxy: RpcProxy;
@@ -18,10 +18,7 @@ describe('RpcProxy', () => {
describe('create', () => {
it('should method return thunk', async () => {
const proxy = await routerProxy.create(
async data => of(true),
handler,
);
const proxy = await routerProxy.create(async data => of(true), handler);
expect(typeof proxy === 'function').to.be.true;
});
@@ -33,5 +30,26 @@ describe('RpcProxy', () => {
await proxy(null);
expectation.verify();
});
it('should attach "catchError" operator when observable was returned', async () => {
const expectation = handlerMock.expects('handle').once();
const proxy = routerProxy.create(async (client, data) => {
return throwError(new RpcException('test'));
}, handler);
(await proxy(null, null)).subscribe(null, () => expectation.verify());
});
});
describe('isObservable', () => {
describe('when observable', () => {
it('should return true', () => {
expect(routerProxy.isObservable(of('test'))).to.be.true;
});
});
describe('when not observable', () => {
it('should return false', () => {
expect(routerProxy.isObservable({})).to.be.false;
});
});
});
});

View File

@@ -80,10 +80,12 @@ describe('ServerGrpc', () => {
describe('createService', () => {
it('should call "createServiceMethod"', async () => {
const handlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
const handlers = objectToMap({
test: null,
test2: () => ({}),
};
});
sinon
.stub(server, 'createPattern')
.onFirstCall()

View File

@@ -1,8 +1,7 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { NO_PATTERN_MESSAGE } from '../../constants';
import { ServerMqtt } from '../../server/server-mqtt';
import { Observable } from 'rxjs';
describe('ServerMqtt', () => {
let server: ServerMqtt;
@@ -59,9 +58,12 @@ describe('ServerMqtt', () => {
it('should subscribe each acknowledge patterns', () => {
const pattern = 'test';
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[pattern]: handler,
};
});
server.bindEvents(mqttClient);
const expectedPattern = 'test_ack';
@@ -111,9 +113,12 @@ describe('ServerMqtt', () => {
});
it(`should call handler with expected arguments`, () => {
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[channel]: handler,
};
});
server.handleMessage(
channel,

View File

@@ -52,12 +52,16 @@ describe('ServerNats', () => {
subscribe: subscribeSpy,
};
});
it('should subscribe each acknowledge patterns', () => {
it('should subscribe to each acknowledge patterns', () => {
const pattern = 'test';
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[pattern]: handler,
};
});
server.bindEvents(natsClient);
expect(subscribeSpy.calledWith(pattern)).to.be.true;
});
@@ -104,9 +108,12 @@ describe('ServerNats', () => {
});
it(`should call handler with expected arguments`, () => {
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[channel]: handler,
};
});
server.handleMessage(channel, { pattern: '', data, id: '2' }, null, '');
expect(handler.calledWith(data)).to.be.true;

View File

@@ -66,9 +66,12 @@ describe('ServerRedis', () => {
it('should subscribe each acknowledge patterns', () => {
const pattern = 'test';
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[pattern]: handler,
};
});
server.bindEvents(sub, null);
const expectedPattern = 'test_ack';
@@ -104,9 +107,12 @@ describe('ServerRedis', () => {
});
it(`should call handler with expected arguments`, () => {
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[channel]: handler,
};
});
server.handleMessage(channel, {}, null);
expect(handler.calledWith(data)).to.be.true;

View File

@@ -93,9 +93,12 @@ describe('ServerRMQ', () => {
});
it('should call handler if exists in handlers object', async () => {
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[JSON.stringify(pattern)]: handler as any,
};
});
await server.handleMessage(msg);
expect(handler.calledOnce).to.be.true;
});

View File

@@ -68,9 +68,12 @@ describe('ServerTCP', () => {
});
it('should call handler if exists in handlers object', () => {
const handler = sinon.spy();
(server as any).messageHandlers = {
const objectToMap = obj =>
new Map(Object.keys(obj).map(key => [key, obj[key]]) as any);
(server as any).messageHandlers = objectToMap({
[msg.pattern]: handler as any,
};
});
server.handleMessage(socket, msg);
expect(handler.calledOnce).to.be.true;
});

View File

@@ -1,7 +1,7 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { Server } from '../../server/server';
import { Observable, of, throwError as _throw } from 'rxjs';
import * as sinon from 'sinon';
import { Server } from '../../server/server';
class TestServer extends Server {
public listen(callback: () => void) {}
@@ -18,13 +18,13 @@ describe('Server', () => {
server.addHandler(pattern, callback as any);
const handlers = server.getHandlers();
expect(handlers[JSON.stringify(pattern)]).to.equal(callback);
expect(handlers.get(JSON.stringify(pattern))).to.equal(callback);
});
it(`should add handler as string pattern key`, () => {
server.addHandler(pattern.test, callback as any);
const handlers = server.getHandlers();
expect(handlers[pattern.test]).to.equal(callback);
expect(handlers.get(pattern.test)).to.equal(callback);
});
});
describe('send', () => {
@@ -96,17 +96,17 @@ describe('Server', () => {
describe('getHandlerByPattern', () => {
describe('when handler exists', () => {
it('should return expected handler', () => {
const pattern = 'pattern';
const channel = 'pattern';
const expectedResult = {};
(server as any).messageHandlers[pattern] = expectedResult;
expect(server.getHandlerByPattern(pattern)).to.be.eql(expectedResult);
const handlers = new Map([[channel, expectedResult]]);
(server as any).messageHandlers = handlers;
expect(server.getHandlerByPattern(channel)).to.be.eql(expectedResult);
});
});
describe('when handler does not exists', () => {
it('should return null', () => {
const pattern = 'test';
const expectedResult = null;
expect(server.getHandlerByPattern(pattern)).to.be.eql(null);
const channel = 'test';
expect(server.getHandlerByPattern(channel)).to.be.eql(null);
});
});
});

View File

@@ -1,4 +1,5 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
@@ -38,7 +39,7 @@ export function FileFieldsInterceptor(
async intercept(
context: ExecutionContext,
call$: Observable<any>,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
@@ -55,7 +56,7 @@ export function FileFieldsInterceptor(
},
),
);
return call$;
return next.handle();
}
}
const Interceptor = mixin(MixinInterceptor);

View File

@@ -1,4 +1,5 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
@@ -35,7 +36,7 @@ export function FileInterceptor(
async intercept(
context: ExecutionContext,
call$: Observable<any>,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
@@ -52,7 +53,7 @@ export function FileInterceptor(
},
),
);
return call$;
return next.handle();
}
}
const Interceptor = mixin(MixinInterceptor);

View File

@@ -1,4 +1,5 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
@@ -36,7 +37,7 @@ export function FilesInterceptor(
async intercept(
context: ExecutionContext,
call$: Observable<any>,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
@@ -53,7 +54,7 @@ export function FilesInterceptor(
},
),
);
return call$;
return next.handle();
}
}
const Interceptor = mixin(MixinInterceptor);

View File

@@ -1,3 +1,4 @@
import { CallHandler } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
import { expect } from 'chai';
import { of } from 'rxjs';
@@ -13,9 +14,11 @@ describe('FileFieldsInterceptor', () => {
expect(targetClass.prototype.intercept).to.not.be.undefined;
});
describe('intercept', () => {
let stream$;
let handler: CallHandler;
beforeEach(() => {
stream$ = of('test');
handler = {
handle: () => of('test'),
};
});
it('should call object with expected params', async () => {
const fieldName1 = 'file';
@@ -33,7 +36,7 @@ describe('FileFieldsInterceptor', () => {
.stub((target as any).multer, 'fields')
.returns(callback);
await target.intercept(new ExecutionContextHost([]), stream$);
await target.intercept(new ExecutionContextHost([]), handler);
expect(fieldsSpy.called).to.be.true;
expect(fieldsSpy.calledWith(argument)).to.be.true;
@@ -55,7 +58,7 @@ describe('FileFieldsInterceptor', () => {
array: () => callback,
};
expect(
target.intercept(new ExecutionContextHost([]), stream$),
target.intercept(new ExecutionContextHost([]), handler),
).to.eventually.throw();
});
});

View File

@@ -1,3 +1,4 @@
import { CallHandler } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
import { expect } from 'chai';
import { of } from 'rxjs';
@@ -10,9 +11,11 @@ describe('FileInterceptor', () => {
expect(targetClass.prototype.intercept).to.not.be.undefined;
});
describe('intercept', () => {
let stream$;
let handler: CallHandler;
beforeEach(() => {
stream$ = of('test');
handler = {
handle: () => of('test'),
};
});
it('should call single() with expected params', async () => {
const fieldName = 'file';
@@ -22,7 +25,7 @@ describe('FileInterceptor', () => {
.stub((target as any).multer, 'single')
.returns(callback);
await target.intercept(new ExecutionContextHost([]), stream$);
await target.intercept(new ExecutionContextHost([]), handler);
expect(singleSpy.called).to.be.true;
expect(singleSpy.calledWith(fieldName)).to.be.true;
@@ -37,7 +40,7 @@ describe('FileInterceptor', () => {
single: () => callback,
};
expect(
target.intercept(new ExecutionContextHost([]), stream$),
target.intercept(new ExecutionContextHost([]), handler),
).to.eventually.throw();
});
});

View File

@@ -1,3 +1,4 @@
import { CallHandler } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
import { expect } from 'chai';
import { of } from 'rxjs';
@@ -10,9 +11,11 @@ describe('FilesInterceptor', () => {
expect(targetClass.prototype.intercept).to.not.be.undefined;
});
describe('intercept', () => {
let stream$;
let handler: CallHandler;
beforeEach(() => {
stream$ = of('test');
handler = {
handle: () => of('test'),
};
});
it('should call array() with expected params', async () => {
const fieldName = 'file';
@@ -24,7 +27,7 @@ describe('FilesInterceptor', () => {
.stub((target as any).multer, 'array')
.returns(callback);
await target.intercept(new ExecutionContextHost([]), stream$);
await target.intercept(new ExecutionContextHost([]), handler);
expect(arraySpy.called).to.be.true;
expect(arraySpy.calledWith(fieldName, maxCount)).to.be.true;
@@ -39,7 +42,7 @@ describe('FilesInterceptor', () => {
array: () => callback,
};
expect(
target.intercept(new ExecutionContextHost([]), stream$),
target.intercept(new ExecutionContextHost([]), handler),
).to.eventually.throw();
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/testing",
"version": "5.4.1",
"version": "5.5.0",
"description": "Nest - modern, fast, powerful node.js web framework (@testing)",
"author": "Kamil Mysliwiec",
"license": "MIT",

View File

@@ -3,6 +3,7 @@
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"skipLibCheck": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"removeComments": false,

View File

@@ -1,18 +1,41 @@
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
import { empty } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { WsExceptionsHandler } from '../exceptions/ws-exceptions-handler';
export class WsProxy {
public create(
targetCallback: (...args: any[]) => Promise<void>,
targetCallback: (...args: any[]) => Promise<any>,
exceptionsHandler: WsExceptionsHandler,
): (...args: any[]) => Promise<void> {
): (...args: any[]) => Promise<any> {
return async (...args: any[]) => {
try {
return await targetCallback(...args);
} catch (e) {
const host = new ExecutionContextHost(args);
exceptionsHandler.handle(e, host);
const result = await targetCallback(...args);
return !this.isObservable(result)
? result
: result.pipe(
catchError(error => {
this.handleError(exceptionsHandler, args, error);
return empty();
}),
);
} catch (error) {
this.handleError(exceptionsHandler, args, error);
}
};
}
handleError<T>(
exceptionsHandler: WsExceptionsHandler,
args: any[],
error: T,
) {
const host = new ExecutionContextHost(args);
exceptionsHandler.handle(error, host);
}
isObservable(result: any): boolean {
return result && isFunction(result.subscribe);
}
}

View File

@@ -6,10 +6,12 @@ export class ObservableSocket {
const init = new ReplaySubject<T>();
init.next(server);
const connection = new Subject();
const disconnect = new Subject();
return {
init,
connection: new Subject(),
disconnect: new Subject(),
connection,
disconnect,
server,
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/websockets",
"version": "5.4.1",
"version": "5.5.0",
"description": "Nest - modern, fast, powerful node.js web framework (@websockets)",
"author": "Kamil Mysliwiec",
"license": "MIT",

View File

@@ -1,8 +1,9 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { of, throwError } from 'rxjs';
import * as sinon from 'sinon';
import { WsProxy } from '../../context/ws-proxy';
import { WsExceptionsHandler } from '../../exceptions/ws-exceptions-handler';
import { WsException } from '../../errors/ws-exception';
import { WsExceptionsHandler } from '../../exceptions/ws-exceptions-handler';
describe('WsProxy', () => {
let routerProxy: WsProxy;
@@ -30,5 +31,26 @@ describe('WsProxy', () => {
await proxy(null, null);
expectation.verify();
});
it('should attach "catchError" operator when observable was returned', async () => {
const expectation = handlerMock.expects('handle').once();
const proxy = routerProxy.create(async (client, data) => {
return throwError(new WsException('test'));
}, handler);
(await proxy(null, null)).subscribe(null, () => expectation.verify());
});
});
describe('isObservable', () => {
describe('when observable', () => {
it('should return true', () => {
expect(routerProxy.isObservable(of('test'))).to.be.true;
});
});
describe('when not observable', () => {
it('should return false', () => {
expect(routerProxy.isObservable({})).to.be.false;
});
});
});
});

View File

@@ -1,23 +1,23 @@
import {
CallHandler,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
NestInterceptor,
ExecutionContext,
HttpStatus,
} from '@nestjs/common';
import { HttpException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(
catchError(err =>
throwError(new HttpException('Message', HttpStatus.BAD_GATEWAY)),
),
);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err =>
throwError(new HttpException('New message', HttpStatus.BAD_GATEWAY)),
),
);
}
}

View File

@@ -1,16 +1,20 @@
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return call$.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
}
}

View File

@@ -1,13 +1,15 @@
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(timeout(5000));
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(5000));
}
}

View File

@@ -1,4 +1,9 @@
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@@ -11,8 +16,8 @@ export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
call$: Observable<T>,
next: CallHandler<T>,
): Observable<Response<T>> {
return call$.pipe(map(data => ({ data })));
return next.handle().pipe(map(data => ({ data })));
}
}

View File

@@ -1,16 +1,20 @@
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return call$.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
}
}

View File

@@ -1,28 +1,28 @@
import {
CallHandler,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
NestInterceptor,
ExecutionContext,
HttpStatus,
} from '@nestjs/common';
import { HttpException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ExceptionInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(
catchError(err =>
throwError(
new HttpException(
'Exception interceptor message',
HttpStatus.BAD_GATEWAY,
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err =>
throwError(
new HttpException(
'Exception interceptor message',
HttpStatus.BAD_GATEWAY,
),
),
),
),
);
);
}
}

View File

@@ -1,16 +1,20 @@
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return call$.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
}
}

View File

@@ -1,13 +1,15 @@
import { Injectable, NestInterceptor, ExecutionContext } from '@nestjs/common';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(map(data => ({ data })));
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => ({ data })));
}
}

View File

@@ -1,9 +1,5 @@
{
"extends": "tsconfig.json",
"include": [
"**/*.spec.ts"
],
"exclude": [
"node_modules"
]
}
"include": ["**/*.spec.ts"],
"exclude": ["node_modules"]
}