Repository: lukeautry/tsoa (ts)

Issue:
Ability to handle streams returned from controller
I have a use-case where I want my controller to pipe streams through to the response object (eg. proxying from s3). This isn't possible to be encoded currently, but the changes required seem pretty minimal. 

## Sorting

* **I'm submitting a ...**
  - [x] feature request

* I confirm that I
  - [x] used the [search](https://github.com/lukeautry/tsoa/search?type=Issues) to make sure that a similar issue hasn't already been submit

## Expected Behavior

- Controllers can return a `Readable` stream object instead of a `Buffer`
- The swagger spec should be the same as if you returned a buffer directly

## Possible Solution

- Add check to `promiseHandler` in route templates that pipes the response rather than sending it via `json`
- Swagger generator should be aware of the `Readable` type and output the same return type as a `Buffer`

## Context (Environment)
Version of the library: 2.5.10
Version of NodeJS: 12
* Confirm you were using yarn not npm: [x]

## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->

## Breaking change?

Not a breaking change.

Pull request description:
Add ability to handle streams returned from controller
I have a use-case where I want my controller to pipe streams through to the response object (eg. proxying from s3). This isn't possible to be encoded currently, but the changes required seem pretty minimal. 

My assumptions:

- The swagger spec should be the same as if you returned a buffer directly.
- The route template should pipe the response rather than sending it via `json`

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/lukeautry/tsoa/tree/master/docs/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/lukeautry/tsoa/pulls) for the same update/change?
* [x] Have you written unit tests?
* [x] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [x] This PR is associated with an existing issue?

Resolves #551

### If this is a new feature submission:

* [x] Has the issue had a maintainer respond to the issue and clarify that the feature is something that aligns with the [goals](https://github.com/lukeautry/tsoa#goal) and [philosophy](https://github.com/lukeautry/tsoa#philosophy) of the project?

**Potential Problems With The Approach**

- I couldn't find any relevant tests for controllers using buffers, as the structure here should be quite similar I imagine
- I'm not experienced with Koa or Hapi so wasn't sure how to integrate the `data.pipe(res)` pattern that I used in the express routes template

**Test plan**

Some guidance here would be great.

Patch:
diff --git a/package.json b/package.json
index baaa5b51..bebe6840 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "tsoa",
   "description": "Build swagger-compliant REST APIs using TypeScript and Node",
-  "version": "2.5.10",
+  "version": "2.5.11",
   "main": "./dist/index.js",
   "typings": "./dist/index.d.ts",
   "keywords": [
diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts
index 080bcf5c..ad474628 100644
--- a/src/metadataGeneration/typeResolver.ts
+++ b/src/metadataGeneration/typeResolver.ts
@@ -156,6 +156,11 @@ export class TypeResolver {
         return bufferMetaType;
       }
 
+      if (typeReference.typeName.text === 'Readable') {
+        const streamMetaType: Tsoa.BufferType = { dataType: 'buffer' };
+        return streamMetaType;
+      }
+
       if (typeReference.typeName.text === 'Array' && typeReference.typeArguments && typeReference.typeArguments.length === 1) {
         const arrayMetaType: Tsoa.ArrayType = {
           dataType: 'array',
diff --git a/src/routeGeneration/templates/express.hbs b/src/routeGeneration/templates/express.hbs
index 65e2389a..63f8a1f9 100644
--- a/src/routeGeneration/templates/express.hbs
+++ b/src/routeGeneration/templates/express.hbs
@@ -72,9 +72,9 @@ export function RegisterRoutes(app: express.Express) {
             }
 
             {{#if ../../iocModule}}
-            const controller = iocContainer.get<{{../name}}>({{../name}});
+            const controller: any = iocContainer.get<{{../name}}>({{../name}});
             if (typeof controller['setStatus'] === 'function') {
-                (<any>controller).setStatus(undefined);
+                controller.setStatus(undefined);
             }
             {{else}}
             const controller = new {{../name}}();
@@ -161,7 +161,9 @@ export function RegisterRoutes(app: express.Express) {
 
                 // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
 
-                if (data || data === false) { // === false allows boolean result
+                if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') {
+                    data.pipe(response);
+                } else if (data || data === false) { // === false allows boolean result
                     response.status(statusCode || 200).json(data);
                 } else {
                     response.status(statusCode || 204).end();
diff --git a/src/routeGeneration/templates/hapi.hbs b/src/routeGeneration/templates/hapi.hbs
index 8556d08a..a8d7b765 100644
--- a/src/routeGeneration/templates/hapi.hbs
+++ b/src/routeGeneration/templates/hapi.hbs
@@ -79,7 +79,10 @@ export function RegisterRoutes(server: any) {
                     }
 
                     {{#if ../../iocModule}}
-                    const controller = iocContainer.get<{{../name}}>({{../name}});
+                    const controller: any = iocContainer.get<{{../name}}>({{../name}});
+                    if (typeof controller['setStatus'] === 'function') {
+                        controller.setStatus(undefined);
+                    }
                     {{else}}
                     const controller = new {{../name}}();
                     {{/if}}
diff --git a/src/routeGeneration/templates/koa.hbs b/src/routeGeneration/templates/koa.hbs
index ee72f394..1b28ced6 100644
--- a/src/routeGeneration/templates/koa.hbs
+++ b/src/routeGeneration/templates/koa.hbs
@@ -71,7 +71,10 @@ export function RegisterRoutes(router: KoaRouter) {
             }
 
             {{#if ../../iocModule}}
-            const controller = iocContainer.get<{{../name}}>({{../name}});
+            const controller: any = iocContainer.get<{{../name}}>({{../name}});
+            if (typeof controller['setStatus'] === 'function') {
+                controller.setStatus(undefined);
+            }
             {{else}}
             const controller = new {{../name}}();
             {{/if}}
diff --git a/src/swagger/specGenerator3.ts b/src/swagger/specGenerator3.ts
index e1205ca6..b85af5d1 100644
--- a/src/swagger/specGenerator3.ts
+++ b/src/swagger/specGenerator3.ts
@@ -84,6 +84,29 @@ export class SpecGenerator3 extends SpecGenerator {
           scheme: 'basic',
           type: 'http',
         } as Swagger.BasicSecurity3;
+      } else if (definitions[key].type === 'oauth2') {
+        const definition = definitions[key] as Swagger.OAuth2PasswordSecurity &
+          Swagger.OAuth2ApplicationSecurity &
+          Swagger.OAuth2ImplicitSecurity &
+          Swagger.OAuth2AccessCodeSecurity &
+          Swagger.OAuth2Security3;
+        const oauth = (defs[key] || {
+          type: 'oauth2',
+          description: definitions[key].description,
+          flows: definition.flows || {},
+        }) as Swagger.OAuth2Security3;
+
+        if (definition.flow === 'password') {
+          oauth.flows.password = { tokenUrl: definition.tokenUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        } else if (definition.flow === 'accessCode') {
+          oauth.flows.authorizationCode = { tokenUrl: definition.tokenUrl, authorizationUrl: definition.authorizationUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        } else if (definition.flow === 'application') {
+          oauth.flows.clientCredentials = { tokenUrl: definition.tokenUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        } else if (definition.flow === 'implicit') {
+          oauth.flows.implicit = { authorizationUrl: definition.authorizationUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        }
+
+        defs[key] = oauth;
       } else {
         defs[key] = definitions[key];
       }
diff --git a/src/swagger/swagger.ts b/src/swagger/swagger.ts
index 7cad2afe..e6317649 100644
--- a/src/swagger/swagger.ts
+++ b/src/swagger/swagger.ts
@@ -279,6 +279,17 @@ export namespace Swagger {
     in: 'query' | 'header';
   }
 
+  export interface OAuth2Security3 extends BaseSecurity {
+    type: 'oauth2';
+    flows: OAuthFlow;
+  }
+
+  export interface OAuth2SecurityFlow3 extends BaseSecurity {
+    tokenUrl?: string;
+    authorizationUrl?: string;
+    scopes?: OAuthScope;
+  }
+
   export interface OAuth2ImplicitSecurity extends BaseOAuthSecurity {
     type: 'oauth2';
     description?: string;
@@ -309,5 +320,9 @@ export namespace Swagger {
     [scopeName: string]: string;
   }
 
-  export type Security = BasicSecurity | BasicSecurity3 | ApiKeySecurity | OAuth2AccessCodeSecurity | OAuth2ApplicationSecurity | OAuth2ImplicitSecurity | OAuth2PasswordSecurity;
+  export type OAuthFlow = {
+    [flowName in OAuth2FlowTypes]: OAuth2SecurityFlow3;
+  };
+  export type OAuth2FlowTypes = 'authorizationCode' | 'implicit' | 'password' | 'clientCredentials';
+  export type Security = BasicSecurity | BasicSecurity3 | ApiKeySecurity | OAuth2AccessCodeSecurity | OAuth2ApplicationSecurity | OAuth2ImplicitSecurity | OAuth2PasswordSecurity | OAuth2Security3;
 }


Test patch:
diff --git a/tests/fixtures/controllers/getController.ts b/tests/fixtures/controllers/getController.ts
index dd38ab38..fdfc07b1 100644
--- a/tests/fixtures/controllers/getController.ts
+++ b/tests/fixtures/controllers/getController.ts
@@ -1,3 +1,4 @@
+import { Readable } from 'stream';
 import { Controller, Example, Get, OperationId, Query, Request, Route, SuccessResponse, Tags } from '../../../src';
 import '../duplicateTestModel';
 import { GenericModel, TestClassModel, TestModel, TestSubModel } from '../testModel';
@@ -148,6 +149,15 @@ export class GetTestController extends Controller {
     return new Buffer('testbuffer');
   }
 
+  @Get('HandleStreamType')
+  public async getStream(): Promise<Readable> {
+    const readable = new Readable();
+    readable._read = () => ({});
+    readable.push(Buffer.from('testbuffer'));
+    readable.push(null);
+    return readable;
+  }
+
   @Get('GenericModel')
   public async getGenericModel(): Promise<GenericModel<TestModel>> {
     return {
diff --git a/tests/fixtures/defaultOptions.ts b/tests/fixtures/defaultOptions.ts
index b49da445..6662052f 100644
--- a/tests/fixtures/defaultOptions.ts
+++ b/tests/fixtures/defaultOptions.ts
@@ -12,6 +12,43 @@ export function getDefaultOptions(outputDirectory: string = '', entryFile: strin
       basic: {
         type: 'basic',
       },
+      application: {
+        type: 'oauth2',
+        flow: 'application',
+        tokenUrl: '/ats-api/auth/token',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
+      password: {
+        type: 'oauth2',
+        flow: 'password',
+        tokenUrl: '/ats-api/auth/token',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
+      accessCode: {
+        type: 'oauth2',
+        flow: 'accessCode',
+        tokenUrl: '/ats-api/auth/token',
+        authorizationUrl: '/ats-api/auth/authorization',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
+      implicit: {
+        type: 'oauth2',
+        flow: 'implicit',
+        authorizationUrl: '/ats-api/auth/authorization',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
     },
     version: '1.0.0',
   };
diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts
index 954e9d78..4657e9d8 100644
--- a/tests/integration/express-server.spec.ts
+++ b/tests/integration/express-server.spec.ts
@@ -123,6 +123,13 @@ describe('Express Server', () => {
     });
   });
 
+  it('returns streamed responses', () => {
+    return verifyGetRequest(`${basePath}/GetTest/HandleStreamType`, (err, res) => {
+      expect(res.text).to.equal('testbuffer');
+      return;
+    });
+  });
+
   it('parsed body parameters', () => {
     const data = getFakeModel();
 
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index d4df56a5..c8e62fa9 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -95,6 +95,102 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
       expect(basic.type).to.equal('http');
       expect(basic.scheme).to.equal('basic');
     });
+
+    it('should replace type: oauth2 with type password: oauth2 and flows with password', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.password) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const password = specDefault.spec.components.securitySchemes.password as Swagger.OAuth2Security3;
+
+      expect(password.type).to.equal('oauth2');
+      expect(password.flows.password).exist;
+
+      const flow = password.flows.password;
+
+      expect(flow.tokenUrl).to.equal('/ats-api/auth/token');
+      expect(flow.authorizationUrl).to.be.undefined;
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
+
+    it('should replace type: oauth2 with type application: oauth2 and flows with clientCredentials', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.application) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const app = specDefault.spec.components.securitySchemes.application as Swagger.OAuth2Security3;
+
+      expect(app.type).to.equal('oauth2');
+      expect(app.flows.clientCredentials).exist;
+
+      const flow = app.flows.clientCredentials;
+
+      expect(flow.tokenUrl).to.equal('/ats-api/auth/token');
+      expect(flow.authorizationUrl).to.be.undefined;
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
+
+    it('should replace type: oauth2 with type accessCode: oauth2 and flows with authorizationCode', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.accessCode) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const authCode = specDefault.spec.components.securitySchemes.accessCode as Swagger.OAuth2Security3;
+
+      expect(authCode.type).to.equal('oauth2');
+      expect(authCode.flows.authorizationCode).exist;
+
+      const flow = authCode.flows.authorizationCode;
+
+      expect(flow.tokenUrl).to.equal('/ats-api/auth/token');
+      expect(flow.authorizationUrl).to.equal('/ats-api/auth/authorization');
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
+
+    it('should replace type: oauth2 with type implicit: oauth2 and flows with implicit', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.implicit) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const imp = specDefault.spec.components.securitySchemes.implicit as Swagger.OAuth2Security3;
+
+      expect(imp.type).to.equal('oauth2');
+      expect(imp.flows.implicit).exist;
+
+      const flow = imp.flows.implicit;
+
+      expect(flow.tokenUrl).to.be.undefined;
+      expect(flow.authorizationUrl).to.equal('/ats-api/auth/authorization');
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
   });
 
   describe('paths', () => {