Skip to content

Commit

Permalink
fix(common): Fix responses typings when a response is sent
Browse files Browse the repository at this point in the history
Closes: 742
  • Loading branch information
Romakita committed Feb 2, 2020
1 parent cb5f92c commit 7cb6de3
Show file tree
Hide file tree
Showing 36 changed files with 858 additions and 699 deletions.
17 changes: 5 additions & 12 deletions docs/docs/snippets/middlewares/custom-endpoint-decorator-status.ts
@@ -1,17 +1,10 @@
import {applyDecorators, StoreSet} from "@tsed/core";
import {IResponseOptions, UseAfter, mapReturnedResponse} from "@tsed/common";

export function Status(code: number, options: IResponseOptions = {}) {
const response = mapReturnedResponse(options);
import {UseAfter} from "@tsed/common";
import {applyDecorators} from "@tsed/core";

export function CustomStatus(code: number) {
return applyDecorators(
StoreSet("statusCode", code),
StoreSet("response", response),
StoreSet("responses", {[code]: response}),
UseAfter((request: any, response: any, next: any) => {
if (response.statusCode === 200) {
response.status(code);
}
UseAfter((req: any, res: any, next: any) => {
res.status(code);
next();
})
);
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/mvc/builders/ControllerBuilder.ts
Expand Up @@ -3,6 +3,7 @@ import {InjectorService} from "@tsed/di";
import * as Express from "express";
import {bindEndpointMiddleware} from "../components/bindEndpointMiddleware";
import {SendResponseMiddleware} from "../components/SendResponseMiddleware";
import {statusAndHeadersMiddleware} from "../components/statusAndHeadersMiddleware";
import {IPathMethod} from "../interfaces/IPathMethod";
import {ControllerProvider} from "../models/ControllerProvider";
import {EndpointMetadata} from "../models/EndpointMetadata";
Expand Down Expand Up @@ -86,6 +87,7 @@ export class ControllerBuilder {
.concat(beforeMiddlewares) // Endpoint before-middlewares
.concat(mldwrs) // Endpoint middlewares
.concat(endpoint) // Endpoint handler
.concat(statusAndHeadersMiddleware)
.concat(afterMiddlewares) // Endpoint after-middlewares
.filter((item: any) => !!item)
.map((middleware: any) => HandlerBuilder.from(middleware).build(injector));
Expand Down
19 changes: 19 additions & 0 deletions packages/common/src/mvc/components/statusAndHeadersMiddleware.ts
@@ -0,0 +1,19 @@
import * as Express from "express";

export function statusAndHeadersMiddleware(request: Express.Request, response: Express.Response, next: any) {
const {
statusCode,
response: {headers = {}}
} = request.ctx.endpoint;

if (response.statusCode === 200) {
// apply status only if the isn't already modified
response.status(statusCode);
}

// apply headers
Object.entries(headers).forEach(([key, schema]) => {
schema.value !== undefined && response.set(key, String(schema.value));
});
next();
}
2 changes: 0 additions & 2 deletions packages/common/src/mvc/decorators/index.ts
Expand Up @@ -49,6 +49,4 @@ export * from "./params/endpointInfo";

// utils
export * from "./utils/mapReturnedResponse";
export * from "./utils/mapHeaders";
export * from "./utils/mapReturnedResponse";
export * from "./utils/getStorableMetadata";
18 changes: 18 additions & 0 deletions packages/common/src/mvc/decorators/method/endpointFn.ts
@@ -0,0 +1,18 @@
import {DecoratorParameters, getDecoratorType, Type} from "@tsed/core";
import {EndpointMetadata} from "../../models/EndpointMetadata";
import {EndpointRegistry} from "../../registries/EndpointRegistry";

/**
*
* @param fn
* @decorator
*/
export function EndpointFn(fn: (endpoint: EndpointMetadata, parameters: DecoratorParameters) => void) {
return <T>(target: Type<any>, property: string, descriptor: TypedPropertyDescriptor<T>): TypedPropertyDescriptor<T> | void => {
if (getDecoratorType([target, property, descriptor]) === "method") {
fn(EndpointRegistry.get(target, property!), [target, property, descriptor]);

return descriptor;
}
};
}
51 changes: 36 additions & 15 deletions packages/common/src/mvc/decorators/method/header.ts
@@ -1,8 +1,33 @@
import {applyDecorators, StoreMerge} from "@tsed/core";
import {IResponseHeader} from "../../interfaces/IResponseHeader";
import {IHeadersOptions, IResponseHeaders} from "../../interfaces/IResponseHeaders";
import {mapHeaders} from "../utils/mapHeaders";
import {UseAfter} from "./useAfter";
import {deepMerge} from "@tsed/core";
import {IResponseHeader, IResponseHeaders} from "../../interfaces";
import {EndpointFn} from "./endpointFn";

export type IHeaderOptions = string | number | IResponseHeader;

export interface IHeadersOptions {
[key: string]: IHeaderOptions;
}

export function mapHeaders(headers: IHeadersOptions): IResponseHeaders {
return Object.keys(headers).reduce<IResponseHeaders>((newHeaders: IResponseHeaders, key: string, index: number, array: string[]) => {
const value: any = headers[key];
let type = typeof value;
let options: any = {
value
};

if (type === "object") {
options = value;
type = typeof options.value;
}

options.type = options.type || type;

newHeaders[key] = options;

return newHeaders;
}, {});
}

/**
* Sets the response’s HTTP header field to value. To set multiple fields at once, pass an object as the parameter.
Expand Down Expand Up @@ -53,19 +78,15 @@ import {UseAfter} from "./useAfter";
* @decorator
* @endpoint
*/
export function Header(headerName: string | number | IHeadersOptions, headerValue?: string | number | IResponseHeader): Function {
export function Header(headerName: string | number | IHeadersOptions, headerValue?: IHeaderOptions): Function {
if (headerValue !== undefined) {
headerName = {[headerName as string]: headerValue};
}
const headers: IResponseHeaders = mapHeaders(headerName as IHeadersOptions);

return applyDecorators(
StoreMerge("response", {headers}),
UseAfter((request: any, response: any, next: any) => {
Object.keys(headers).forEach(key => {
response.set(key, headers[key].value);
});
next();
})
);
return EndpointFn(endpoint => {
const {response} = endpoint;

response.headers = deepMerge(response.headers || {}, headers);
});
}
38 changes: 26 additions & 12 deletions packages/common/src/mvc/decorators/method/returnType.ts
@@ -1,31 +1,45 @@
import {getDecoratorType, Type} from "@tsed/core";
import {EndpointRegistry} from "../../registries/EndpointRegistry";
import {cleanObject, deepMerge} from "@tsed/core";
import {IResponseOptions} from "../../interfaces/IResponseOptions";
import {EndpointFn} from "./endpointFn";

const isSuccessStatus = (code: number | undefined) => code && 200 <= code && code < 300;

/**
* Define the returned type for the serialization.
*
* ```typescript
* @Controller('/')
* export class Ctrl {
*
* @Get('/')
* @ReturnType(User)
* get(): Promise<User> { }
* @ReturnType(200, {type: User, collectionType: Map})
* get(): Promise<Map<User>> { }
* }
*
* ```
*
* @returns {Function}
* @param type
* @param response
* @decorator
* @endpoint
*/
export function ReturnType(type: Type<any> | any): Function {
return <T>(target: Type<any>, targetKey?: string, descriptor?: TypedPropertyDescriptor<T>): TypedPropertyDescriptor<T> | void => {
if (getDecoratorType([target, targetKey, descriptor]) === "method") {
EndpointRegistry.get(target, targetKey!).type = type;
export function ReturnType(response: Partial<IResponseOptions> = {}): Function {
return EndpointFn(endpoint => {
const {responses, statusCode} = endpoint;
const code = response.code || statusCode; // implicit

return descriptor;
if (isSuccessStatus(response.code)) {
const {response} = endpoint;
responses.delete(statusCode);
endpoint.statusCode = code;
endpoint.responses.set(code, response);
}
};

response = {
code,
description: "",
...deepMerge(endpoint.get(code), cleanObject(response))
};

endpoint.responses.set(response.code!, response as IResponseOptions);
});
}
25 changes: 10 additions & 15 deletions packages/common/src/mvc/decorators/method/status.ts
@@ -1,6 +1,6 @@
import {applyDecorators, StoreSet, StoreMerge} from "@tsed/core";
import {IResponseOptions} from "../../interfaces/IResponseOptions";
import {applyDecorators, StoreMerge, StoreSet} from "@tsed/core";
import {mapReturnedResponse} from "../utils/mapReturnedResponse";
import {ReturnType} from "./returnType";
import {UseAfter} from "./useAfter";

/**
Expand Down Expand Up @@ -46,18 +46,13 @@ import {UseAfter} from "./useAfter";
* @decorator
* @endpoint
*/
export function Status(code: number, options: IResponseOptions = {}) {
const response = mapReturnedResponse(options);
export function Status(code: number, options: TsED.ResponseOptions = {description: ""}) {
const {use, collection} = options as any;

return applyDecorators(
StoreSet("statusCode", code),
StoreMerge("response", response),
StoreMerge("responses", {[code]: response}),
UseAfter((request: any, response: any, next: any) => {
if (response.statusCode === 200) {
response.status(code);
}
next();
})
);
return ReturnType({
...options,
code,
type: options.type || use,
collectionType: options.collectionType || collection
});
}
21 changes: 0 additions & 21 deletions packages/common/src/mvc/decorators/utils/mapHeaders.ts

This file was deleted.

@@ -1,5 +1,5 @@
import {isObject, isPrimitive} from "@tsed/core";
import {IParamOptions} from "../../../interfaces/IParamOptions";
import {IParamOptions} from "../../interfaces/IParamOptions";

export function mapParamsOptions(args: any[]): IParamOptions<any> {
if (args.length === 1) {
Expand Down
4 changes: 1 addition & 3 deletions packages/common/src/mvc/interfaces/IResponseError.ts
@@ -1,10 +1,8 @@
import {IResponseHeaders} from "./IResponseHeaders";

/**
* Interface can be implemented to customize the error sent to the client.
*/
export interface IResponseError extends Error {
errors?: any[];
origin?: Error;
headers?: IResponseHeaders;
headers?: {};
}
11 changes: 0 additions & 11 deletions packages/common/src/mvc/interfaces/IResponseHeader.ts

This file was deleted.

9 changes: 0 additions & 9 deletions packages/common/src/mvc/interfaces/IResponseHeaders.ts

This file was deleted.

27 changes: 18 additions & 9 deletions packages/common/src/mvc/interfaces/IResponseOptions.ts
@@ -1,17 +1,26 @@
import {IMetadataType} from "@tsed/core";
import {IResponseHeaders} from "./IResponseHeaders";

declare global {
namespace TsED {
interface ResponseOptions {}
interface ResponseHeader {
value?: string | number;
}

interface ResponseHeaders {
[key: string]: ResponseHeader;
}

interface ResponseOptions extends IMetadataType {
code?: number;
headers?: {
[key: string]: ResponseHeader;
};
}
}
}

export interface IResponseOptions extends IMetadataType, TsED.ResponseOptions {
code?: number;
headers?: {
[exampleName: string]: IResponseHeaders;
};
export interface IResponseOptions extends TsED.ResponseOptions {}

[key: string]: any;
}
export interface IResponseHeaders extends TsED.ResponseHeaders {}

export interface IResponseHeader extends TsED.ResponseHeader {}
2 changes: 0 additions & 2 deletions packages/common/src/mvc/interfaces/index.ts
Expand Up @@ -6,8 +6,6 @@ export * from "./IMiddlewareError";
export * from "./IControllerMiddlewares";
export * from "./PathParamsType";
export * from "./IResponseOptions";
export * from "./IResponseHeader";
export * from "./IResponseHeaders";
export * from "./IResponseError";
export * from "./IParamOptions";
export * from "./HandlerType";
Expand Down

0 comments on commit 7cb6de3

Please sign in to comment.