20200627のNode.jsに関する記事は6件です。

【AWS;Lambda入門】第三弾;Linebotなじゃんけんゲームで遊ぶ♬

今回は、apigatewayを利用したnode.js@Lambdaでじゃんけんゲームを作ってみた。
環境構築やプログラムなどは、全て以下の参考①~④を参考に進めた。
だいたい、最初の①と②でLinebotが動く。そして③と④でLambda版のLinebotが動いた。筆者も言っているように感動ものである。
ちなみに、ほぼ1日でLinebotが動くようになった。
したがって、コードのほとんどは参考のまんまであることをお断りしておきます。
【参考】
LINEBotをみんなで作ろう〜環境構築編〜【GWアドベントカレンダー1日目】
LINEBotをみんなで作ろう〜おうむ返しbotを作ろう編〜【GWアドベントカレンダー3日目】
LINEBotをみんなで作ろう〜レイヤーとAPIgateway設定編〜【GWアドベントカレンダー7日目】
LINEBotをみんなで作ろう〜コードを実装編〜【GWアドベントカレンダー最終日】
ということで、この先のことも考えてじゃんけんゲームを作ってみた。

やったこと

・環境構築(Line)
・環境構築(Lambda@AWS)
・じゃんけんボット

・環境構築(Line)

参考①の解説のとおりで環境が構築出来ました。

LINE Developerのサイトにログインできるようにする。

  • ログイン→開発者名、メール入力
  • プロバイダーを作成→プロバイダー名;この下にボットを構築
  • 新規チャネルを作成→Messaging APIを選択
  • Messaging APIのチャンル作成する→ボット名、その他
  • Botの設定→
    • channel secret(短い方)とChannel access token (long-lived)(長い方)をアプリ側に設定、
    • アプリの公開アドレスからWebhook URLを設定、
    • BotのLINE Official Account featuresのeditから
      • アカウント設定で応答設定;bot オン、
      • あいさつ オフ、
      • 応答メッセージ オフ、
      • Webhook オン

おうむ返しボット

まず、自PC、Windows10でNode.jsを利用しておうむ返しボットを作ります。

ngrokをインストールする

Download & setup ngrokから自PCのOsにあったものをダウンロードして、解凍するとngrok.exeが入っているのでこれを実行すれば(ダブルクリックで)コンソールが動きました。そこで以下を実行すると実行できます。
※ちなみに、専用コンソールで実行してください。WindowsPromptだと動きませんでした
ngrok -v
ngrok version 2.3.35

node.js(v10以上推奨)をインストールする

node.js(v10以上推奨)ダウンロードからWindowsInstaller;node-v12.18.1-x64.msiをダウンロードして、インストール。
通常のコマンドラインで以下のように確認できる。
>node -v
v12.18.1

エディタ

使い慣れたjupyter notebookを使いました。

おうむ返しボットのnode.jsコード

以下のようにdesktop配下にDirを作って、そこで環境構築して作業します。参考にいろいろな拡張があって凄い記事になっていました。
【参考】
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

cd deskTop
mkdir test-linebot
cd test-linebot
npm init -y 
npm i @line/bot-sdk express

次にプログラムのメインとなるindex.jsをtest-linebotフォルダ内に作成します。
エディタで以下をコピペします。
上記のとおり、短い方と長い方をBotのBasic settingsとMessaging API settingsからコピペして設定します。

index.js
"use strict";
const express = require("express");
const line = require("@line/bot-sdk");
const PORT = process.env.PORT || 5000;

const config = {
  channelSecret: "短い方", 
  channelAccessToken: "長い方"
};
const app = express();
app.post("/webhook", line.middleware(config), (req, res) => {
  console.log(req.body.events);
  Promise.all(req.body.events.map(handleEvent)).then((result) =>
    res.json(result)
  );
});
const client = new line.Client(config);
async function handleEvent(event) {
  if (event.type !== "message" || event.message.type !== "text") {
    return Promise.resolve(null);
  };
  let mes = { type: "text", text: event.message.text };
  return client.replyMessage(event.replyToken, mes);
}
app.listen(PORT);
console.log(`Server running at ${PORT}`);

ここで、肝心な部分として、返信している文字列は
eventが以下のようなものなので、
event.message.text='動くかな?'となります。

{
    type: 'message',
    replyToken: '*******************************',
    source: { userId: '*****************', type: '***' },
    timestamp: **************,
    mode: 'active',
    message: { type: 'text', id: '*************', text: '動くかな?' }
}

これで、Linebotを動かしてみます。
\Desktop\test-linebot>node index.js
Server running at 5000
この状態で、もう一個ターミナルを起動して、
ngrok http 5000
これで上記のindex.jsを公開します。
以下のような表示が出現します。以下は1時間37分ほど経過するとexpireするものになっています。
以下の表示からhttps://73e6916ab721.ngrok.ioというサイトからフォワードしていることが分かるので、ボットのWebhook URLとして、https://73e6916ab721.ngrok.io/webhookとします。
※因みにこのサーバーは一意だと思いますが、毎回起動の度に変化します。また、Session Expiresの時間も長いとき(約7時間半)も短いとき(約1時間半)もバラバラです。

ngrok by @inconshreveable                                                                               (Ctrl+C to quit)

Session Status                online
Session Expires               1 hour, 37 minutes
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://73e6916ab721.ngrok.io -> http://localhost:5000
Forwarding                    https://73e6916ab721.ngrok.io -> http://localhost:5000

これで動くと思います。動かなかったら、参考②や参考⑤を見てください。どちらもとても丁寧に書かれています。
出来ると、単なるおうむ返しですが、なんとなくうれしいです。

・環境構築(Lambda@AWS)

今度は、上記のindex.jsをLambdaで動作させApiGatewayを利用して、Linebotと連携します。
これも、参考③のとおりです。
・AWSアカウントでログイン;コンソールへ行く
・Lambdaを開く
・Lambda関数を作成・関数名(lamdbaTst)、ランタイムNode.Node.js 12js 12.x、ロール(基本的な...)等の設定
・コンソールに戻って、(検索して)API Gatewayを選択
・RestAPI 構築・REST・新しいAPI・API名;linebotAPI・エンドポイントタイプ;リージョン・APIの作成
・アクション・メソッドの作成・Post設定
・アクション;POSTセットアップ・Lambda関数・Lambdaプロキシ統合の使用;チェック・Lambda関数;lambdaTst・保存
・Lambda関数に権限を追加するダイアログ;OK
・アクション;POSTメソッドの実行・メソッドリクエスト・HTTPリクエストヘッダー・ヘッダー追加・名前欄にX-Line-Signatureと入力、必須にチェック
・アクション・APIのデプロイ・レイヤーの作成
・デプロイされるステージ;新しいステージ・ステージ名;test1・デプロイ
・Lambdaの関数の画面へ行き、関数にAPIGatewayが連携(追加)されていれば成功
・利用するNode.js環境をzipファイルにまとめて、レイヤーとして登録する・参考③はMac環境ですが、全く同じように作成します。
・cd Desktop・mkdir nodejs・npm init -y・npm i @line/bot-sdk・nodejsディレクトリをzip圧縮
・Lambda関数ページのレイヤーを開く・レイヤーの作成でnodejs.zipをアップロード。名前はそれらしい名前;linebot-SDK。・互換性のあるランタイムオプション;node.js 12,x node.js 10.x・作成
・Lambda関数ページのレイヤーを連携・レイヤーの追加・ランタイムと互換性のあるレイヤーのリストから選択・名前;linebot-SDK
・バージョン;1・追加
・関数画面でレイヤーで読み込めていたら(下に表示されたら)成功
・関数名をクリックして下へスクロール、環境変数の項目を探す、編集をクリック・編集・ACCESSTOKEN;長い方・CHANNELSECRET;短い方・保存
・Lambda関数をクリックして、関数を以下のコードをコピペして張り付ける。・右上の保存
・WEBhookURLを設定する・API Gatewayを押下・APIエンドポイントをコピペ・LinebotのWEBhookURLに張り付ける
これで、動きました。
さて、張り付けるコードは以下のじゃんけんゲームです。

じゃんけんゲームのコード

index.js
"use strict";
// モジュール呼び出し
const crypto = require("crypto");
const line = require("@line/bot-sdk");

// インスタンス生成
const client = new line.Client({ channelAccessToken: process.env.ACCESSTOKEN });

exports.handler = (event, context, callback) => {
  // 署名検証
  const signature = crypto
    .createHmac("sha256", process.env.CHANNELSECRET)
    .update(event.body)
    .digest("base64");
  const checkHeader = (event.headers || {})["X-Line-Signature"];
  const body = JSON.parse(event.body);
  const events = body.events;
  console.log(events);

  // 署名検証が成功した場合
  if (signature === checkHeader) {
    events.forEach(async (event) => {
      let message;
      // イベントタイプごとに関数を分ける
      switch (event.type) {
        // メッセージイベント
        case "message":
          message = await messageFunc(event);
          break;
        // フォローイベント
        case "follow":
          message = { type: "text", text: "追加ありがとうございます!" };
          break;
        // ポストバックイベント
        case "postback":
          message = await postbackFunc(event);
          break;
      }
      // メッセージを返信
      if (message != undefined) {
        client
          .replyMessage(body.events[0].replyToken, message)
          .then((response) => {
            const lambdaResponse = {
              statusCode: 200,
              headers: { "X-Line-Status": "OK" },
              body: '{"result":"completed"}',
            };
            context.succeed(lambdaResponse);
          })
          .catch((err) => console.log(err));
      }
    });
  }
  // 署名検証に失敗した場合
  else {
    console.log("署名認証エラー");
  }
};

const messageFunc = async function (event) {
  var mes = await mesmakeFunc(event);
  let message;
  switch (event.message.type) {
      case "text":
          message= { type: "text", text: mes };
      break;
          case "image":
          message = { type: "text", text: "がぞうを受け取ったよ!" };
      break;
          case "sticker":
          message= { type: "text", text: "ステッカーを送ってくれたんだね!" };
      break;
  }
  if (message !== undefined) {
      return client.replyMessage(event.replyToken, message);
  }
  return message;
};
const mesmakeFunc = async function (event) {
  console.log('event.text:', event.message.text);
  var mes = ['グー','チョキ','パー',event.message.text];
  var r = Math.floor( Math.random() * 4 );
  var mes0 = mes[r]; 
  console.log('message:',mes0);
  return mes0;
};

const postbackFunc = async function (event) {
  let message;
  message = { type: "text", text: "ポストバックイベントを受け付けました!" };
  return message;
};

自PCでのじゃんけんゲームのコード

index.js
"use strict";

const express = require("express");
const line = require("@line/bot-sdk");
const PORT = process.env.PORT || 5000;

const config = {
  channelSecret: "短い方", 
  channelAccessToken: "長い方"
};

const app = express();

app.post("/webhook", line.middleware(config), (req, res) => {
  console.log(req.body.events);
  Promise.all(req.body.events.map(handleEvent)).then((result) =>
    res.json(result)
  );
});

const client = new line.Client(config);

async function handleEvent(event) {
  if (event.type !== "message" ) {
    return Promise.resolve(null);
  };
  let mes = await messageFunc(event);
    console.log('mes_message:',mes);
  return client.replyMessage(event.replyToken, mes);
};

const messageFunc = async function (event) {
  var mes = await mesmakeFunc(event);
  let message;
    console.log('message_message:',event.message.type);
  switch (event.message.type) {
      case "text":
          message= { type: "text", text: mes };
      break;
          case "image":
          message = { type: "text", text: "がぞうを受け取ったよ!" };
          console.log('image_message:',message);
      break;
          case "sticker":
          message= { type: "text", text: "ステッカーを送ってくれたんだね!" };
          console.log('sticker_message:',message);
      break;
  }
  return message;
};

const mesmakeFunc = async function (event) {
  console.log('event.text:', event.message.text);
  var mes = ['グー','チョキ','パー',event.message.text];
  var r = Math.floor( Math.random() * 4 );
  var mes0 = mes[r]; 
  console.log('message:',mes0);
  return mes0;
};

app.listen(PORT);
console.log(`Server running at ${PORT}`);    

まとめ

・Linebotを自PCとLambdaで連携して遊んでみた
・ちょっとした拡張として、じゃんけんゲームを作成してみた

・利用性の高いアプリを作ってみようと思う

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

S3に保存されたwavファイルをLambdaでGoogle Cloud Speech-to-Textを使って文字起こしする

S3に保存されたwavファイルをLambdaでGoogle Cloud Speech-to-Textを使って文字起こしする

はじめに

以下リンク記事を参考に、S3に保存されたwavファイルをLambdaでGoogle Cloud Speech-to-Textを使って文字起こしをやってみたのでまとめておきます。

S3 + ElasticTranscoder + Lambda + Google Cloud Speech-to-Text APIで、動画の音声を自動でテキストにする

?完全なる上位互換なので、私の記事は読む必要ないかと。

1. S3の作成

特別なことはしないです。
1-1. 適当なバケット名、リージョン(私は東京)で次へ
1-2. オプションの設定はノータッチ
1-3. アクセス許可の設定もノータッチ(パブリックアクセスをすべてブロック)
1-4. 確認

2. ローカルでGoogle Cloud Speech-to-Text APIをインストール

Google Cloud Speech-to-Text APIを使うため、npmを使ってローカルでインストールします。
後ほどlayerとしてlambdaに追加するため、ディレクトリ名はnodejsにしてください。

mkdir nodejs
cd nodejs
npm init
npm install @google-cloud/speech

node_modulesが入ったnodejsフォルダをzip(nodejs.zip)にしておきましょう。

3. Lambda layerの作成

先ほど作成したzipファイルをlayerとしてlambdaに追加します。
3-1. Lambdaのレイヤー画面からレイヤーの作成へ
3-2. 適当な名前、説明
3-3. zipファイルをアップロード
3-4. ランタイムはご自身のNode.jsのバージョンで選択(私は10.x)
4-5. 作成

4. Google Cloud Speech-to-Textのサービスアカウントを取得

lambda関数に配置するための、サービスアカウントキーをJSON形式で取得します。
4-1. GCPのAPIとサービス内のCloud Speech-to-Textへ
4-2. APIを有効化する
4-3. 認証情報で「+認証情報を作成」、サービスアカウントを選択し、サービスアカウントを作成
4-4. 作ったら鍵の追加をして、JSON形式の秘密鍵を取得

5. lambda関数の作成

5-1. 関数の作成に行き、適当な名前、ランタイム(私はNode.js 10.x)を選択して作成
5-2. アクセス権限に移動し、実行ロールを選択する
5-3. S3にアクセスするポリシーをアタッチする(とりあえずAmazonS3FullAccess)
5-4. lambdaに戻ってトリガーを追加へ移動し、S3で作成したバケットを選択
5-5. 3で作成したlayerを追加(ランタイムが一致してないと出てこないので注意)
5-6. 4で作成したサービスアカウント情報を追加するため、関数コードのところで、JSONファイルを作成
5-7. ここに4でダウンロードしてきた秘密鍵JSONファイルの情報をコピペ(githubのpublic repoとかに上げないように注意!)
5-8. credientials.jsonを環境変数に追加
image.png

5-9. コードはこちらのコードを拝借しています。少し変えているので以下に貼っておきます。

const AWS    = require('aws-sdk');
const speech = require('@google-cloud/speech');
const client = new speech.SpeechClient();
var s3 = new AWS.S3({
    apiVersion: '2012-09-25'
});

exports.handler = function(event, context) {
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    var name = key.split('.')[0];
    var params = {
        Bucket: bucket,
        Key: key
    };
    s3.getObject(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            var audioBytes = data.Body.toString('base64');
            const audio = { content: audioBytes };
            const config = {
                encoding: 'LINEAR16',
                sampeRateHertz: 16000,
                languageCode: 'ja-JP',
            };
            const request = {
                audio: audio,
                config: config,
            };

            client.recognize(request).then(
                data => {
                    const response = data[0];
                    const transcription = response.results.map(
                        result => result.alternatives[0].transcript
                    ).join('\n');
                console.log(name);
                console.log(transcription);

                }).catch(err => {
                    console.error('ERROR:', err);
                }
            );
        };
    });
}

最終的にはこんな感じかと↓
image.png

結果

console.log(transcription);で出力してるtranscriptionに文字起こし結果が入ってます。cloudwatch logで確認してみてください。

最後に

参考記事の劣化版みたいな記事になってしまいましたが、備忘録として書かせていただきました。
劣化版なりの改良点としは以下です。
* node_modulesも一緒にzipにして、関数コードに入れようとしたがうまくいかなかったのでlayerから入れた(nodejsというファイル名にするの注意ですね)
* 環境変数としてサービスアカウントの情報を追加する手順の追加
* ソースコードの簡易化と修正

間違い等ある場合はぜひコメントください。
次回はmp3の文字起こしをやってみたいと思います。

参考

S3 + ElasticTranscoder + Lambda + Google Cloud Speech-to-Text APIで、動画の音声を自動でテキストにする

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

コンセプト | webpack

https://webpack.js.org/concepts/ の日本語訳。

コンセプト

webpack は、最新の JavaScript アプリケーションの為の静的モジュールバンドラーです。
webpack がアプリケーションを処理するとき、プロジェクトが必要とするすべてのモジュールを整理して、 1 つ以上のバンドルを生成する 依存グラフ を内部的に構築します。

JavaScript モジュールと Webpack モジュールの詳細については、 こちら をご覧ください。

バージョン4.0.0以降、 webpack はプロジェクトをバンドルするための構成ファイルを必要としません。
しかし、開発者のニーズに合わせて設定することができ、 驚くべきカスタマイズ性 を有しています。
よりよく合うように柔軟に設定することができます。
webpack の利用を開始するには、中核となるコンセプトを理解するだけで十分です。

このドキュメントは、個々のコンセプトの使用例のリンクを提供すると共に、コンセプトの概要を説明することを目的としています。
モジュールバンドラーの背後にある思想と、内部でどのように機能するかについての理解を深めるには、次のリソースを参照してください。

エントリー

エントリポイントは、内部 依存関係グラフ の構築を開始するために Webpack が使用する必要があるモジュールを示します。
webpack は、そのエントリポイントが(直接的および間接的に)依存している他のモジュールとライブラリを特定します。
デフォルト値は ./src/index.js です。
しかし webpack の設定で entry プロパティを指定することで、異なる(または複数のエントリーポイント)を指定できます。例えば:

webpack.config.js
module.exports = {
  entry: './path/to/my/entry/file.js'
};

詳細は エントリポイント のセクションをご覧ください。

出力

output プロパティは、バンドルファイルを出力する場所とファイルの命名規則を webpack に指示します。
デフォルトでは、メインの出力ファイルは ./dist/main.js 、他の出力ファイルは ./dist フォルダーになります。
設定で output フィールドを指定することにより、ファイルの出力先と命名規則を設定することができます。

webpack.config.js
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js'
  }
};

上記の例では、 output.filename プロパティと output.path プロパティを使用して、バンドルの名前と出力先を webpack に指示しています。
一番上にインポートされている path モジュールは、ファイルパスの操作に使用される Node.js のコアモジュール です。

output プロパティは、 もっと多くの設定機能 を有しています。
その背後にあるコンセプトについて学びたい場合は、 output セクションをご覧ください。

ローダー

デフォルトでは webpack は JavaScript ファイルと JSON ファイルのみを認識します。
ローダーを使用すると、 webpack は他のタイプのファイルを処理し、それらを有効な モジュール に変換してアプリケーションにおいて使用し、依存関係グラフに追加することができます。

どのタイプのモジュールでもインポートできることに注意してください。
.css ファイルは webpack に固有の機能であり、他のバンドラーやタスクランナーではサポートされていない場合があります。
開発者がより正確な依存関係グラフを作成できるようにするため、 CSS 言語の拡張は正当化されると私たちは考えています。

webpack のローダー設定には大まかに 2 つのプロパティがあります。

test プロパティは変換するファイルを識別します。
use プロパティは変換を行うために使用するローダーを示します。

webpack.config.js
const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

上記の設定では、 testuse の 2 つの必須プロパティを持つ単一モジュールの rules プロパティを定義しています。
これは webpack のコンパイラに次のように指示します。

「やあ、 webpack コンパイラー。 require()/import ステートメント内の .txt ファイルに解決されるパスに遭遇したとき、バンドルに追加する前に raw ローダーを使用して変換してくれ」

webpack の設定でルールを定義するときは、 rules ではなく module.rules でルールを定義していることを覚えておくことが重要です。
開発者の利益のために、設定が誤っている場合、 webpack は警告を出します。

正規表現を使用してファイルを照合する場合は、引用符で囲まない場合があることに注意してください。
つまり、 /\.txt$/'/\.txt$/' または "/\.txt$/" と同じではありません。
前者は .txt で終わるすべてのファイルに一致するように webpack に指示し、後者は絶対パス '.txt' を持つ単一のファイルに一致するように webpack に指示します。
これはおそらくあなたの意図するところではないでしょう。

ローダーについての詳細は ローダー セクションをご覧ください。

プラグイン

ローダーは特定のタイプのモジュールの変換に使用されますが、プラグインを利用して、バンドルの最適化、アセットの管理、環境変数の注入などの幅広いタスクを実行することができます。

プラグインを使用して Webpack の機能を拡張する方法については プラグインインターフェイス をご覧ください。

プラグインを使用するには、プラグインを require() してプラグイン配列に追加する必要があります。
ほとんどのプラグインはオプションを通じてカスタマイズ可能です。プラグインはさまざまな用途で複数回使用できるため、 new 演算子を使用して呼び出し、プラグインのインスタンスを作成する必要があります。

webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin'); // npm を介してインストール
const webpack = require('webpack'); // 組込みプラグインを利用する

module.exports = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

上記の例では、 html-webpack-plugin は、生成されたすべてのバンドルを自動的に挿入し、アプリケーションの HTML ファイルを生成します。

すぐに使える webpack のプラグインはたくさんあります!
プラグインの一覧 をご覧ください。

Webpack の設定でプラグインを使用するのは簡単です。
しかし、さらに知っておく価値のある多くのユースケースがあります。
詳しくは こちら をご覧ください。

モード

mode パラメーターを developmentproduction 、または none のいずれかに設定することにより、各環境に対応する Webpack の組み込み最適化を有効にすることができます。
デフォルト値は production です。

module.exports = {
  mode: 'production'
};

mode の詳細と、どのような最適化が行われるかについては こちら をご覧ください。

ブラウザの互換性

webpack は、 ES5 準拠 のすべてのブラウザーをサポートしています(IE8 以下はサポートされていません)。
webpackは import()require.ensure() のための Promise を必要とします。
古いブラウザをサポートする場合は、これらの式を使用する前に ポリフィルをロードする 必要があります。

動作環境

webpack は Node.js バージョン 8.x 以降で実行されます。

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

AbortController not defined

Electronでwavesurfer.jsを使おうとしたら、AbortController not defined エラーが起こる。

解決策

Electronのプロジェクトディレクトリにpolyfillをインストール

$ npm install --save abortcontroller-polyfill

wavesurfer.js の頭に以下の行を入れて、importする。

wavesurfer.js
require('abortcontroller-polyfill/dist/polyfill-patch-fetch')

解決

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

TypeORM環境をCLIで構築

背景

前回、sequelizeを利用したコードのtypescript化を試みて、modelの呼び出し元はtypescriptっぽく書くようにできたけど、モデル自体はtypescript化できずに、中途半端な感じで終わってしまった。

https://qiita.com/yusuke-ka/items/244d3dfafb578fd84b1a

sequelizeは、元々typescriptをサポートしていなかったため、typescriptと少し相性が悪いとの記事もいくつか見つけた。

そこで今回は最近伸びてきているTypeORMを試してみようと思う。
こちらは元々typescriptが前提となっているORMなので、typescript化で苦労することはなさそう。

googleトレンドで見ると、sequelizeに追いつく勢いで伸びてきている。
(むしろtypescriptに限定すれば、既にTypeORMの方が人気がある気がする)

image.png

TypeORMのCLIを使った環境構築

DBは以前インストールしたpostgresql(windows)を利用する。
https://qiita.com/yusuke-ka/items/448843020c0406363ba5#%E6%BA%96%E5%82%99

pgadmin4でデータベースインスタンスだけ作っておく。

image.png

データベース名("typeorm"とした)を入力して作成。

ここからは、コード エディタ(VS Code)上での作業。
まずはベースとなるnode環境を作る。

> mkdir typeorm
> cd typeorm
> yarn init

検証なので、とりあえず全部デフォルト設定。

続いて、TypeORMのインストール。
また、DBはpostgresqlを使うのでpg(node-postgres)もインストール。

> yarn add typeorm
> yarn add pg

TypeORMのCLIがyarnで簡単に使えるようにpackage.jsonに以下を追加しておく。

package.json
{
  ...
  "scripts": {
    "typeorm-cli": "typeorm"
  },
  ...
}

TypeORMのCLIを使って、express、postgresqlのTypeORM環境を一気に構築。

> yarn typeorm-cli init --express --database postgres

自動で以下のようなフォルダ/ファイルが生成される。

|- typeorm/
 |- src/
  |- controller/
    |- UserController.ts
  |- entry/
    |- User.ts
  |- migration/
  |- index.ts
  |- routes.ts 
 |- ormconfig.json
 |- tsconfig.json

設定ファイル(ormconfig.json)で接続先DBに合わせて設定を変更。

ormconfig.json
{
  "type": "postgres",
  "host": "localhost",
  "port": 5432,
  "username": "postgres",
  "password": "postgres",
  "database": "typeorm",
  "synchronize": true,
  "logging": false,
  "entities": ["src/entity/**/*.ts"],
  "migrations": ["src/migration/**/*.ts"],
  "subscribers": ["src/subscriber/**/*.ts"],
  "cli": {
    "entitiesDir": "src/entity",
    "migrationsDir": "src/migration",
    "subscribersDir": "src/subscriber"
  }
}

今回は以下だけ変更。

ormconfig.json
 ...
 "username": "postgres",
 "password": "postgres",
 "database": "typeorm",
 ...

これでTypeORMの環境構築は完了。簡単ですね。

TypeORMのCLIで自動構築された環境の確認

CLIで作成すると、サンプルとして"User"というモデルを操作するコードが自動で入っている。

自動生成されたモデル(src/entry/User.ts)は以下のようになっている。

User.ts
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

モデルを定義するときは、これを真似して<モデル名>.tsをentryフォルダ以下に配置すればよい模様。

続いて、コントローラー(src/controller/UserController.ts)は以下のようになっていた。

UserController.ts
import {getRepository} from "typeorm";
import {NextFunction, Request, Response} from "express";
import {User} from "../entity/User";

export class UserController {

    private userRepository = getRepository(User);

    async all(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.find();
    }

    async one(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.findOne(request.params.id);
    }

    async save(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.save(request.body);
    }

    async remove(request: Request, response: Response, next: NextFunction) {
        let userToRemove = await this.userRepository.findOne(request.params.id);
        await this.userRepository.remove(userToRemove);
    }

}

ここではモデルに対する操作を書く感じですね。
全件取得、1件取得、保存(作成/更新)、削除の機能がサンプルとして実装されている。

getRepository(Hoge)で取得されるRepositoryをTypeORMが提供してくれているので、このRepositoryのメソッド(findやsaveなど)を呼び出して、DBにアクセスしているっぽい。

次は、index.ts。expressを使用する感じで自動生成されている。

index.ts
import "reflect-metadata";
import {createConnection} from "typeorm";
import * as express from "express";
import * as bodyParser from "body-parser";
import {Request, Response} from "express";
import {Routes} from "./routes";
import {User} from "./entity/User";

createConnection().then(async connection => {

    // create express app
    const app = express();
    app.use(bodyParser.json());

    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined);

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });

    // setup express app here
    // ...

    // start express server
    app.listen(3000);

    // insert new users for test
    await connection.manager.save(connection.manager.create(User, {
        firstName: "Timber",
        lastName: "Saw",
        age: 27
    }));
    await connection.manager.save(connection.manager.create(User, {
        firstName: "Phantom",
        lastName: "Assassin",
        age: 24
    }));

    console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results");

}).catch(error => console.log(error));

routes.tsに定義されているRouteをforEachで回しているようなので、新しいAPIを追加するときには、このコード(index.ts)は変更せずに、routes.tsに定義を追加すれば良さそう。

最後のほうにある
// insert new users for test
以下はサーバー起動時にテストデータを入れているコードのようなので、実際に利用する際には消しておいた方がよさそう。

というか必要ないので、さっそく消しておく。

index.ts
import "reflect-metadata";
import {createConnection} from "typeorm";
import * as express from "express";
import * as bodyParser from "body-parser";
import {Request, Response} from "express";
import {Routes} from "./routes";

createConnection().then(async connection => {

    // create express app
    const app = express();
    app.use(bodyParser.json());

    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined);

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });

    // setup express app here
    // ...

    // start express server
    app.listen(3000);

    console.log("Express server has started on port 3000. ");

}).catch(error => console.log(error));

routes.tsを見てみる。これも自動生成されている。

routes.ts
import {UserController} from "./controller/UserController";

export const Routes = [{
    method: "get",
    route: "/users",
    controller: UserController,
    action: "all"
}, {
    method: "get",
    route: "/users/:id",
    controller: UserController,
    action: "one"
}, {
    method: "post",
    route: "/users",
    controller: UserController,
    action: "save"
}, {
    method: "delete",
    route: "/users/:id",
    controller: UserController,
    action: "remove"
}];

サンプルとして、全件取得、1件取得、追加(更新)、削除の4つのAPIが定義されている模様。

最後に設定系のファイルを見てみる。

typeORMの設定ファイル(ormconfig.json)は、先ほど見たので、package.jsonから。

package.json
{
   "name": "typeorm",
   "version": "1.0.0",
   "main": "index.js",
   "license": "MIT",
   "scripts": {
      "typeorm-cli": "typeorm",
      "start": "ts-node src/index.ts"
   },
   "dependencies": {
      "typeorm": "0.2.25",
      "reflect-metadata": "^0.1.10",
      "pg": "^7.3.0",
      "express": "^4.15.4",
      "body-parser": "^1.18.1"
   },
   "devDependencies": {
      "ts-node": "3.3.0",
      "@types/node": "^8.0.29",
      "typescript": "3.3.3333"
   }
}

scriptsstartが追加されている。これでサーバーを実行する模様。
あとは、必要な依存が自動的に追加されている。

tsconfig.jsonも確認。

tsconfig.json
{
   "compilerOptions": {
      "lib": [
         "es5",
         "es6"
      ],
      "target": "es5",
      "module": "commonjs",
      "moduleResolution": "node",
      "outDir": "./build",
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "sourceMap": true
   }
}

typeORMのREADMEに書いてある設定が自動的に入っているようです。
https://github.com/typeorm/typeorm#installation

TypeORMのサンプルコードを実行してみる。

TypeORMのコードを実行してみようと思う。

まずは、モデルをDBに反映してみる。

migration:generateを実行すると、DBの内容とモデルを比較してマイグレーションファイルを作成してくれるようなので、これを実行してみる。

> yarn typeorm-cli migration:generate -n UserMigration

エラーが発生した。

yarn run v1.22.4
$ typeorm migration:generate -n UserMigration
Error during migration generation:
D:\study\orm\typeorm\src\entity\User.ts:1
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
^^^^^^

SyntaxError: Cannot use import statement outside a module
...

本家サイトのここを参考にしてやってみる。
https://github.com/typeorm/typeorm/blob/master/docs/using-cli.md#installing-cli

package.jsonのscriptsを以下のように変更(typeormのscriptを追加)。
(ts-nodeは自動でインストールされているはずだけど、もし入っていなければ、インストールする必要があるかも)

   ...
   "scripts": {
      "typeorm-cli": "typeorm",
      "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
      "start": "ts-node src/index.ts"
   },
   ...

再度実行してみる。
(今度は、yarn typeorm-cliではなく、yarn typeorm)

> yarn typeorm migration:generate -n UserMigration

今度は成功。

yarn run v1.22.4
$ node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -n UserMigration
Migration D:\study\orm\typeorm/src/migration/1593226939171-UserMigration.ts has been generated successfully.
Done in 2.03s.

こんな感じでマイグレーションファイルが生成された。

1593226939171-UserMigration.ts
import {MigrationInterface, QueryRunner} from "typeorm";

export class UserMigration1593226939171 implements MigrationInterface {
    name = 'UserMigration1593226939171'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "age" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP TABLE "user"`);
    }

}

DBに反映させてみる。

> yarn typeorm migration:run

userテーブルが作成された。

image.png

サーバーを起動。

> yarn start

今回もchromeの拡張ツール「Advanced REST client」で動作確認。
http://localhost:3000/users に各種リクエストを送ってみる。

routes.tsに定義されているRouteを見てURLを指定。

まずは全件取得。

image.png

何も登録してないので、結果は空の配列。

つづいて、ユーザーを登録してみる。

image.png

application/jsonで指定したパラメータでユーザーが登録された。

DBにも登録されている。

image.png

同様にもう一人ユーザーを追加した後、再度全件取得してみる。

image.png

今度は配列が空じゃない状態で返ってきた。

パスにIDを指定して取得。

image.png

1件だけ返ってきた。

データ更新。
パラメータにID(integer)を指定してPOSTリクエストを送る。

image.png

データが更新された。

最後に、削除を試してみる。

image.png

DBからデータが消えているようだが、レスポンスが返ってこない。

index.tsでresultがundefinedの時にレスポンスを返していないのが原因かと思われる。

index.ts
    ...
    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined); // ← ココ

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });
    ...

試しに200を返すように書き換えてみる。

index.ts
    ...
    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : res.send(200)); // ← ココ

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });
    ...

他のIDを指定して削除を再実行。

image.png

今度はレスポンスが返ってきた。
修正が正しいかどうかは置いといて、原因は特定できた。

さいごに

今回はTypeORMの環境構築を試してみた。

いくつかハマりポイントはあったもののsequelizeをtypescript化するよりは遥かに楽だったし、モデルもコントローラーもすべてtypescriptで書けそうなので、sequelizeの時のように中途半端な感じにならないのも良さげ。
(もう少し時間をかけて調べれば、sequelizeもいい感じにtypescript化できる方法があるのかもしれないけど…)

sequelizeほど成熟していない感はあるものの、typescriptで書くことが前提ならTypeORMの方がやりやすい気がします。

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

【AWS】VPC内のLambdaから別のLambdaを呼び出す

ちょっと前にやったことなので記憶があいまいですが、
今後もやる可能性のある事なので備忘録として残しておこうと思います。

VPC内で実行するLambda

AWSのEC2やRDSなどのVPC内のリソースにLambdaでアクセスする場合、
LambdaにVPC設定をすることで可能になります↓

【AWS】LambdaをVPC内で実行し、EC2のMySQLにアクセスする
https://www.geekfeed.co.jp/geekblog/lambda_vpc

しかし一方で、VPCにpublicなサブネットを設定していない場合、VPC内Lambdaからは外部のネットワークに接続することができなくなります。
つまり以下のアクセスが不可能となります。

  1. VPC内Lambda→VPC内Lambda
  2. VPC内Lambda→VPC外Lambda
  3. VPC内Lambda→VPC外リソース(PrivateLinkがサポートされているもの以外)

例えば、VPC内リソースからデータ取得後、そのデータを別リソースに保存したりする場合などに
データ取得Lambda→データ保存Lambdaというように連携することがあると思いますが、
LambdaからLambdaを呼び出す場合もネットワーク経由で行うため、これができなくなってしまいます。
※Lambdaを呼び出そうとしたときにTimeoutになります。

今回はこれを解決し、VPC内Lambdaから外部リソースへのアクセスができるように設定したいと思います。

方針

PrivateLinkによる外部サービスアクセス

まず、冒頭に書いた「3. VPC内Lambda→VPC外リソース」ですが、
AWSのサービスでPrivateLinkがサポートされている場合、Publicサブネットの設定などの面倒な設定はせずに
VPC内Lambdaから外部サービスへのアクセスが可能です。

例えば、EC2, CloudWatch, SNSなどの多くのサービスがPrivateLinkにサポートされています。
PrivateLinkがサポートしているサービスはこちらを参照

が、Lambdaは無いです。

そこで、Lambdaやその他の外部リソースにアクセスできるようにするため、
VPCそのものに設定を加えていきたいと思います。

構成

VPCにNAT Gatewayを持つPublicサブネットを作成し、PrivateサブネットからそのNAT Gateway経由でアクセスします。
tempsnip.png

Publicサブネット+NAT Gatewayの設定

VPCを作成した時点でPrivateサブネットは作成されているはずですので、
Publicサブネットの作成からしていきたいと思います。

①Publicサブネットの作成

普通にサブネットを作成します。わかりやすいようにNameタグを設定しておきます。
AZの指定ですが、すでにVPC Lambdaで設定しているPrivateサブネットが存在するAZを指定しておきます。
キャプチャ.PNG

②NAT Gatewayの作成

NAT Gatewayを作成します。
①で作成したPublicサブネットを指定します。
2.png

③ルートテーブルの設定

さてここまでprivate, publicと言ってきましたが、
AWSの参考記事にも書かれている通り、

注: サブネットがプライベートであるかパブリックであるかは、そのルートテーブルにより決まります。パブリックサブネットにはインターネットゲートウェイを指すルートがあり、プライベートサブネットにはありません。

です。
つまり以下の手順によって
NAT Gatewayを設定したサブネットがPrivateサブネットになり、
Internet Gatewayを設定したサブネットがPublicサブネットになります。

Privateサブネットを関連付けたルートテーブルの作成

デフォルトで作成されているルートテーブルがあると思いますが、新規に作成しています。
NAT Gatewayを設定します。
public.png

Publicサブネットを関連付けたルートテーブルの作成

VPC作成時のInternet Gatewayを設定します。
private.png

以上で設定終了です。

アクセスの流れ的には
VPC内Lambda (private subnet) → NAT Gateway (public subnet) → Internet Gateway → 外部
って感じですかね?

AWS歴3か月の雑魚なので認識間違ってたらすみません。

lambdaの呼び出し方

通常のlambda呼び出しと変わりません。
ランタイムはNode.jsです。

lambda.js
const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();

exports.handler = function(event, context, callback) {

    // ペイロード。呼び出すLambda関数に受け渡す引数的な。
    let payload = {
        "hoge":"huga"
    };

    // ペイロードをStringにする。
    payload = JSON.stringify(payload);

    // invoke引数設定
    let params = {
        FunctionName:"実行するlambda名",
        InvocationType:"RequestResponse",
        Payload:payload
    };

    // Lambda関数呼び出し
    lambda.invoke(params, function(err, data){
        if(err){
            context.fail(err);
        }
        context.succeed(data);
    });

}

備考

参考のAWSリンクにもありますが、
private、publicのサブネットを持ったVPC作成を一発でやる方法があります。
用途がはっきりしているVPCを作成する場合はこれも使えるかも。

参考

How do I give internet access to my Lambda function in a VPC?
VPC内のLambdaからインターネットにアクセスする

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