20200331のNode.jsに関する記事は5件です。

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.js
src/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>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Command Line Interface (CLI) の出力を `--query` で制御し、パイプラインで活用する

job_tobisyoku.png

これを

% 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 も有用

そもそもこういった出力制御というのは、本質的に頑張りたい処理ではないので、極力最小化させたいわけです

最小化すれば、小さな処理を組み合わせた複雑なこともより書きやすくなります

organizations でも

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 でやりますと、楽しいですよ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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")
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":
          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 というフィールドができているのですが、
今回のリポジトリではその程度の汚染は許容範囲として無視します。
このあたりをキッチリきれいに整えるとしたらもう少し工夫が必要になるでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
let {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.py
import 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']を指定する必要があるとのことです。

必ずつけ忘れないようにしましょう。
以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-node

swaggerプロジェクトの作成

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-container

Dockerコンテナ起動

同じディレクトリにdocker-compose.yamlを作成して、swagger-mock(mockサーバーコンテナ)とswagger-editor(swagger.yamlを編集するためのコンテナ)を定義します。

docker-compose.yaml
version: "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の編集画面が開きます。

キャプチャ.PNG

試しに新しいAPIとして次のようにuser API(Get, POST)を定義してみます。
左側の編集パネルに次のように追記します。

swagger.yaml
swagger: "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プロジェクトを作成するあたり)
おいおい勉強して更新していきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む