背景
NestJsでgRPCのサーバー編をやってみましたが、
protoファイルで定義したmessageと同様にinterfaceを作成したり、結構煩雑なのでproto-ts
を導入し、ある程度の自動化を図ってみます
準備
protocのインストール
インストールというか、protocをダウンロードしてPATHを通すだけです。
ダウンロードは、以下から実施しましょう。
Releases · protocolbuffers/protobuf · GitHub
proto-tsのインストール
こちらはプロジェクトフォルダでnpmインストールします。
npm i ts-proto
.protoの定義
proto/sample.proto
syntax = "proto3"; import "google/protobuf/empty.proto"; package sample; service SampleService { rpc FindOne (SampleRequest) returns (SampleResponse) {} rpc getAll (google.protobuf.Empty) returns (Sample2Response) {} } message SampleRequest { int32 id = 1; } message SampleResponse { int32 id = 1; string word = 2; } message Sample2Response { repeated SampleResponse sampleResponses = 1; }
- メソッドを二つ定義しました。
protocコマンドを--plugin
オプションで、proto-ts
を指定して実行する
protoc --plugin=protoc-gen-ts_proto=".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd" --ts_proto_opt=nestJs=true --ts_proto_opt=outputClientImple=false --ts_proto_opt=addGrpcMetadata=true --ts_proto_out=src -Isrc\proto .\src\proto\sample.proto
- protocの
--plugin
でproto-tsのcmdファイルを指定する。proto-ts
をインストールすると、node_modules.bin配下に作成されているはず。- Windows環境の場合は、
--plugin=protoc-gen-ts_proto=".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd"
のように書かないといけない- 公式のQuickStartにも書いてあった・・・。
-ts_proto_opt=nestJs=true
とすることで、nestjsに特化したtsファイルを出力してくれる。- このオプションを指定しないと全然違うものが出来上がります。
コマンド実行で生成されるtsファイル
/* eslint-disable */ import { Metadata } from "@grpc/grpc-js"; import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices"; import { Observable } from "rxjs"; import { Empty } from "./google/protobuf/empty"; export const protobufPackage = "sample"; export interface SampleRequest { id: number; } export interface SampleResponse { id: number; word: string; } export interface Sample2Response { sampleResponses: SampleResponse[]; } export const SAMPLE_PACKAGE_NAME = "sample"; export interface SampleServiceClient { findOne(request: SampleRequest, metadata?: Metadata): Observable<SampleResponse>; getAll(request: Empty, metadata?: Metadata): Observable<Sample2Response>; } export interface SampleServiceController { findOne( request: SampleRequest, metadata?: Metadata, ): Promise<SampleResponse> | Observable<SampleResponse> | SampleResponse; getAll(request: Empty, metadata?: Metadata): Promise<Sample2Response> | Observable<Sample2Response> | Sample2Response; } export function SampleServiceControllerMethods() { return function (constructor: Function) { const grpcMethods: string[] = ["findOne", "getAll"]; for (const method of grpcMethods) { const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); GrpcMethod("SampleService", method)(constructor.prototype[method], method, descriptor); } const grpcStreamMethods: string[] = []; for (const method of grpcStreamMethods) { const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); GrpcStreamMethod("SampleService", method)(constructor.prototype[method], method, descriptor); } }; } export const SAMPLE_SERVICE_NAME = "SampleService";
controllerの定義
.protoから生成されたtsファイルを使用して、controllerを作成する
sample.controller.ts
import { Metadata } from '@grpc/grpc-js'; import { Sample2Response, SampleRequest, SampleResponse, SampleServiceController, SampleServiceControllerMethods, } from './sample'; import { Empty } from './google/protobuf/empty'; import { Controller } from '@nestjs/common'; @Controller('sampele') @SampleServiceControllerMethods() export class SampleController implements SampleServiceController { private data: SampleResponse[] = [ { id: 1111, word: 'hogehoge' }, { id: 2222, word: 'fugafuga' }, ]; findOne(request: SampleRequest, metadata?: Metadata): SampleResponse { return this.data.find((a) => a.id === request.id); } getAll(request: Empty, metadata?: Metadata): Sample2Response { return { sampleResponses: this.data }; } }
- コントローラークラスには、protoファイルから生成されたtsファイルにある
@SampleServiceControllerMethods()
を付与する。 - また、コントローラークラスのinterfaceも生成されたtsファイルで定義してくれている(
SampleServiceController
)ので、実装する形で定義する。
main.tsへのprotoファイルのパス設定追加
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { join } from 'path'; import { GrpcOptions, Transport } from '@nestjs/microservices'; async function bootstrap() { const app = await NestFactory.createMicroservice<GrpcOptions>(AppModule, { transport: Transport.GRPC, options: { url: 'localhost:5000', package: ['postCode', 'sample'], //add 'sample' protoPath: [ join(__dirname, 'proto/postCode.proto'), join(__dirname, 'proto/sample.proto'), //add ], }, }); await app.listen(); } bootstrap();
module.tsへの追加
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { SampleController } from './sample.controller'; @Module({ imports: [], controllers: [AppController, SampleController], // add SampleController providers: [AppService], }) export class AppModule {}
実行してみる
今回は、BloomRPCで実行して確認します。
FindOne
getAll
想定どおりデータが取得できています!
今回のハマりどころ・・・
.protoファイルでパラメータなしを表すためgoogle.protobuf.Empty
を使用しましたが、
当初importをimport "google/protobuf/Empty.proto";
を使用していました。(EmptyのEが大文字)
この状態で、nestjsの起動を行うと・・・
[Nest] 28756 - 2023/08/09 10:33:29 ERROR [Server] no such type: google.protobuf.Empty Error: The invalid .proto definition (file at "undefined" not found) at ServerGrpc.loadProto (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:285:39) at ServerGrpc.bindEvents (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:42:34) at ServerGrpc.start (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:37:20) at ServerGrpc.listen (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:30:24) at processTicksAndRejections (node:internal/process/task_queues:95:5) C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:382 throw Error("no such type: " + path); ^ Error: no such type: google.protobuf.Empty at Service.lookupType (C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:382:15) at Method.resolve (C:\microservice-study\post-code-service\node_modules\protobufjs\src\method.js:156:44) at Service.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\service.js:111:20) at Namespace.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:307:25) at Root.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:307:25) at Root.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\root.js:259:43) at loadProtosWithOptionsSync (C:\microservice-study\post-code-service\node_modules\@grpc\proto-loader\src\util.ts:80:14) at Object.loadSync (C:\microservice-study\post-code-service\node_modules\@grpc\proto-loader\src\index.ts:391:47) at ServerGrpc.loadProto (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:280:62) at ServerGrpc.bindEvents (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:42:34)
こんなエラーが出現しました。。。
これは、import "google/protobuf/empty.proto";
(emptyのeが小文字)を使用することで、解消しました。
一応、こちらのIssueに書いていました・・・。
皆様、お気をつけて・・・
所感
Windows環境でのprotocコマンドの実行でいろいろエラーがでましたが、公式をちゃんと確認すれば解消しました。
いちおう、nestjsのサーバ編は一応できましたが、もうちょっと自動化を図りたいとことです。。。