From c3e2d5be982f0164496031abc7fc0e5c94e386ce Mon Sep 17 00:00:00 2001 From: Tolga Paksoy Date: Sun, 18 Feb 2024 17:54:19 +0100 Subject: [PATCH] feat(core,common): add `@RawBody()` decorator --- .../decorators/http/route-params.decorator.ts | 47 +++++++++++++++++++ .../common/enums/route-paramtypes.enum.ts | 1 + packages/core/router/route-params-factory.ts | 2 + .../core/router/router-execution-context.ts | 1 + .../test/router/route-params-factory.spec.ts | 11 +++++ .../router/router-execution-context.spec.ts | 16 ++++++- 6 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/common/decorators/http/route-params.decorator.ts b/packages/common/decorators/http/route-params.decorator.ts index 77885591893..b14e63cee9d 100644 --- a/packages/common/decorators/http/route-params.decorator.ts +++ b/packages/common/decorators/http/route-params.decorator.ts @@ -508,6 +508,53 @@ export function Body( ); } +/** + * Route handler parameter decorator. Extracts the `rawBody` Buffer + * property from the `req` object and populates the decorated parameter with that value. + * + * For example: + * ```typescript + * async create(@RawBody() rawBody: Buffer | undefined) + * ``` + * + * @see [Request object](https://docs.nestjs.com/controllers#request-object) + * @see [Raw body](https://docs.nestjs.com/faq/raw-body) + * + * @publicApi + */ +export function RawBody(): ParameterDecorator; + +/** + * Route handler parameter decorator. Extracts the `rawBody` Buffer + * property from the `req` object and populates the decorated parameter with that value. + * Also applies pipes to the bound rawBody parameter. + * + * For example: + * ```typescript + * async create(@RawBody(new ValidationPipe()) rawBody: Buffer) + * ``` + * + * @param pipes one or more pipes - either instances or classes - to apply to + * the bound body parameter. + * + * @see [Request object](https://docs.nestjs.com/controllers#request-object) + * @see [Raw body](https://docs.nestjs.com/faq/raw-body) + * @see [Working with pipes](https://docs.nestjs.com/custom-decorators#working-with-pipes) + * + * @publicApi + */ +export function RawBody( + ...pipes: ( + | Type> + | PipeTransform + )[] +): ParameterDecorator { + return createPipesRouteParamDecorator(RouteParamtypes.RAW_BODY)( + undefined, + ...pipes, + ); +} + /** * Route handler parameter decorator. Extracts the `params` * property from the `req` object and populates the decorated diff --git a/packages/common/enums/route-paramtypes.enum.ts b/packages/common/enums/route-paramtypes.enum.ts index 738532aef29..8acda4d7692 100644 --- a/packages/common/enums/route-paramtypes.enum.ts +++ b/packages/common/enums/route-paramtypes.enum.ts @@ -3,6 +3,7 @@ export enum RouteParamtypes { RESPONSE, NEXT, BODY, + RAW_BODY, QUERY, PARAM, HEADERS, diff --git a/packages/core/router/route-params-factory.ts b/packages/core/router/route-params-factory.ts index 3192af6d836..688a26c7366 100644 --- a/packages/core/router/route-params-factory.ts +++ b/packages/core/router/route-params-factory.ts @@ -20,6 +20,8 @@ export class RouteParamsFactory implements IRouteParamsFactory { return res as any; case RouteParamtypes.BODY: return data && req.body ? req.body[data] : req.body; + case RouteParamtypes.RAW_BODY: + return req.rawBody; case RouteParamtypes.PARAM: return data ? req.params[data] : req.params; case RouteParamtypes.HOST: diff --git a/packages/core/router/router-execution-context.ts b/packages/core/router/router-execution-context.ts index 9e4fa897b18..c91207d897f 100644 --- a/packages/core/router/router-execution-context.ts +++ b/packages/core/router/router-execution-context.ts @@ -345,6 +345,7 @@ export class RouterExecutionContext { public isPipeable(type: number | string): boolean { return ( type === RouteParamtypes.BODY || + type === RouteParamtypes.RAW_BODY || type === RouteParamtypes.QUERY || type === RouteParamtypes.PARAM || type === RouteParamtypes.FILE || diff --git a/packages/core/test/router/route-params-factory.spec.ts b/packages/core/test/router/route-params-factory.spec.ts index 39dd2257464..be4075abf1a 100644 --- a/packages/core/test/router/route-params-factory.spec.ts +++ b/packages/core/test/router/route-params-factory.spec.ts @@ -13,6 +13,7 @@ describe('RouteParamsFactory', () => { const req = { ip: 'ip', session: null, + rawBody: Buffer.from('{"foo":"bar"}'), body: { foo: 'bar', }, @@ -67,6 +68,16 @@ describe('RouteParamsFactory', () => { ).to.be.eql(req.body); }); }); + describe(`RouteParamtypes.RAW_BODY`, () => { + it('should return rawBody buffer', () => { + expect( + (factory as any).exchangeKeyForValue( + RouteParamtypes.RAW_BODY, + ...args, + ), + ).to.be.eql(req.rawBody); + }); + }); describe(`RouteParamtypes.HEADERS`, () => { it('should return headers object', () => { expect( diff --git a/packages/core/test/router/router-execution-context.spec.ts b/packages/core/test/router/router-execution-context.spec.ts index b531ee972fa..f8e032d57cb 100644 --- a/packages/core/test/router/router-execution-context.spec.ts +++ b/packages/core/test/router/router-execution-context.spec.ts @@ -210,7 +210,7 @@ describe('RouterExecutionContext', () => { beforeEach(() => { consumerApplySpy = sinon.spy(consumer, 'apply'); }); - describe('when paramtype is query, body or param', () => { + describe('when paramtype is query, body, rawBody or param', () => { it('should call "consumer.apply" with expected arguments', () => { contextCreator.getParamValue( value, @@ -238,6 +238,19 @@ describe('RouterExecutionContext', () => { ), ).to.be.true; + contextCreator.getParamValue( + value, + { metatype, type: RouteParamtypes.RAW_BODY, data: null }, + transforms, + ); + expect( + consumerApplySpy.calledWith( + value, + { metatype, type: RouteParamtypes.RAW_BODY, data: null }, + transforms, + ), + ).to.be.true; + contextCreator.getParamValue( value, { metatype, type: RouteParamtypes.PARAM, data: null }, @@ -261,6 +274,7 @@ describe('RouterExecutionContext', () => { }); it('otherwise', () => { expect(contextCreator.isPipeable(RouteParamtypes.BODY)).to.be.true; + expect(contextCreator.isPipeable(RouteParamtypes.RAW_BODY)).to.be.true; expect(contextCreator.isPipeable(RouteParamtypes.QUERY)).to.be.true; expect(contextCreator.isPipeable(RouteParamtypes.PARAM)).to.be.true; expect(contextCreator.isPipeable(RouteParamtypes.FILE)).to.be.true;