- 投稿日:2020-03-31T23:30:13+09:00
Node.js+Expressのインストール、起動まで
node.jsのインストール
EC2のセキュリティグループの設定で3000番を開けておくインストール $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash $ . ~/.nvm/nvm.sh $ nvm install node バージョン確認 $ node -e "console.log('Running Node.js ' + process.version)"expressのインストール
npm init -y npm i express -Sサーバーのファイルの記述し、実行する。
$ mkdir src $ mkdir src/public $ vi src/app.js $ vi src/public/index.html $ node src/app.jssrc/app.js// express モジュールのインスタンス作成 const express = require('express'); const app = express(); // パス指定用モジュール const path = require('path'); // 3000番ポートで待ちうける app.listen(3000, () => { console.log('Running at Port 3000...'); }); // 静的ファイルのルーティング app.use(express.static(path.join(__dirname, 'public'))); // その他のリクエストに対する404エラー app.use((req, res) => { res.sendStatus(404); });public/index.html<!DOCTYPE html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>TEST</title> <link rel="stylesheet" href="/css/sample.css"> </head> <body> <h1>Hello World!</h1> <p id="hoge"></p> <div> <img src="/img/sample.png"> </div> <script src="/js/sample.js"></script> </body> </html>
- 投稿日:2020-03-31T19:06:43+09:00
AWS Command Line Interface (CLI) の出力を `--query` で制御し、パイプラインで活用する
これを
% aws organizations describe-organization { "Organization": { "MasterAccountEmail": "master-account@your-organization-example.com", "MasterAccountArn": "arn:aws:organizations::111111111111:account/o-xxxxxxxxxx/111111111111", "MasterAccountId": "111111111111", "Id": "o-xxxxxxxxxx", "AvailablePolicyTypes": [ { "Status": "ENABLED", "Type": "SERVICE_CONTROL_POLICY" } ], "FeatureSet": "ALL", "Arn": "arn:aws:organizations::111111111111:organization/o-xxxxxxxxxx" } }こう する話です
% aws organizations describe-organization --query 'Organization.MasterAccountEmail' --output text master-account@your-organization-example.comそしてたとえばこのように応用する
% aws ec2 describe-regions --query 'Regions[].{Name:RegionName}' --output text | xargs -I{} aws ec2 describe-availability-zones --region {} --query 'AvailabilityZones[].{c1:RegionName,c2:ZoneName,c3:State}' --output text eu-north-1 eu-north-1a available eu-north-1 eu-north-1b available eu-north-1 eu-north-1c available ap-south-1 ap-south-1a available ap-south-1 ap-south-1b available ...(省略)
- ※全リージョン全AZを列挙
なぜ awscli の
--query
オプションにこだわるのか。まず、 JSON 解析をわざわざ他のツールに依存したくないから です。
jq はイマイチだし、JSON なんだから JavaScript で書くのが一番自然な気はするとはいえ、Node.js で実際やってみるとこれはこれで面倒に感じてしまう。最初の
describe-organization
を JavaScript でやるとしたらこんなかんじでしょうか% aws organizations describe-organization | node -e " const data = []; process.stdin .on('readable', () => { let chunk; while ((chunk = process.stdin.read()) !== null) { data.push( chunk ); } } ) .on('end', () => { process.stdout.write( JSON.parse( data.join( '' ) ).Organization.MasterAccountEmail ); } ); "https://nodejs.org/api/process.html#process_process_stdin
readline だとこのような感じか
% aws organizations describe-organization | node -e " const data = []; require( 'readline' ) .createInterface( { input: process.stdin } ) .on( 'line', ( l ) => { data.push( l ); } ) .on( 'close', () => { process.stdout.write( JSON.parse( data.join( '' ) ).Organization.MasterAccountEmail ); } ); "やはり
--query
パラメタでやった方が楽な気がします。メリットは次のような感じでしょうか
- 他ツール非依存
- パラメタを考案する必要はあるが、比較的低負担で見通しがよい
--output text
との組み合わせで以降の処理にもパイプで繋げやすい- ※場合によっては
--filters
も有用そもそもこういった出力制御というのは、本質的に頑張りたい処理ではないので、極力最小化させたいわけです
最小化すれば、小さな処理を組み合わせた複雑なこともより書きやすくなりますaws organizations list-accounts --query 'Accounts[].{Id:Id,Name:Name}' --output text | while read a; do printf "$a\n"; aws organizations list-tags-for-resource --resource-id "`echo $a | awk '{print $1}'`" --query 'Tags[].{Key:Key,Value:Value}' --output table; done
- ※全メンバーアカウントのタグを列挙
- ※マスターアカウントで動作します
工夫次第で様々に活用できますが、リージョンやアカウントを横断した組織統制系のオペレーションなども、わざわざブラウザに行くまでもなく、このようにささっと CLI でやりますと、楽しいですよ。
- 投稿日:2020-03-31T16:46:18+09:00
tsoa で 3rd party 製の型利用時のエラーを無視するモンキーパッチ
tsoa で 3rd party 製の型利用時のエラーを無視するモンキーパッチ
- この記事の対象者
tsoa
を活用し始めていて、generateSwaggerSpec
やgenerateRoute
が自力でできて(できかけて)いる人- 外部の型つかうんじゃねぇよ、って 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.tsconst 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.tsimport { 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": 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)}`); 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 というフィールドができているのですが、
今回のリポジトリではその程度の汚染は許容範囲として無視します。
このあたりをキッチリきれいに整えるとしたらもう少し工夫が必要になるでしょう。
- 投稿日:2020-03-31T15:53:54+09:00
Pythonのprint出力をNode.jsが認識できない問題について
こんにちは。
こちらの記事は、Python-shellを用いたNode.jsとpythonの連携時に発生したバグの
解決方法について、自らの経験を記述しているものです。
英語が分かる方はこちらの公式ドキュメントを参照していただいた方が正確ですので
ご参照ください。
https://www.npmjs.com/package/python-shellバージョン
python 3.8.1
pyenv 1.2.17
node.js 13.3.0問題
Node.jsからpythonのスクリプトに引数を渡して実行させ、実行結果を
Node.jsで受け取ったところ、
python側の出力では正しくjsonデータが送られているのですが、
Node.js側では空文字からなるリスト['']しか受け取れないという現象が発生しました。
また、この現象が発生した際には、pyenvを用いて作成した環境下で行っていたのですが、
system環境で実行したところ、正常に値を受け取ることが判明し、
環境の違いによってコードが動いたり、動かなかったりする状態でした。該当コードは以下の通りです。
Node.js側のコードconnect_test.jslet {PythonShell} = require('python-shell'); let options = {args:[input_string]}; PythonShell.run('data_processing.py', options, function(err, data){ if (err) throw err; console.log(JSON.parse(data)); });Python側のコード
data_processing.pyimport json import sys data_info = sys.argv[1] ''' コード中略 ''' print(json.dumps(output_json))解決策
Node.js側のコードのoption変数に以下のコードを付け足すことで
pyenvで作成した環境下でも動作するようになりました。//修正前 let options = {args:[input_string]}; //修正後 let options = {pythonOptions:['-u'], args:[input_string]};ドキュメントによると
pythonOptions
pythonへ渡す際のオプションスイッチの配列で、リアルタイムにprint()の結果を
受け取る際には、['-u']を指定する必要があるとのことです。必ずつけ忘れないようにしましょう。
以上です。
- 投稿日:2020-03-31T00:45:06+09:00
swagger-nodeとDockerで簡単にモックサーバーを構築する
はじめに
APIを呼び出す機能をテストする際に、とりあえずモックサーバーを立てたい。
そんなときに便利なのが、SwaggerのNode.js 製のモジュールであるswagger-nodeです。SwaggerでAPIを定義しておくだけで、モックサーバーを起動できます。
便利なのは、自前でテストデータを用意しなくても、データ型(string, number, boolean, array, object etc)に応じた適当な値をレスポンスしてくれる点です。Swaggerの概要やswagger-nodeの使い方はこちらで紹介されているため、詳細は割愛します。
Swaggerとswagger-nodeこの記事では、Node.jsのインストールが面倒なのと、環境を汚さずに使いたいということで、
swagger-nodeとDockerを組み合わせてモックサーバーを構築する方法を説明します。前提
dockerとdocker-composeを利用できること
モックサーバーを構築する
Dockerfileの作成
まずはswagger-nodeを内包するDockerイメージを作成します。
最新のLTS版であるNode.js v12だとエラーになるようなので、v10を使用します。
https://github.com/swagger-api/swagger-node/issues/586
npm install --save swagger-router
すればOKとの情報もありましたが、未確認です。適当なディレクトリでDockerfileを作成します。
FROM node:10 # アプリケーションディレクトリを作成する WORKDIR /usr/src/app # swagger-nodeをグローバルインストールする RUN npm install swagger -g ENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 10; done"]これをビルトしてDockerイメージを作成します。
$ docker build . -t swagger-nodeswaggerプロジェクトの作成
swaggerプロジェクトを作成するために、swagger-nodeをインストールしたコンテナを起動して、swaggerコマンドを実行します。
作成したswaggerプロジェクトはホストにマウントするために、-v $PWD:/usr/src/app
オプションを付けておきます。$ docker run -d -v $PWD:/usr/src/app --name first-swagger-container swagger-node $ docker exec -it first-swagger-container swagger project create sample-project ? Framework? connect ❯ express hapi restify sails作成されたsample-projectはこのような構成になります。
swagger.yamlがAPI定義ファイルです。sample-project/
├ api/
│ ├ controllers/
│ ├ helpers/
│ ├ mocks/
│ └ swagger
│ └ swagger.yaml
├ config/
├ node_modules/
├ test/
├ .gitignore
├ app.js
├ package-lock.json
├ package.json
└ README.mdなお、ここで作成したコンテナは不要なので削除します。
$ docker rm --force first-swagger-containerDockerコンテナ起動
同じディレクトリにdocker-compose.yamlを作成して、
swagger-mock
(mockサーバーコンテナ)とswagger-editor
(swagger.yamlを編集するためのコンテナ)を定義します。docker-compose.yamlversion: "3.6" services: swagger-mock: build: . environment: - CHOKIDAR_USEPOLLING=true ports: - "10010:10010" volumes: - .:/usr/src/app networks: examples-net: ipv4_address: 172.16.239.103 # -m: モックモードとして起動するオプション entrypoint: bash -c "cd sample-project && swagger project start -m" swagger-editor: build: . ports: - "8000:8000" volumes: - .:/usr/src/app networks: examples-net: ipv4_address: 172.16.239.104 # -s: 起動時にブラウザが立ち上がらないようにする # -p: 指定しない場合ポートがランダムになるため8000で固定する # --host: ipv4_addressで指定したIPを設定する entrypoint: bash -c "cd sample-project && swagger project edit -s -p 8000 --host 172.16.239.104" networks: examples-net: name: examples-net driver: bridge ipam: driver: default config: - subnet: 172.16.239.0/24
swagger-mock
のポイントとしては、CHOKIDAR_USEPOLLING=true
を設定している点です。
これは、マウントされているファイルが更新された際にコンテナを自動起動するための設定です。
これによって、swagger.yamlが更新された際に即時にモックサーバーに反映されるようになります。
- 補足
- swagger-nodeには、nodemonというモジュールが組み込まれており、ファイル更新を検知したら自動で再起動する仕組み(いわゆるホットリロード)が実装されています。 しかし、Dockerと組み合わせた場合にこれが効かなくなってしまい、Dockerコンテナとしてホットリロードする方法を採用しました。
それではDockerコンテナを起動してみます。
$ docker-compose up -dコンテナが起動すると
swagger-mock
がモックサーバーとして利用できる状態になります。
試しにhttp://<コンテナのIPアドレス>:10010
に対して、hello API(初回作成時のサンプルAPI)を呼ぶと、レスポンスを返してくれます。$ curl http://192.168.99.100:10010/hello?name=Scott {"message":"Sample text"}swagger.yamlを更新
目的とするAPIのモックサーバーを起動するには、swagger.yamlを編集する必要があります。
そこでswagger-editor
コンテナを使用します。
ブラウザでhttp://<コンテナのIPアドレス>:8000
にアクセスするとswagger.yamlの編集画面が開きます。試しに新しいAPIとして次のようにuser API(Get, POST)を定義してみます。
左側の編集パネルに次のように追記します。swagger.yamlswagger: "2.0" version: "0.0.1" title: Hello World App # during dev, should point to your local machine host: localhost:10010 # basePath prefixes all resource paths basePath: / # schemes: # tip: remove http to make production-grade - http - https # format of bodies a client can send (Content-Type) consumes: - application/json # format of the responses to the client (Accepts) produces: - application/json paths: ...(略)... /user: x-swagger-router-controller: UserController get: operationId: getUser parameters: - name: id in: query required: true type: string responses: "200": description: Success schema: properties: name: type: string age: type: number post: operationId: postUser parameters: - name: "user" in: "body" required: true schema: required: - name - age type: object properties: name: type: string age: type: number responses: "200": description: Success schema: properties: message: type: string ...(略)...swagger.yamlの書き方について、基本的にはSwaggerの構文に従えばOKです。
ここで重要なのはx-swagger-router-controller
に任意の値を設定することです。
これはswagger-node独自の項目で、ルーティングするコントローラー(.js)を設定するものです。
swagger project start
コマンドで普通に起動する場合は、ここで設定したコントローラー(.js)にルーティングされ、実装された処理に従ってレスポンスが返されます。
モックサーバーと起動する(-m
オプションを付与する)場合は、コントローラーが存在しなくても、responsesで定義している型に応じてレスポンスを返してくれるのです。動作確認
ブラウザの編集画面に書き込んだ時点で、swagger.yamlが更新され、上述したホットリロードの設定によってswagger-mockコンテナが再起動されます。
つまり、即時にモックサーバーに反映されています。
自身でコンテナを再起動する必要はありません。実際にuser APIを呼んでみます。
$ curl http://192.168.99.100:10010/user?id=xxx {"name":"Sample text","age":1} $ curl -X POST -H 'Content-Type:application/json' -d '{"id": "aaa", "name": "hoge", "age": 10}' http://192.168.99.100:10010/user {"message":"Sample text"}適当な値が設定されたレスポンスを返してくれました。
所感
APIを呼び出す機能をテストしたい場合、Swagger定義をもらって編集画面を開いてコピペして、
x-swagger-router-controller
という若干の設定を行えば、簡単にモックサーバーを起動できます。あとは、自分でAPIを定義する場合にも、「swagger.yamlを編集→API呼び出しして確認」というライフサイクルを回しやすそうです。
おわりに
Dockerを勉強中なので、無駄な手順が含まれているように思います。(特にswaggerプロジェクトを作成するあたり)
おいおい勉強して更新していきたいと思います。