Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8835

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

$
0
0

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

  • この記事の対象者
    • tsoaを活用し始めていて、
    • generateSwaggerSpecgenerateRouteが自力でできて(できかけて)いる人
    • 外部の型つかうんじゃねぇよ、って tsoa に怒られてる人

前提

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

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

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

サンプルコード

controller.ts
@Route("type-detector/v1")exportclassTypeDetectorControllerextendsController{@Get("resources")@SuccessResponse("200","okdayo")asyncgetResource():Promise<TestEntity[]>{returnawaitTestEntity.find();}}
entities.ts
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@ColumnBaseEntityあたりは軒並み 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();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 というフィールドができているのですが、
今回のリポジトリではその程度の汚染は許容範囲として無視します。
このあたりをキッチリきれいに整えるとしたらもう少し工夫が必要になるでしょう。


Viewing all articles
Browse latest Browse all 8835

Trending Articles