Skip to content

Commit

Permalink
add custom headers, allow response object
Browse files Browse the repository at this point in the history
  • Loading branch information
Insidexa committed Apr 12, 2020
1 parent fbb9a0b commit e30f01e
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 49 deletions.
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
NestJS JSON RPC package - [nestjs-json-rpc](https://www.npmjs.com/package/@jashkasoft/nestjs-json-rpc) npm package
## NestJS JSON RPC package - [nestjs-json-rpc](https://www.npmjs.com/package/@jashkasoft/nestjs-json-rpc) npm package

![Build](https://github.com/Insidexa/nestjs-rpc/workflows/Build/badge.svg)

Expand Down Expand Up @@ -54,14 +54,11 @@ Fields description
| `id` | `@RpcId()` | get client operation id | false | if not send - response not send, RPC notification |
However, you don't have an access to the native response object in
Exception filters, Interceptors and Guards (as in the HTTP app).
Because RPCModule in rpc batch request collect responses from handlers
and if you using response you override headers or send response in some handlers.
Maybe, you can receive errors, for example `headers already sent`.
##### Changelog versions:
`7.3.0`
- allow response object
- add custom headers
`7.2.0`
- add injection scopes ( REQUEST / TRANSIENT ) to JSON RPC handlers
- add logging register handlers
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jashkasoft/nestjs-json-rpc",
"version": "7.2.0",
"version": "7.3.0",
"description": "JSON RPC module for NestJS framework",
"author": "jashka",
"main": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion samples/express-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@jashkasoft/nestjs-json-rpc": "7.2.0",
"@jashkasoft/nestjs-json-rpc": "7.3.0",
"@nestjs/common": "7.0.7",
"@nestjs/core": "7.0.7",
"@nestjs/platform-express": "7.0.7",
Expand Down
2 changes: 1 addition & 1 deletion samples/fastify-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@jashkasoft/nestjs-json-rpc": "7.2.0",
"@jashkasoft/nestjs-json-rpc": "7.3.0",
"@nestjs/common": "7.0.7",
"@nestjs/core": "7.0.7",
"@nestjs/platform-express": "7.0.7",
Expand Down
2 changes: 1 addition & 1 deletion samples/request-scoped/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@jashkasoft/nestjs-json-rpc": "7.2.0",
"@jashkasoft/nestjs-json-rpc": "7.3.0",
"@nestjs/common": "7.0.7",
"@nestjs/core": "7.0.7",
"@nestjs/platform-express": "7.0.7",
Expand Down
49 changes: 31 additions & 18 deletions src/context/json-rpc-context-creator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContextType, Controller, PipeTransform, Type } from '@nestjs/common/interfaces';
import { ContextType, Controller, HttpServer, PipeTransform, Type } from '@nestjs/common/interfaces';
import { GuardsConsumer } from '@nestjs/core/guards/guards-consumer';
import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
Expand All @@ -10,26 +10,22 @@ import { HandlerMetadata, HandlerMetadataStorage } from '@nestjs/core/helpers/ha
import { ContextUtils } from '@nestjs/core/helpers/context-utils';
import { ForbiddenException, RouteParamMetadata } from '@nestjs/common';
import { STATIC_CONTEXT } from '@nestjs/core/injector/constants';
import { CUSTOM_ROUTE_AGRS_METADATA, ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
import { CUSTOM_ROUTE_AGRS_METADATA, HEADERS_METADATA, ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum';
import { isFunction, isString } from '@nestjs/common/utils/shared.utils';
import { isEmpty, isFunction, isString } from '@nestjs/common/utils/shared.utils';
import { FORBIDDEN_MESSAGE } from '@nestjs/core/guards/constants';
import { ParamProperties } from '@nestjs/core/router/router-execution-context';
import { Fn } from '../types';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { IRpcHandler } from '../interfaces';

/**
* Response set to null because
* RPCModule in rpc batch request collect responses from handlers
* and if you using response you override headers or send response in some handlers.
* Maybe, you can receive errors, for example `headers already sent`.
*/
const response = null;
import { JsonRpcResponseController } from './json-rpc-response-controller';
import { CustomHeader } from '@nestjs/core/router/router-response-controller';

export class JsonRpcContextCreator {
private readonly handlerMetadataStorage = new HandlerMetadataStorage();
private readonly contextUtils = new ContextUtils();
private readonly responseController: JsonRpcResponseController;
private readonly DEFAULT_RESPONSE_CODE = 200;

constructor(
private readonly paramsFactory: IRouteParamsFactory,
Expand All @@ -39,7 +35,9 @@ export class JsonRpcContextCreator {
private readonly guardsConsumer: GuardsConsumer,
private readonly interceptorsContextCreator: InterceptorsContextCreator,
private readonly interceptorsConsumer: InterceptorsConsumer,
private readonly applicationRef: HttpServer,
) {
this.responseController = new JsonRpcResponseController(applicationRef);
}

public create<TContext extends string = ContextType>(
Expand All @@ -56,6 +54,9 @@ export class JsonRpcContextCreator {
fnHandleResponse,
paramtypes,
getParamsMetadata,
responseHeaders,
hasCustomHeaders,
httpStatusCode,
} = this.getMetadata(instance, callback, methodName, contextType);

const paramsOptions = this.contextUtils.mergeParamsMetatypes(
Expand Down Expand Up @@ -106,12 +107,17 @@ export class JsonRpcContextCreator {
) => {
const args = this.contextUtils.createNullArray(argsLength);
if (fnCanActivate) {
await fnCanActivate([ req, response ]);
await fnCanActivate([ req, res, next ]);
}

this.responseController.setStatus(res, httpStatusCode);
if (hasCustomHeaders === true) {
this.responseController.setHeaders(res, responseHeaders);
}

const result = await this.interceptorsConsumer.intercept(
interceptors,
[ req, response ],
[ req, res, next ],
instance,
callback,
handler(args, req, res, next),
Expand Down Expand Up @@ -163,15 +169,16 @@ export class JsonRpcContextCreator {
);

const fnHandleResponse = this.createHandleResponseFn();

const responseHeaders = this.reflectResponseHeaders(callback);
const hasCustomHeaders = !isEmpty(responseHeaders);
const handlerMetadata: HandlerMetadata = {
argsLength,
fnHandleResponse,
paramtypes,
getParamsMetadata,
httpStatusCode: 200,
hasCustomHeaders: false,
responseHeaders: [],
httpStatusCode: this.DEFAULT_RESPONSE_CODE,
hasCustomHeaders,
responseHeaders,
};
this.handlerMetadataStorage.set(instance, methodName, handlerMetadata);
return handlerMetadata;
Expand Down Expand Up @@ -208,7 +215,7 @@ export class JsonRpcContextCreator {
) =>
this.paramsFactory.exchangeKeyForValue(numericType, data, {
req,
res: response,
res,
next,
});
return { index, extractValue, type: numericType, data, pipes };
Expand Down Expand Up @@ -312,4 +319,10 @@ export class JsonRpcContextCreator {
}
return resultOrDeffered;
}

public reflectResponseHeaders(
callback: (...args: unknown[]) => unknown,
): CustomHeader[] {
return Reflect.getMetadata(HEADERS_METADATA, callback) || [];
}
}
2 changes: 1 addition & 1 deletion src/context/json-rpc-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class JsonRpcProxy {
try {
return await targetCallback(req, res, next);
} catch (e) {
const host = new ExecutionContextHost([ req, null ]);
const host = new ExecutionContextHost([ req, res, next ]);
exceptionsHandler.next(e, host);

return e;
Expand Down
25 changes: 25 additions & 0 deletions src/context/json-rpc-response-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HttpServer } from '@nestjs/common';
import { CustomHeader } from '@nestjs/core/router/router-response-controller';

export class JsonRpcResponseController {
constructor(
private applicationRef: HttpServer,
) {
}

public setHeaders<TResponse = unknown>(
response: TResponse,
headers: CustomHeader[],
) {
headers.forEach(({ name, value }) =>
this.applicationRef.setHeader(response, name, value),
);
}

public setStatus<TResponse = unknown>(
response: TResponse,
statusCode: number,
) {
this.applicationRef.status(response, statusCode);
}
}
4 changes: 3 additions & 1 deletion src/rpc-callback-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class RpcCallbackProxy {
private readonly container: NestContainer,
private readonly injector: Injector,
) {
const httpAdapterRef = container.getHttpAdapterRef();
this.executionContextCreator = new JsonRpcContextCreator(
new RouteParamsFactory(),
new PipesContextCreator(container, this.config),
Expand All @@ -38,11 +39,12 @@ export class RpcCallbackProxy {
new GuardsConsumer(),
new InterceptorsContextCreator(container, this.config),
new InterceptorsConsumer(),
httpAdapterRef,
);
this.exceptionsFilter = new RouterExceptionFilters(
container,
this.config,
container.getHttpAdapterRef(),
httpAdapterRef,
);
}

Expand Down
61 changes: 61 additions & 0 deletions test/custom-headers.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { JsonRpcModule, RpcHandler, RpcId, RpcMethod, RpcPayload, RpcVersion, IRpcHandler } from '../src';
import { Header } from '@nestjs/common';

@RpcHandler({
method: 'test',
})
export class TestHandler implements IRpcHandler<any> {
@Header('Handler-Name', TestHandler.name)
public async invoke(
@RpcPayload() payload: any,
@RpcVersion() version: string,
@RpcMethod() method: string,
@RpcId() id: any,
) {
return payload;
}
}

describe('Test json rpc custom headers', () => {
let app;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
JsonRpcModule.forRoot({
path: '/rpc',
}),
],
providers: [TestHandler],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

describe('positive', () => {
it('has single custom header', async () => {
const { header } = await request(app.getHttpServer())
.post('/rpc')
.send({ jsonrpc: '2.0', id: 1, params: { field: 1 }, method: 'test' })
.expect(200);

const handlerNameHeader = header['handler-name'];

expect(handlerNameHeader).not.toBeUndefined();
});

it('custom header contains expected value', async () => {
const { header } = await request(app.getHttpServer())
.post('/rpc')
.send({ jsonrpc: '2.0', id: 1, params: { field: 1 }, method: 'test' })
.expect(200);

const handlerNameHeader = header['handler-name'];

expect(handlerNameHeader).toEqual(TestHandler.name);
});
});
});
17 changes: 15 additions & 2 deletions test/app.e2e-spec.ts → test/specification.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { TestHandler } from './test.handler';
import { JsonRpcModule } from '../src';
import { JsonRpcModule, RpcHandler, RpcId, RpcMethod, RpcPayload, RpcVersion, IRpcHandler } from '../src';

@RpcHandler({
method: 'test',
})
export class TestHandler implements IRpcHandler<any> {
public async invoke(
@RpcPayload() payload: any,
@RpcVersion() version: string,
@RpcMethod() method: string,
@RpcId() id: any,
) {
return payload;
}
}

describe('Test json rpc specification', () => {
let app;
Expand Down
15 changes: 0 additions & 15 deletions test/test.handler.ts

This file was deleted.

0 comments on commit e30f01e

Please sign in to comment.