tsoa[~v3] で 3rd party 製の型利用時のエラーを無視するモンキーパッチ

# 06/01更新
tsoa v3がいつのまにか公開されていた。
v3では、外部パッケージの型を含めても怒られなくなっている！

しかし、timezone-moment.Moment 等を使うと

```
UnhandledPromiseRejectionWarning:
Error: Multiple matching models found for referenced type Moment;
please make model names unique.
```
などと怒られてしまうので、そういったケースにも応用可能。

# tsoa で 3rd party 製の型利用時のエラーを無視するモンキーパッチ

- この記事の対象者
    - `tsoa` を活用し始めていて、
    - `generateSwaggerSpec` や `generateRoute` が自力でできて(できかけて)いる人
    - 外部の型つかうんじゃねぇよ(or 複数定義があるからなんとかしろよ)、って tsoa に怒られてる人

## 前提
[tsoa](https://github.com/lukeautry/tsoa) という便利なライブラリを、 express wrapper として活用しています。

かいつまんで説明すると、
controller を記述することで、 swagger 定義と express の routes 定義を自動出力でき、
controller と swagger のダブルメンテを行うことなく、快適に REST API 開発が可能になります。

ちなみに例に用いるリポジトリでは、
 TypeScript 用 ORMapper である [TypeORM と AuroraServerless MySQL を組み合わせて](https://qiita.com/tokicat/items/65523f95d2cecfe42ced)モデル定義運用しています)

## サンプルコード

```ts:controller.ts
@Route("type-detector/v1")
export class TypeDetectorController extends Controller {
  @Get("resources")
  @SuccessResponse("200", "okdayo")
  async getResource(): Promise<TestEntity[]> {
    return await TestEntity.find();
  }
}
```

```ts:entities.ts
const MOMENT_OPTS: ColumnOptions = {
  type: "datetime",
  transformer: {
    from: (from: Date) => from && moment(from),
    to: (to: Moment) => to?.toISOString(),
  },
};

@Entity({ engine: "InnoDB ROW_FORMAT=DYNAMIC" })
export class TestEntity extends BaseEntity {
  constructor(init: Partial<TestEntity>) {
    super();
    Object.assign(this, init);
  }

  @PrimaryGeneratedColumn("increment") seq: number;
  @Column() dateAt: Date;
  @Column(MOMENT_OPTS) dateAtM: Moment;
}

```

`@Entity` `@Column` `BaseEntity` あたりは軒並み TypeORM の仕組みなので気にする必要はありません
Moment 型のフィールドを持った単なる class か interface と考えてください

## 問題点

これらの型定義と controller をもとに、
tsoa で `generateSwaggerSpec` 等を実行しようとすると、
以下のように怒られてしまいます

```sh

$ ts-node-dev src/swagger/swagger-generator.ts

There was a problem resolving type of 'TestEntity'.
(node:46567) UnhandledPromiseRejectionWarning:
Error: No matching model found for referenced type Moment.
If Moment comes from a dependency, please create an interface in your own code that has the same structure.
Tsoa can not utilize interfaces from external dependencies.
Read more at https://github.com/lukeautry/tsoa/blob/master/docs/ExternalInterfacesExplanation.MD
```

外部 module で定義されたモデルはよみこめねーよ、って言ってますね。
string, Date などの基本的な型(と、その複合体)しか使えない仕様のようです。

この場では具体的にいうと、以下2つの型が該当していました。

- 時刻フィールドをもつために使っている Moment
- TypeORM の仕組みを活用するために継承させている BaseEntity

## 解決したいこと

- Moment 型のフィールド
    - 出力される SwaggerSpec / Express.Routes 上では、 Date 型として扱いたい
- BaseEntity 型のフィールド
    - TypeORM の基本的な機能を提供するためのスーパークラスである
    - 継承することでフィールドが増えたりはしないので、単純に無視したい

## 解決策

Tsoa.TypeResolver が型の分析を司っているみたいです。
その処理に対し、上記の解決したい型であった場合は、
強制的に 別な型としてみなすようなモンキーパッチを作成しました。

```ts:swagger-generator.ts
import { generateRoutes, generateSwaggerSpec, RoutesConfig, SwaggerConfig } from "tsoa";
import { TypeResolver } from "tsoa/dist/metadataGeneration/typeResolver";

(async () => {
  externalTypesPatch();

  const swaggerOpts: SwaggerConfig = {
    schemes: ["http", "https"],
    host: "localhost:8080",
    basePath: "/",
    entryFile: "src/type-detector-express.ts",
    specVersion: 3,
    outputDirectory: "src/swagger",
    controllerPathGlobs: ["src/controllers/*.ts"],
  };

  const routeOpts: RoutesConfig = {
    basePath: "",
    entryFile: "src/type-detector-express.ts",
    routesDir: "src/swagger",
  };

  generateSwaggerSpec(swaggerOpts, routeOpts, undefined, []).then(() =>
    console.log("Swagger Refreshed!")
  );
  generateRoutes(routeOpts, swaggerOpts, undefined, []).then(() =>
    console.log("Routes Refreshed!")
  );
})();

/**
 * tsoaでは、3rd party の型が使えない(エラー吐かれる)仕様だが、
 * それでは困る場合に、任意の型を強制的に他の型としてみなすようにするモンキーパッチ
 */
function externalTypesPatch() {
  TypeResolver.prototype["originResolve"] = TypeResolver.prototype.resolve;
  TypeResolver.prototype.resolve = function(...args) {
    const typeName = this?.typeNode?.typeName?.text;
    if (typeName) {
      let override;
      switch (typeName) {
        case "Moment":
          override = { dataType: "datetime" };
          break;
        case "BaseEntity": // v3 ではなくてもいける・・・？
          override = { dataType: "any" };
          break;
        case "Function": // v3 では不要になった
          // このパッチを適用すると、なぜか Function 型？の処理で落ちるようになる事象への対策。
          // どんな影響があるか知らないが、観測範囲内では期待通り動いているので問題なかろう
          override = { dataType: "any" };
          break;
        default:
          break;
      }
      if (override) {
        console.log(`TypeResolver.eolsve Override for ${typeName} -> ${JSON.stringify(override)}`);
        return override;
      }
    }

    return this.originResolve(...args);
  };
}
```

## 結果

```sh
$ ts-node-dev src/swagger/swagger-generator.ts

TypeResolver.eolsve Override for Moment -> {"dataType":"datetime"}
TypeResolver.eolsve Override for Function -> {"dataType":"any"}
TypeResolver.eolsve Override for Moment -> {"dataType":"datetime"}
TypeResolver.eolsve Override for Function -> {"dataType":"any"}
Swagger Refreshed!
Routes Refreshed!
```

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545251/203cb7be-1263-e980-b95e-df7f70550c2a.png"/>
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545251/f69cde2d-b804-7faf-7110-aab90c72538d.png"/>

Schema に、 BaseEntity 由来の本来不要である target というフィールドができているのですが、
今回のリポジトリではその程度の汚染は許容範囲として無視します。
このあたりをキッチリきれいに整えるとしたらもう少し工夫が必要になるでしょう。