LoginSignup
2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-31

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 を活用し始めていて、
    • generateSwaggerSpecgenerateRoute が自力でできて(できかけて)いる人
    • 外部の型つかうんじゃねぇよ(or 複数定義があるからなんとかしろよ)、って tsoa に怒られてる人

前提

tsoa という便利なライブラリを、 express wrapper として活用しています。

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

ちなみに例に用いるリポジトリでは、
TypeScript 用 ORMapper である TypeORM と AuroraServerless MySQL を組み合わせてモデル定義運用しています)

サンプルコード

controller.ts
@Route("type-detector/v1")
export class TypeDetectorController extends Controller {
  @Get("resources")
  @SuccessResponse("200", "okdayo")
  async getResource(): Promise<TestEntity[]> {
    return await TestEntity.find();
  }
}
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 等を実行しようとすると、
以下のように怒られてしまいます


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

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);
  };
}

結果

$ 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!


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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1