tsoa で 3rd party 製の型利用時のエラーを無視するモンキーパッチ
- この記事の対象者
tsoa
を活用し始めていて、generateSwaggerSpec
やgenerateRoute
が自力でできて(できかけて)いる人- 外部の型つかうんじゃねぇよ、って tsoa に怒られてる人
前提
tsoaという便利なライブラリを、 express wrapper として活用しています。
かいつまんで説明すると、
controller を記述することで、 swagger 定義と express の routes 定義を自動出力でき、
controller と swagger のダブルメンテを行うことなく、快適に REST API 開発が可能になります。
ちなみに例に用いるリポジトリでは、
TypeScript 用 ORMapper である TypeORM と AuroraServerless MySQL を組み合わせてモデル定義運用しています)
サンプルコード
@Route("type-detector/v1")exportclassTypeDetectorControllerextendsController{@Get("resources")@SuccessResponse("200","okdayo")asyncgetResource():Promise<TestEntity[]>{returnawaitTestEntity.find();}}
constMOMENT_OPTS:ColumnOptions={type:"datetime",transformer:{from:(from:Date)=>from&&moment(from),to:(to:Moment)=>to?.toISOString(),},};@Entity({engine:"InnoDB ROW_FORMAT=DYNAMIC"})exportclassTestEntityextendsBaseEntity{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 が型の分析を司っているみたいです。
その処理に対し、上記の解決したい型であった場合は、
強制的に 別な型としてみなすようなモンキーパッチを作成しました。
import{generateRoutes,generateSwaggerSpec,RoutesConfig,SwaggerConfig}from"tsoa";import{TypeResolver}from"tsoa/dist/metadataGeneration/typeResolver";(async()=>{externalTypesPatch();constswaggerOpts:SwaggerConfig={schemes:["http","https"],host:"localhost:8080",basePath:"/",entryFile:"src/type-detector-express.ts",specVersion:3,outputDirectory:"src/swagger",controllerPathGlobs:["src/controllers/*.ts"],};constrouteOpts: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 の型が使えない(エラー吐かれる)仕様だが、
* それでは困る場合に、任意の型を強制的に他の型としてみなすようにするモンキーパッチ
*/functionexternalTypesPatch(){TypeResolver.prototype["originResolve"]=TypeResolver.prototype.resolve;TypeResolver.prototype.resolve=function(...args){consttypeName=this?.typeNode?.typeName?.text;if(typeName){letoverride;switch(typeName){case"Moment":override={dataType:"datetime"};break;case"BaseEntity":override={dataType:"any"};break;case"Function":// このパッチを適用すると、なぜか Function 型?の処理で落ちるようになる事象への対策。// どんな影響があるか知らないが、観測範囲内では期待通り動いているので問題なかろうoverride={dataType:"any"};break;default:break;}if(override){console.log(`TypeResolver.eolsve Override for ${typeName} -> ${JSON.stringify(override)}`);returnoverride;}}returnthis.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 というフィールドができているのですが、
今回のリポジトリではその程度の汚染は許容範囲として無視します。
このあたりをキッチリきれいに整えるとしたらもう少し工夫が必要になるでしょう。