Skip to content

Commit a814974

Browse files
Merge branch 'feature-add-redirect-decorator' of https://github.com/johnbiundo/nest into johnbiundo-feature-add-redirect-decorator
2 parents e7a0498 + f4d3c03 commit a814974

12 files changed

Lines changed: 122 additions & 13 deletions

File tree

packages/common/decorators/http/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './route-params.decorator';
33
export * from './http-code.decorator';
44
export * from './create-route-param-metadata.decorator';
55
export * from './render.decorator';
6-
export * from './header.decorator';
6+
export * from './header.decorator';
7+
export * from './redirect.decorator';

packages/common/decorators/http/redirect.decorator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import { REDIRECT_METADATA } from '../../constants';
33
/**
44
* Redirects request.
55
*/
6-
export function Redirect(url: string): MethodDecorator {
6+
export function Redirect(url: string, statusCode?: number): MethodDecorator {
77
return (target: object, key, descriptor) => {
8-
Reflect.defineMetadata(REDIRECT_METADATA, url, descriptor.value);
8+
Reflect.defineMetadata(
9+
REDIRECT_METADATA,
10+
{ statusCode, url },
11+
descriptor.value,
12+
);
913
return descriptor;
1014
};
1115
}

packages/common/interfaces/http/http-server.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface HttpServer<TRequest = any, TResponse = any> {
4545
reply(response: any, body: any, statusCode?: number): any;
4646
status(response: any, statusCode: number): any;
4747
render(response: any, view: string, options: any): any;
48+
redirect(response: any, statusCode: number, url: string): any;
4849
setHeader(response: any, name: string, value: string): any;
4950
setErrorHandler?(handler: Function): any;
5051
setNotFoundHandler?(handler: Function): any;
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { expect } from 'chai';
22
import { Redirect } from '../../decorators/http/redirect.decorator';
33
import { REDIRECT_METADATA } from '../../constants';
4+
import { HttpStatus } from '@nestjs/common';
45

56
describe('@Redirect', () => {
67
const url = 'http://test.com';
8+
const statusCode = HttpStatus.FOUND;
79

810
class Test {
9-
@Redirect(url)
11+
@Redirect(url, statusCode)
1012
public static test() {}
1113
}
1214

13-
it('should enhance method with expected template string', () => {
15+
it('should enhance method with expected redirect url string', () => {
1416
const metadata = Reflect.getMetadata(REDIRECT_METADATA, Test.test);
15-
expect(metadata).to.be.eql(url);
17+
expect(metadata.url).to.be.eql(url);
18+
});
19+
20+
it('should enhance method with expected response code', () => {
21+
const metadata = Reflect.getMetadata(REDIRECT_METADATA, Test.test);
22+
expect(metadata.statusCode).to.be.eql(statusCode);
1623
});
1724
});

packages/core/adapters/http-adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export abstract class AbstractHttpAdapter<
8585
abstract status(response, statusCode: number);
8686
abstract reply(response, body: any, statusCode?: number);
8787
abstract render(response, view: string, options: any);
88+
abstract redirect(response, statusCode: number, url: string);
8889
abstract setErrorHandler(handler: Function);
8990
abstract setNotFoundHandler(handler: Function);
9091
abstract setHeader(response, name: string, value: string);

packages/core/helpers/handler-metadata-storage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Controller } from '@nestjs/common/interfaces';
22
import * as hash from 'object-hash';
33
import { ContextId } from './../injector/instance-wrapper';
44
import { ParamProperties } from './context-utils';
5+
import { RedirectResponse } from '../router/router-response-controller';
56

67
export const HANDLER_METADATA_SYMBOL = Symbol.for('handler_metadata:cache');
78

@@ -11,6 +12,7 @@ export interface HandlerMetadata {
1112
httpStatusCode: number;
1213
responseHeaders: any[];
1314
hasCustomHeaders: boolean;
15+
httpRedirectResponse: RedirectResponse;
1416
getParamsMetadata: (
1517
moduleKey: string,
1618
contextId?: ContextId,

packages/core/router/router-execution-context.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
HTTP_CODE_METADATA,
1212
RENDER_METADATA,
1313
ROUTE_ARGS_METADATA,
14+
REDIRECT_METADATA,
1415
} from '@nestjs/common/constants';
1516
import { RouteParamsMetadata } from '@nestjs/common/decorators';
1617
import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum';
@@ -36,6 +37,7 @@ import { PipesContextCreator } from '../pipes/pipes-context-creator';
3637
import { IRouteParamsFactory } from './interfaces/route-params-factory.interface';
3738
import {
3839
CustomHeader,
40+
RedirectResponse,
3941
RouterResponseController,
4042
} from './router-response-controller';
4143

@@ -86,6 +88,7 @@ export class RouterExecutionContext {
8688
httpStatusCode,
8789
responseHeaders,
8890
hasCustomHeaders,
91+
httpRedirectResponse,
8992
} = this.getMetadata(instance, callback, methodName, module, requestMethod);
9093

9194
const paramsOptions = this.contextUtils.mergeParamsMetatypes(
@@ -205,19 +208,26 @@ export class RouterExecutionContext {
205208
const responseHeaders = this.reflectResponseHeaders(callback);
206209
const hasCustomHeaders = !isEmpty(responseHeaders);
207210

211+
const httpRedirectResponse = this.reflectRedirect(callback);
212+
208213
const handlerMetadata: HandlerMetadata = {
209214
argsLength,
210215
fnHandleResponse,
211216
paramtypes,
212217
getParamsMetadata,
213218
httpStatusCode,
214219
hasCustomHeaders,
220+
httpRedirectResponse,
215221
responseHeaders,
216222
};
217223
this.handlerMetadataStorage.set(instance, methodName, handlerMetadata);
218224
return handlerMetadata;
219225
}
220226

227+
public reflectRedirect(callback: (...args: any[]) => any): RedirectResponse {
228+
return Reflect.getMetadata(REDIRECT_METADATA, callback);
229+
}
230+
221231
public reflectHttpStatusCode(callback: (...args: any[]) => any): number {
222232
return Reflect.getMetadata(HTTP_CODE_METADATA, callback);
223233
}
@@ -360,6 +370,13 @@ export class RouterExecutionContext {
360370
isResponseHandled: boolean,
361371
httpStatusCode?: number,
362372
) {
373+
const redirectResponse = this.reflectRedirect(callback);
374+
if (redirectResponse && redirectResponse.url) {
375+
return async <TResult, TResponse>(result: TResult, res: TResponse) => {
376+
await this.responseController.redirect(result, res, redirectResponse);
377+
};
378+
}
379+
363380
const renderTemplate = this.reflectRenderTemplate(callback);
364381

365382
if (renderTemplate) {

packages/core/router/router-response-controller.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export interface CustomHeader {
66
value: string;
77
}
88

9+
export interface RedirectResponse {
10+
url: string;
11+
statusCode: number;
12+
}
913
export class RouterResponseController {
1014
constructor(private readonly applicationRef: HttpServer) {}
1115

@@ -17,20 +21,36 @@ export class RouterResponseController {
1721
return this.applicationRef.reply(response, result, httpStatusCode);
1822
}
1923

24+
public async redirect<TInput = any, TResponse = any>(
25+
resultOrDeferred: TInput,
26+
response: TResponse,
27+
redirectResponse: RedirectResponse,
28+
) {
29+
const result = await this.transformToResult(resultOrDeferred);
30+
const statusCode =
31+
result && result.statusCode
32+
? result.statusCode
33+
: redirectResponse.statusCode
34+
? redirectResponse.statusCode
35+
: HttpStatus.FOUND;
36+
const url = result && result.url ? result.url : redirectResponse.url;
37+
this.applicationRef.redirect(response, statusCode, url);
38+
}
39+
2040
public async render<TInput = any, TResponse = any>(
21-
resultOrDeffered: TInput,
41+
resultOrDeferred: TInput,
2242
response: TResponse,
2343
template: string,
2444
) {
25-
const result = await this.transformToResult(resultOrDeffered);
45+
const result = await this.transformToResult(resultOrDeferred);
2646
this.applicationRef.render(response, template, result);
2747
}
2848

29-
public async transformToResult(resultOrDeffered: any) {
30-
if (resultOrDeffered && isFunction(resultOrDeffered.subscribe)) {
31-
return resultOrDeffered.toPromise();
49+
public async transformToResult(resultOrDeferred: any) {
50+
if (resultOrDeferred && isFunction(resultOrDeferred.subscribe)) {
51+
return resultOrDeferred.toPromise();
3252
}
33-
return resultOrDeffered;
53+
return resultOrDeferred;
3454
}
3555

3656
public getStatusByMethod(requestMethod: RequestMethod): number {

packages/core/test/router/router-execution-context.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ describe('RouterExecutionContext', () => {
298298
const value = 'test';
299299
const response = { render: sinon.spy() };
300300

301+
sinon.stub(contextCreator, 'reflectRedirect').returns(undefined);
301302
sinon.stub(contextCreator, 'reflectResponseHeaders').returns([]);
302303
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(template);
303304

@@ -312,6 +313,7 @@ describe('RouterExecutionContext', () => {
312313
const result = Promise.resolve('test');
313314
const response = { render: sinon.spy() };
314315

316+
sinon.stub(contextCreator, 'reflectRedirect').returns(undefined);
315317
sinon.stub(contextCreator, 'reflectResponseHeaders').returns([]);
316318
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
317319

@@ -321,5 +323,49 @@ describe('RouterExecutionContext', () => {
321323
expect(response.render.called).to.be.false;
322324
});
323325
});
326+
describe('when "redirectResponse" is present', () => {
327+
beforeEach(() => {
328+
sinon
329+
.stub(adapter, 'redirect')
330+
.callsFake((response, statusCode: number, url: string) => {
331+
return response.redirect(statusCode, url);
332+
});
333+
});
334+
it('should call "res.redirect()" with expected args', async () => {
335+
const redirectResponse = {
336+
url: 'http://test.com',
337+
statusCode: 302,
338+
};
339+
const response = { redirect: sinon.spy() };
340+
341+
sinon.stub(contextCreator, 'reflectRedirect').returns(redirectResponse);
342+
343+
const handler = contextCreator.createHandleResponseFn(null, true, 200);
344+
await handler(redirectResponse, response);
345+
346+
expect(
347+
response.redirect.calledWith(
348+
redirectResponse.statusCode,
349+
redirectResponse.url,
350+
),
351+
).to.be.true;
352+
});
353+
});
354+
355+
describe('when "redirectResponse" is undefined', () => {
356+
it('should not call "res.render()"', () => {
357+
const result = Promise.resolve('test');
358+
const response = { redirect: sinon.spy() };
359+
360+
sinon.stub(contextCreator, 'reflectRedirect').returns(undefined);
361+
sinon.stub(contextCreator, 'reflectResponseHeaders').returns([]);
362+
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
363+
364+
const handler = contextCreator.createHandleResponseFn(null, true, 200);
365+
handler(result, response);
366+
367+
expect(response.redirect.called).to.be.false;
368+
});
369+
});
324370
});
325371
});

packages/core/test/utils/noop-adapter.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class NoopHttpAdapter extends AbstractHttpAdapter {
1414
reply(response: any, body: any): any {}
1515
status(response: any, statusCode: number): any {}
1616
render(response: any, view: string, options: any): any {}
17+
redirect(response: any, statusCode: number, url: string) {}
1718
setErrorHandler(handler: Function): any {}
1819
setNotFoundHandler(handler: Function): any {}
1920
setHeader(response: any, name: string, value: string): any {}

0 commit comments

Comments
 (0)