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

TwitterでTILしたらGitHubに草が生える

$
0
0

はじめに

TIL(Today I Learned): 今日学んだこと

をtwitterのつぶやきで行おうと思いました

  1. #tilというハッシュタグで学びをつぶやく
  2. 1日1回自分の投稿の #tilハッシュタグを拾いに行く
  3. GitHubにコミット

というのがいけないかな〜と思ったのがきっかけです

AWSで CloudWatch Events + Lambda Functionsあたりでできそうな気がしたのでやってみます

参考

準備

  • AWSアカウント
  • Twitter開発者アカウント
  • TILをコミットしていくGitHubリポジトリ

Twitter APIを利用するためには開発者アカウントの登録が必要です

※ 利用目的とか審査とかあってやや面倒です…

手順

til-twitter.png

  1. Amazon CloudWatch Eventsで定期的にAWS Lambdaを起動
  2. LambdaでTwitter APIを叩いて #tilツイートを取得
  3. #tilツイートがあればGitHub APIを叩いてコミットする

アプリケーションの動きとしては上記の流れを想定します

なのでやらないといけないことは、

  1. Lambda Functionsの作成
    1. Twitter APIを叩いて #tilツイートを取得するjsを作る
    2. GitHub APIを叩いてコミットするjsを作る
    3. Lambdaに登録する
  2. 定期的にLambda Functionsを実行するCloudWatch Eventsの作成

という感じになります

1. Lambda Functionsの作成

最終的にはこんなjsができあがりました

https://github.com/halnique/til-twitter/blob/master/index.js

色々詰め込み過ぎだし改善の余地は大いにありそうですが、中身を見ていきます

1-1. Twitter APIを叩いて #tilツイートを取得する

Twitter開発者アカウントを登録してアプリ作成を行うと、以下の値が得られます

  • Consumer API key
  • Consumer API secret key
  • Access token
  • Access token secret

これらを環境変数から設定し、twitterモジュールを利用します

constTwitter=require('twitter');consttwitterClient=newTwitter({consumer_key:process.env.API_KEY,consumer_secret:process.env.API_SECRET,access_token_key:process.env.ACCESS_TOKEN,access_token_secret:process.env.ACCESS_TOKEN_SECRET,});

あとはTwitter APIのドキュメントを見ながら、特定のツイートを取得するように実装します

constnowString=`${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()-1}`;constparams={q:`(from:${process.env.ACCOUNT_NAME}) since:${nowString}${TARGET_HASHTAG}`,count:process.env.MAX_COUNT||5,};consttweets=awaittwitterClient.get('search/tweets',params).catch(()=>[]);

ACCOUNT_NAMEは自分のTwitterアカウントを環境変数で設定します

最終的には毎日日付が変わったタイミングぐらいで実行させるので、前日以降のツイートに絞っています

※ jsの日付処理貧弱すぎない?みんなmoment.jsとか使うの?

実際はこのあとに各ツイートのハッシュタグを厳密にチェックしてますが、だいたい↑ぐらいでお目当てのツイートは取得できるはずです

1-2. GitHub APIを叩いてPRを作成する

これが地味に大変だった…

CLIなら git addして git commitして git pushするだけのかんたんなお仕事ですが、APIでやろうとするとある程度踏み入った理解が必要になります

流れとしては blobを作って treeを作って commitを作って HEADのSHAを書き換えるということになります

constprevRefsHead=awaitgetRefsHead();constcommitSha=prevRefsHead.object.sha;constprevCommit=awaitgetCommit(commitSha);constblob=awaitpostBlob(data[i]);consttree=awaitpostTree(prevCommit.tree.sha,blob.sha,i+1);constcommit=awaitpostCommit(commitSha,tree.sha,i+1);awaitpatchRefsHead(commit.sha);awaitsleep(1);

中身はそれぞれ対応したAPIを実行しているだけですが、こちらもドキュメントとにらめっこしながらパラメータと流れを調節しました

※ 連続して実行したときにコミットがうまくいかないことがあったので、1件ずつsleepするようにしてます

1-3. Lambdaに登録する

jsは何度もお試し実行すると思うので、ローカルでDockerとかで書くのがよいです

jsができあがったらLambdaに登録していきます

注意点として、Lambda上で外部モジュールは基本的にそのまま requireすることはできません

今回でいうと

constTwitter=require('twitter');

ですね

これは事前に Lambda Layersとして作成しておくことで解決できます

image-lambda-layers1.png

zipファイルを直接アップロードするか、 S3に上げておいてそれを利用することができます

zipファイルの中身は注意が必要で、例えば modules.zipを解凍したときに以下の構成になっている必要があります

$ ls -1 modules/
node_modules/

node_modulesの中に利用したいモジュールが入っているイメージです

この構成になっていないと、Lambda Functionsの方で利用するのにうまくいきません

先にLambda Layersを作ったら、Lambda Functionsを作成していきます

image-lambda1.png

ランタイムはLambda Layersと同じになるようにします

Lambda FunctionsもzipファイルをアップロードしたりS3のファイルを利用することができますが、今回はそのまま作成したコードを貼り付けます

image-lambda2.png

環境変数をたくさん使うので、ぽちぽち登録します

image-lambda3.png

タイムアウト設定をデフォルトの3秒 -> 30秒に変更しておきます

image-lambda4.png

最後にLambda FunctionsにLambda Layersを追加すればOKです

image-lambda5.png

右上から適当なテストイベントを作ってテストしてみて、無事に動けばLambdaとしては完成です

2. 定期的にLambda Functionsを実行するCloudWatch Eventsの作成

image-cloudwatch-events3.png

image-cloudwatch-events2.png

以上

簡単に設定できました

Cron式については、日付が変わったころに前日分のツイートを取得するような感じで実行されるようにします

CloudWatch EventsのCron式で実行されるイベントは、UTCで誤差1分以内だそうです

前日に #tilツイートをしていれば、日本時間でだいたい翌日の朝9時頃にコミットができあがる想定ですね

実行結果

image-tweet1.png

image-commit1.png

コミットされました

これで無事にツイートするだけで草が生える環境ができあがりました

学び

  • node_modulesを Lambda Layersに追加しておくことで、Lambda Functionsで使えるようになって便利
  • Gitのコミットができるまでの流れ 10.2 Git Internals - Git Objects
  • CloudWatch EventsのスケジュールでCron式を使う場合、日と曜日のどちらかは ?にする必要がある

Todo

  • Lambda FunctionsとLambda Layersへのデプロイを GitHub Actionsで自動化したい人生だった…
  • GCPで Cloud FunctionsCloud Schedulerでも似たようなことができそうなのでやってみたい

まとめ

GitHubの草が生えているからといって活発に開発をしているとは限らないぞ


Firebaseを使ってみたので、所感を語ってみる

$
0
0

目的

Firebaseって何?
使ってみたいけどどんなものなの?って人向け

ざっくりFirebaseってどんなものなのか?

ざっくり説明します。
Googleが提供するBaasの一種。
Node.jsを利用したWebアプリケーションのバックエンドサービスです。
と言っても何を言っているのかわからないと思います。

Firebaseの基本的な構想としては(と言うよりもBaasの構想かな?)バックエンドの開発が必要ないサービスです。
例えば、ログイン機能。
Authenticationの機能がFirebase側で用意されていますので、フロントでFirebaseのAuthenticationを記述します。
そうするとあら簡単、ログイン機能完成です。

こう言ったバックエンドサービスを提供しているのがFirebaseなんですよ。って理解で良いと思います。

Node.jsとは?

Node.jsとは、javascriptで開発可能な、サーバーサイド向けのプラットフォームのことです。
プラットフォームである。と言うことが重要です。フレームワークではありません。
簡単に言うと、Javascriptのコードを解釈して、バックエンドで動作させることができるのがNode.jsです。
実行する機能を持っているので、フレームワークとは明確に異なります。(単純なAPIの集合体ではないってことです。)

Node.jsの利点

フロントもバックエンドも同じ言語で記述できる。

Javascriptをかけるエンジニアだけ集めればいいじゃん!
 → 人員を集めるハードルが下がる。
  → そして地球は平和になる。

シングルスレッドで、かつ非同期で動作するJavascriptをサーバー側でも動作させることができる。

多くの端末からアクセスされるシステムに強い!(C10K問題の解消)
 → スマホアプリなんかの開発が楽になる!
  → そして地球は平和になる。

ここがすごいよFirebase

バックエンドの処理がいらない!

上の説明と重複しますが、以下のようなシステムの場合を考えてみましょう。
「ログイン」
「会員登録」
「商品登録」(写真アップロード)


etc

こんなシステムの場合、バックエンドの処理が一切必要ありません。

ログイン・・・Firebaseが用意している関数を利用すれば即実装可能。
DBへの登録・・・フロント側からDBの保存処理用の関数を呼べば登録可能。
写真のアップロード・・・Storage(AWSでいうとS3みたいな機能)への保存処理をフロントから呼ぶ



みたいな感じですので、フロント側の処理でほとんどの処理が実装可能です。

ミドルウェアが一切必要ない!

これすごいんすよ。
一番感動したのが、ローカル環境でコマンドラインから

firebase serve

って打つと、ローカルからFirebaseのDBを勝手に参照してくれるんです。
各自で環境を用意する必要が一切ない。

Node.js入れて、firebaseCli入れて、firebase loginして実行すればどこでも誰でも環境完成です。
configで参照先編集して、とかvagrantの環境を自分用に編集してとか、ファイアーウォールの設定してとか諸々の準備が必要ありません。

運用費やっす!

小規模のスマホアプリやWebアプリなら断然安いです。
企業様向けのシステムでも小規模システムなら運用費数百円〜千円前後で済むこともあります。

大規模になると、多分高くなります。(従量課金なので)
見積もったことないのでわからないんですが、大規模な開発はそもそもBaasの基本設計思考から外れるんじゃないのかな?と個人的には思っています。

ここが困るよFirebase

RDBMSとは違う!どっちかっていうとKVSに近いDB!

つまりこういうことです。

・条件文に否定が使えない。
・条件文にorが存在しない。(最新のアップデートで使えるようになったんでしたっけ?)
・Group Byが使えない
 etc

RDBMSに慣れていると、すっごく不便に感じます。

解消方法としては、Algoriaってツールが代表的ですね。
Algoriaに関しては・・・ざっくり言うと、全文検索を実現してくれるツールです。
使用感としては、AWSのElasticsearchと似た感じなのかなぁ。
※Algoriaの導入にはお金がかかります。

Functionsで拾えないエラーがある。

Functionsっていうバックエンドで動かしたい場合に利用する機能があります。(例えばバッチ処理とか、他システムからのAPI通信とか)
ここでエラーログが出力されなくて困りました。

今回本問題にぶつかったのは、恐らくメモリが足りなくて怒られているんじゃないか?という予想をしたエラー。
本当の原因はまだ分かっていないんですが、何故か処理が途中で止まる。エラーログも吐き出されない。
本当に困りました。メモリを上げて実行してみたら正常に動作しましたので、一応解決という方針にしましたが。。。

Firebaseの導入すべきプロジェクト

独断と偏見による 導入するならこんなプロジェクトじゃない?

・リアルタイムで通信が必要なシステム
・接続クライアントが多いシステム
・小規模なプロジェクト

独断と偏見による 導入すべきではないプロジェクト

・大規模なプロジェクト
・複雑なデータ管理が必要なプロジェクト

まとめ

私が感じた個人的なFirebaseの強みは、バックエンドの処理が必要ないこと。
逆にいうと、必要機能はFirebaseが用意してくれているので、Firebaseを超えた機能を使いたい場合、または、既存機能を使わず、カスタマイズして利用したい。などは非常に大変だと思います。

つまりカスタマイズ性が低い。
その点で言うと、AWSはカスタマイズ性が高いです。できないことがほぼ無い。ミドルウェアだって用意し放題だし。

しかしながら、小規模開発、またはリアルタイム通信を沢山行いたい。と言う案件であれば、機能、料金的にみても、Firebaseがオススメです。
(と言うかC10K問題にぶつかるんならNode.js利用が良いのですが。)

因みに、私が個人的にアプリ開発をするとなれば、Firebaseを使います。
圧倒的な運用費の安さが魅力です。
企業様からの案件だったら、案件内容にもよりますが、要望に答えやすいように、AWSをオススメしちゃうかなー。
まぁでも、提案内容にもよりますが、機能的な制限はAWSよりはありますが、Firebaseを選ぶ企業様も多いんじゃないかなー。だって安いし。

ってのが個人的な所感です。

ゼロから始めるtoio.js 買ったばかりのMacbook Proでサンプルを動かすまで

$
0
0

これは「toio™(ロボットトイ | toio(トイオ)) Advent Calendar 2019」の18日目の記事になります。

はじめに

先日記事に書いたtoio.jsの作例は全てwindows PC上で開発しました。
ソフトウェア初心者がtoio.jsで作ってみた 5つの作例紹介

最近別件でiOS App開発を始めたいなと思い、新しくMacbook Proを購入しました。
せっかくなので、Macでもtoio.jsを動かしたいということで、環境構築を行いました。
備忘録も兼ねて、サンプルを動かすまでを記録に残しておきます。

使用機種とmacOSバージョン

  • Macbook Pro 13インチ 2019年モデル (A2159)
  • macOS 10.15.1

1.Node.js環境のインストール

toio.js公式によると、Node.js version 8以降が必要なようです。
スクリーンショット 2019-12-10 15.58.49.png

今回はバージョン依存の問題にはまった時のことを考えて、
Node.jsのバージョンを自由に変更できるように、nodebrewを入れます。

homebrew -> nodebrewの順にインストールを行った後、
所望のバージョンのNode.jsをnodebrewを使ってインストールします。

1-1 homebrewのインストール

homebrewはmacOS用パッケージマネージャーです。
homebrew 公式HP

公式HP上にあるインストール用コマンドを一行打つだけで、インストールが完了します。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

※更新がかかるかもしれないので、公式HPのコマンドを参照してください。

途中でログイン用のpasswordが求められるので入力して進めます。

1-2 nodebrewのインストール

次はnodebrewのインストールを行います。
nodebrewを使うと、nodeのバージョンを細かく選択して使用することができます。

参考にした記事 NodebrewでNodeをインストールする

上記サイトに従い、インストールと環境変数の追加を行ってください。

!!重要!!
環境変数のPATHを通さないと以降の手順でNode.jsのインストールに失敗します。

1-3 Node.jsのインストール

nodebrewを使ってNode.jsのインストールを進めます。
今回は公式でサポートしている最小バージョンの8を入れてみます。

インストールできるNode.jsのバージョンは以下のコマンドで確認できます。

$ nodebrew ls-remote

たくさんのバージョンが出てきますが、今回はバージョン8の中の最新版、v8.16.2を入れてみます。
nodebrew installというコマンドもありますが、nodebrew install-binaryの方が早いという記事があったので、こちらのコマンドを使用します。

$ nodebrew install-binary v8.16.2

インストールしただけでは、使用する状態になっていません。
試しに現状のnodeのバージョンを調べるために、"nodebrew ls"というコマンドを打ってみます。
current: none となっており、これは現状使用するバージョンが指定されていないことを示しています。
"node -v"というNode.jsのコマンドでバージョン確認してみても、そもそもnodeのコマンド自体が認識できない状態です。

$ nodebrew ls
v8.16.2

current: none

$ node -v
-bash: node: command not found

使うバージョンを以下のコマンドで指定します。

$ nodebrew use v8.16.2
use v8.16.2

改めて確認してみると、
current: v8.16.2
となっており、使用するバージョンが正しく設定できていることがわかります。
"node -v"で確認しても、正しくバージョンが帰ってきてます。これでNode.jsの準備は完了です。

$ nodebrew ls
v8.16.2

current: v8.16.2

$ node -v
v8.16.2

2 toio.jsのインストール

toio.js公式情報に従い、以下のコマンドを打っていきます。
たったこれだけで見事、toio.jsの環境構築が完了しました。

npm install -g yarn
git clone https://github.com/toio/toio.js.git   # clone repository
cd toio.js                                      # move to repository root
yarn install                                    # install dependencies
yarn build                                      # build @toio/* packages
yarn example:<name of example>                  # start sample application (see below)

最後に

windowsでの環境構築に比べ、とても簡単に環境構築が完了して感無量でした。
ここまで設定が完了したら、前回の記事の作例中のコードを動かすことができますので、是非試してみてください。
ソフトウェア初心者がtoio.jsで作ってみた 5つの作例紹介

私の作ったサンプル実行手順

  • toio.js/examplesの中にディレクトリを作る(名前は任意)
  • index.jsの名前でソースコードを保存する
  • 以下コマンドを打って、実行する
$ node index.js

AzureFunctionsをJavascriptで構築する時の嵌りどころ

$
0
0

はじめに

Javascript初心者がAzureFunctionsの開発を行った際に嵌ったポイントをお伝えしたいと思います。

前提

実際の案件概要は以下のような感じです

  • 概要:検証用のPoCアプリの開発
  • 構成:SPA(Vue.js) + AzureFunctions + CosmosDBでのサーバレスアプリケーション(基本PaaSで)
  • ローカル環境:Windows + VisualStudioCode
  • 言語:Javascript(フロントエンドとバックエンドで統一したかった)
  • 開発時期:2019年8月~2019年11月 ※本記事で書いている内容は現在時点でも修正されているかもしれませんし、今後も修正される予定のものも多いので、あくまで参考程度に見てもらえると助かります

本記事で触れるテーマ

  • ベースOSの選択
  • ローカル環境構築について
    • Proxyを突破する技術
  • Lintの設定
  • Bindingsの落とし穴

ベースOSの選択

Functionsのリソースを作成する際、事情がない限りはWindowsを指定する形で良いと思います。
Azureの開発速度は凄まじいですが、優先順位はとしてはどうしてもWindows > Linux、 C# > その他の言語になると思います。
(ドキュメント量や対応済・未対応のIssueの数から鑑みて)

嵌ったポイント①:LinuxOSを選択

個人的にはLinuxOSの方が慣れていたので、今回の開発でもLinuxを使おうと思い、インフラ担当の方にLinuxでリソースを作製していただきました。

WebApps

AzureWebAppsでフロントエンドを作成した際、ベースOSとしてlinuxを選択してはまりました。
【事象】アプリをリリースしても資産が置き換わらない
【対策】資産の中に入ってコマンド打って修正する
【参考URL】https://stackoverflow.com/questions/54236862/cannot-get-index-html-azure-linux-web-app

上記のstackoverflowを見てヒイヒイ言いながらexpressの設定をしたおかげで、なんとか対応すること出来ました

Functions

今回CosmosDBを利用する必要があったため、Functionsを利用する際に
ExtensionBandlesをインストールする必要がありました。
【参考URL】https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-register

LinuxベースだとHTTPTriggerとTimerTriggerの選択肢しか表示されないため、手動インストールができません(Windowsだと出てきます)

色々試行錯誤しましたが解決策がわからなかったため、結局リソースをWindowsベースで作り直してもらうことで対応しました。

将来的には改善されるかもしれませんが、特別な理由がない限り現時点でのベースOSはWindowsで良いと思います(そもそもOSを意識していては、折角のサーバレスが台なしな感じもしますが・・・)

ローカル環境構築について

次に嵌まったポイントとしてはローカル開発環境の構築です。

基本的にIDEとしてはVisualStudioCode(以下vscode)を使います。
素直にMicrosoft製品で固めます。

基本的にはazure-functions-core-toolsnpm installすればローカルのデバッグはできるのですが、
やはりというか、認証Proxyで引っかかりました。
認証Proxyの仕様については各社によって差異があるので、以下のやり方で突破可能か不明ですが、ある程度参考にしてもらえたらと思います。

認証Proxyのどこで引っかかるのか

AzureFunctionsを起動する際、ローカルでもクラウドでもExtension.Bandle(C#で書かれたライブラリ)が必要になります。
AzureFunctions自体は様々な言語で開発することができますが、基本的にはExtension.Bandleのライブラリを呼び出して様々な処理を実行させています。

  1. func startコマンドでFunctionsを起動する
  2. ローカルでExtension.Bandleを探す
  3. ない場合、ダウンロードしに行く
  4. Extension.Bandleのダウンロードが完了次第、各Functionが起動する

認証Proxy化で引っかかるのは3の部分です。
Extension.Bandleをダウンロードする処理はazure-functions-core-toolsの機能の中で行われるのですが、これがおそらくC#で書かれている処理のため、ローカルで環境変数にProxyを設定しても処理が途中で止まり、タイムアウトとなってしまいました。

ExtensionBundlesの入手方法

ではどうしたか、というとAzure Functionsの実行に必要なExtensionBundles(前回梅田がStorage経由でお渡ししたモジュール群)をネット経由で入手する方法が分かりました。
以下のGitHubのページからリリース版のzipファイルをローカルにダウンロードし、ローカルのC:\Users\[ユーザ名]\AppData\Local\Temp\Functions\ExtensionBundlesの配下(パスについてはユーザごとに変わる可能性がありますので、VSCODEの画面からご確認ください)に解凍後のフォルダを置くことで、ExtensionBundleを用いたFunctionsの実行が可能となります

https://github.com/Azure/azure-functions-extension-bundles/releases

これでなんとかローカルでデバッグができる状況まで漕ぎつけました。

Lintについて

ESLintのデフォルトのルールを採用すると、Azure Functionsの性質上、必ず通せないエラーが発生します。
https://github.com/eslint/eslint/issues/11723

Functionsを記述する場合、引数として、contextを指定します。これはお約束

module.exports = async function(context, req) {

そしてDBやBLOB、Queueに結果を出力しようとすると以下のような記述になります。

context.bindings.XXXX = AAAAA

このような記述をするとLintのルールの再代入にあたるため、エラーとなってしまいます

今回私はルールの方を無効化する対応をとってしまいましたが、
functionsをreturn文で終わらせる書き方をすると、ここのLintのルールを守りつつコードを書くことが可能になると思います。

【参考URL】https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-reference-node

Bindingsの落とし穴

AzureFunctionsを使う上ではfunction.jsonに記述するBindings指定が本当に便利です。
わざわざDBコネクションを確立したり、オブジェクトストレージのパスを気にしたりみたなところに気を遣わずにプログラムを作成することができます。
※Bindingsを使わない場合、各関数にライブラリをimportしないといけなくなるため、処理を軽量化することが難しくなります
【参考URL】https://github.com/MicrosoftDocs/azure-docs.ja-jp/blob/master/articles/azure-functions/functions-bindings-cosmosdb.md#tab/javascript

function.jsonでCosmosDBにつなぐ際、SQL文を書かずともある程度データを選択して抽出することができます。

書き方は以下のようになります。

{
"type": "cosmosDB",
"name": "index.jsの中で使う変数名",
"databaseName": "DB名",
"collectionName": "コレクション名",
"connectionStringSetting": "DB接続文字列",
"direction": "in",
"id": "{hugahuga}",
"partitionKey": "{hogehoge}"
},

この書き方をすると、Pertition=hogehoge内のid=hugahugaのデータをオブジェクトとして抽出してくれるので非常に便利です。

が、実際に動かすうえでは以下のような制約がありました
- id指定とPertition指定は同時にやらないといけない
- id指定のみだと実行時にエラーとなる
- Pertition指定のみだとエラーにならないが、実際はPertition指定が効いてないため、全件検索と同じになってしまう

対象データをidで一意に特定する場合は、idとpertitionKeyの併用は良いかもしれませんが、素直にSqlQueryパラメーターを使った方が良いかもしれません

最後に

つらつらとめんどくさかったことばかり書いていますが、
サーバレスによるアプリ開発は開発が不慣れな人でもある程度の速度を持って開発できるし、インフラやミドルレイヤを気にしなくてよいなど良いこともたくさんあります!
Javascriptは個人的にはとっつきやすい言語(フロントが絡むとまた別かもしれませんが)でした。
ぜひとも、Javascript + AzureFunctionsによるサーバレスデビューを!

【kintone】 APIトークンの自動生成

$
0
0

まえがき

レコードやアプリの操作は、kintone REST APIでほぼほぼ実装できます。
しかし、通知やAPIトークンといった一部設定値については、kintone REST APIでサポートされておりません。
cybozu developer networkのナレッジノート記事で紹介したPuppeteerを使うと、そんなかゆい所に手が届いたりします。
今回は、APIトークンを自動生成する例を紹介します。

デモ

Puppeteerを使って、APIトークンを自動生成します。
demo.gif
生成したAPIトークンが利用可能か、kintone コマンドラインツールを用いて検証しています。
オプションの「-d」にドメイン名、「-a」にアプリID、「-t」にAPIトークンを指定しています。

コード

Puppeteer、readline-syncを利用しています。 npm等を用いてインストールしてください。

・sample.js (実行コード)

(async()=>{constreadline=require('readline-sync');//readline-syncの読み込みconstpuppeteer=require('puppeteer');//puppeteerの読み込み//設定値constdomain='****.cybozu.com';//kintoneのドメインconstbasicUser=false;//Basic認証のユーザー名(設定していない場合はfalse)constbasicPassword=false;//Basic認証のユーザー名(設定していない場合はfalse)constuser='****';//kintoneのログインユーザー名constpassword='****';//kintoneのログインパスワードconstappId=readline.questionInt('App ID: ');//アプリのIDconstaccessRights=[];//追加するAPIトークンに与えるアクセス権accessRights[0]=readline.keyInYN('Record view right: ');accessRights[1]=readline.keyInYN('Record add right: ');accessRights[2]=readline.keyInYN('Record edit right: ');accessRights[3]=readline.keyInYN('Record delete right: ');accessRights[4]=readline.keyInYN('App edit right: ');constbrowser=awaitpuppeteer.launch();//ブラウザ起動constpage=awaitbrowser.newPage();if(basicUser&&basicPassword){//Basic認証(設定している場合)awaitpage.setExtraHTTPHeaders({Authorization:`Basic ${newBuffer.from(`${basicUser}:${basicPassword}`).toString('base64')}`});}awaitpage.goto(`https://${domain}/k/admin/app/apitoken?app=${appId}`);//ログインページへ遷移awaitpage.type('input[name="username"]',user);//kintoneのユーザー名入力awaitpage.type('input[name="password"]',password);//kintoneのログインパスワード入力await(awaitpage.$('.login-button')).click();//「ログイン」ボタンをクリックawaitpage.waitForNavigation({waitUntil:"domcontentloaded"});//APIトークンの設定ページへの遷移を待機awaitpage.waitFor('.gaia-admin-app-apitoken-add');//「生成する」ボタンの描画を待機await(awaitpage.$('.gaia-admin-app-apitoken-add')).click();//「生成する」ボタンをクリックconstapitoken=awaitpage.evaluate((accessRights)=>(newPromise(resolve=>{newMutationObserver(()=>{//APIトークンの生成を待機consttargetRow=document.getElementsByClassName('gaia-admin-app-apitoken-row')[0];accessRights.forEach((accessRight,index)=>{targetRow.childNodes[1].getElementsByTagName('input')[index].checked=accessRight;//APIトークンのアクセス権の変更});resolve(targetRow.childNodes[0].innerText);//APIトークンの取得}).observe(document.getElementsByClassName('gaia-admin-app-apitoken-table-body')[0],{childList:true,subtree:true});})),accessRights);await(awaitpage.$('.button-submit-cybozu')).click();//「保存」ボタンをクリックawaitpage.waitForNavigation({waitUntil:"domcontentloaded"});//アプリの設定ページへの遷移を待機awaitpage.waitFor('.gaia-admin-app-deploy-button');//「アプリを更新」ボタンの描画を待機await(awaitpage.$('.gaia-admin-app-deploy-button')).click();//「アプリを更新」ボタンをクリックawaitpage.waitFor('.gaia-argoui-dialog-buttons-default-tutorial');//「OK」ボタンの描画を待機await(awaitpage.$('.gaia-argoui-dialog-buttons-default-tutorial')).click();//「OK」ボタンをクリックbrowser.close();//ブラウザ停止console.log(apitoken);//コマンドラインにAPIトークンを出力})();

kintoneの仕様変更により、正しく動作しなくなる可能性があります。
予めご了承ください。

Decorator と継承

$
0
0

この記事は NestJS アドベントカレンダー 2019 14 日目の記事です。
寝込んでいたため遅くなり申し訳ありません。

はじめに

この記事では NestJS で多用される Decorator を継承した場合の挙動について説明します。
サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day14-decorator-and-inheritance

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。
また、この Decorator の挙動は ECMA Script 仕様として定義されていない Decorator に対して、TypeScript 3.7.x 時点での実装による挙動であるため、将来的に仕様の策定・変更に伴い TypeScript コンパイラの挙動が変更になる可能性があります。

結論

メソッドの Decorator 情報は継承されます。オーバーライドで切ることができます。

プロパティの Decorator は Class の定義時にしか評価されません。
しかし評価時にクラス名をキーにして container に副作用を与え、 instanceofで比較を行うようなライブラリでは、 instanceof は子 Class に対して親 Class と比較しても true となる(後述します)ため、継承しているような挙動に見えることがあります。

詳しくは以下で、 Method Decorator と Property Decorator に分けて説明します。

Method Decorator の挙動を追う

Decorator を定義した Class を継承した、 Decorator を直接定義していない Class のインスタンスを生成し、 Validator を定義した sayHello()を呼びます。
以下で定義する @LogProxy()は、関数の実行前後にログを出力する簡単な Decorator 関数です。

src/main.ts
functionLogProxy(when:'before'|'after'|'all'){returnfunction(_target:any,key:string,desc:PropertyDescriptor){constprev=desc.value;constnext=function(){if(when==='before'||when==='all'){console.log(`${this.name}.${key} will start.`);}constresult=prev.apply(this);if(when==='after'||when==='all'){console.log(`${this.name}.${key} has finished.`);}returnresult;};desc.value=next;};}classUser{name:string;constructor(name:string){this.name=name;}@LogProxy('all')sayHello(){console.log(`Hello, I am ${this.name}.`);}}classJapaneseUserextendsUser{name:string;constructor(name:string){super(name);this.name=name;}}constalice=newUser('alice');alice.sayHello();constarisu=newJapaneseUser('有栖');arisu.sayHello();
$ yarn ts-node src/main.ts

alice.sayHello will start.
Hello, I am alice.
alice.sayHello has finished.
有栖.sayHello will start.
Hello, I am 有栖.
有栖.sayHello has finished.

コンパイルされた Decorator がどのような挙動をしているのか確認するため、コンパイルされたファイルを読みます。なお、 target は es2019 ですが、 2015 以降であれば Decorator 周りはほぼ変わらないようです。

dist/main.js
var__decorate=(this&&this.__decorate)||function(decorators,target,key,desc){varc=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeofReflect==="object"&&typeofReflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);elsefor(vari=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;returnc>3&&r&&Object.defineProperty(target,key,r),r;};functionLogProxy(when){returnfunction(_target,key,desc){constprev=desc.value;constnext=function(){if(when==='before'||when==='all'){console.log(`${this.name}.${key} will start.`);}constresult=prev.apply(this);if(when==='after'||when==='all'){console.log(`${this.name}.${key} has finished.`);}returnresult;};desc.value=next;};}classUser{constructor(name){this.name=name;}sayHello(){console.log(`Hello, I am ${this.name}.`);}}__decorate([LogProxy('all')],User.prototype,"sayHello",null);classJapaneseUserextendsUser{constructor(name){super(name);this.name=name;}}constalice=newUser('alice');alice.sayHello();constarisu=newJapaneseUser('有栖');arisu.sayHello();//# sourceMappingURL=main.js.map

全てを読まずとも、 __decorateが User.prototype の name に、 decorator 関数を食わせた値を再代入していることが分かります。
下の継承している側の Class では特に defineProperty をしているわけではないので、 Decorator の影響を受け続けています。

そのため、継承した Class でオーバーライドした場合には Decorator の影響は受けません。

src/main.ts
functionLogProxy(when:'before'|'after'|'all'){returnfunction(_target:any,key:string,desc:PropertyDescriptor){constprev=desc.value;constnext=function(){if(when==='before'||when==='all'){console.log(`${this.name}.${key} will start.`);}constresult=prev.apply(this);if(when==='after'||when==='all'){console.log(`${this.name}.${key} has finished.`);}returnresult;};desc.value=next;};}classUser{name:string;constructor(name:string){this.name=name;}@LogProxy('all')sayHello(){console.log(`Hello, I am ${this.name}.`);}}classJapaneseUserextendsUser{name:string;constructor(name:string){super(name);this.name=name;}sayHello(){console.log(`こんにちは、私は${this.name}です。`);}}constalice=newUser('alice');alice.sayHello();constarisu=newJapaneseUser('有栖');arisu.sayHello();
$ yarn ts-node src/main.ts

alice.sayHello will start.
Hello, I am alice.
alice.sayHello has finished.
こんにちは、私は有栖です。

Property Decorator の挙動を追う

同様に、 Decorator を定義した Class とその子 Class を定義します。
以下で定義する @Effect()は、呼び出し時に呼び出し元とプロパティ名、引数を Container に記録する副作用を持つ Decorator 関数です。

src/main.ts
leteffectContainer={};leteffectCounter=0;functionEffect(str:string){returnfunction(target:any,key:string){constclassName=target.constructor.name;constprev=effectContainer[className];effectContainer[className]={...prev,[key]:str};effectCounter++;};}classUser{@Effect('decorating User.name property')name:string;constructor(name:string){this.name=name;}}classJapaneseUserextendsUser{name:string;constructor(name:string){super(name);this.name=name;}}constalice=newUser('alice');console.log(alice.name)constbeth=newUser('beth');console.log(beth.name)constarisu=newJapaneseUser('有栖');console.log(arisu.name)console.log(effectContainer);console.log(effectCounter);
$ yarn ts-node src/main.ts
alice
beth
有栖
{ User: { name: 'decorating User.name property'}}
1

User Class のインスタンスは子 Class 含め複数回生成していますが、 Decorator 関数は 1度しか呼ばれていません。
コンパイル済みの以下のコードを見ると、 Class 宣言の後に1度評価されているのみであることが分かります。

dist/main.js
var__decorate=(this&&this.__decorate)||function(decorators,target,key,desc){varc=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeofReflect==="object"&&typeofReflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);elsefor(vari=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;returnc>3&&r&&Object.defineProperty(target,key,r),r;};leteffectContainer={};functionEffect(str){returnfunction(target,key){constclassName=target.constructor.name;constprev=effectContainer[className];effectContainer[className]={...prev,[key]:str};};}classUser{constructor(name){this.name=name;}}__decorate([Effect('decorating name property')],User.prototype,"name",void0);classJapaneseUserextendsUser{constructor(name){super(name);this.name=name;}sayHello(){console.log(`こんにちは、私は${this.name}です。`);}}constalice=newUser('alice');constbeth=newUser('beth');constarisu=newJapaneseUser('有栖');console.log(effectContainer);//# sourceMappingURL=main.js.map

この例で上げたのが副作用であるのは、 Decorator 関数の返す関数が取れる引数が 2つのみであり、 PropertyDescripter が存在しないため、呼び出し元の Class に対して何も操作することが現状できないためです。
子 Class に対して定義した場合は、新規の定義として実行されます。

classJapaneseUserextendsUser{@Effect('decorating JapaneseUser.name property')name:string;constructor(name:string){super(name);this.name=name;}}
$ yarn ts-node src/main.ts
alice
beth
有栖
{
  User: { name: 'decorating User.name property'},
  JapaneseUser: { name: 'decorating JapaneseUser.name property'}}
2

class-validator の Decorator の挙動

class-validator では上記の Property Decorator を使用して定義しますが、その際に Class 名とプロパティ名を Container に記録しています
内部では instanceof による比較をしているようであるため、 Decorator の定義を継承したような挙動に見えます。

備考: instanceof と子クラスについて

該当する Class のインスタンスであるかの比較に instanceof を使用すると、その子孫クラスと比較した場合も true となります。

classUser{}constuser=newUser()userinstanceofUser//=> trueclassExUserextendsUser{}constexUser=newExUser()exUserinstanceofUser//=>true

子孫クラスであることを明確に区別したい場合は、 Class 名を取得して比較するのが良いです。

user.constructor.name===exUser.constructor.name//=> false

おわりに

この記事では NestJS で多様される Decorator を継承した場合の挙動について説明しました。
Decorator の仕様はまだ安定していないため、今後挙動が変わる可能性がある点はくれぐれもご留意ください。

明日は @potato4dGitHub Actions を利用した NestJS アプリケーションの Google AppEngine への自動デプロイです。

npmのパッケージグローバルインストールは憲法違反です。

$
0
0

結論

npxコマンドを使おう

グローバルインストール

とは

$ npm install -g elm

このように-gをつけてグローバル環境にパッケージをインストールをすることです

ローカルインストール

対して、ローカルインストールは、

$ mkdir my_project && cd my_project
$ npm init
$ npm install elm --save // or --save-dev

こんな感じでインストールすると、my_project/node_modules/の中にパッケージがインストールされます。

違い

ローカルインストールの利点としては、プロジェクト毎にpackage.jsonで管理をするため、作ったプロジェクトを本番環境や他の人の環境に渡すことが簡単になります!

そして、いろいろなプロジェクトに手を出す際に、バージョン管理が簡単になります!

そしてグローバルインストールと違い、パソコンの環境を汚染しないため気持ち良いです!

CLI系のパッケージはどうするの

グローバルインストールをした場合のCLIパッケージの実行ファイルは皆さんがnode.jsをインストールした際にパスを通したディレクトリになります。

そして、ローカルインストールをした場合の実行ファイルのインストール先はmy_project/node_modules/.bin/ディレクトリになり、プロジェクトの度にパスを通すわけにもいかないし、コマンドを打つ度に./my_project/node_modules/.bin/elmなんて打っていたら面倒臭すぎて死にたくなっちゃいますよね

このような理由がある為、CLI系のパッケージをインストールして利用する際、みんながパスを通すであろうグローバルのnode_modulesディレクトリに実行可能ファイルが作られた方が簡単に取り扱える為、Qiita等いろいろな技術サイトで

CLIなのでグローバルイントールしましょう!

などと書かれていたりします。

しかし上で書いた通り、グローバルインストールは憲法違反です

npxコマンド

ローカルにインストールした実行ファイルの参照解決をしてくれるコマンドがnpmには備わっているのです!(npmのバージョンが 5.2.0以上)

まずelmのcliをインストールしたプロジェクトを作りましょう。

$ mkdir my_project && cd my_project
$ npm init
$ npm install elm --save // or --save-dev

上記のようにインストールしたelmの実行ファイルはここにいるのでこんな感じで使えます。

$ ./node_modules/.bin/elm

でもnpxならこう!!!

$ npx elm 

さあ、グローバル環境を汚さずにelmを書こう。

まとめ

こうやって書かれているnpmのcli系パッケージの記事は

$ npm install -g {package_name}
$ {command}

全部こうやって読み替えよう!!

$ npm install {package_name} // オプションで --save or --save-dev
$ npx {command}

理由があってグローバルインストールをしている場合もあるので憲法違反じゃないかもしれないです、そのときは臨機応変に☺️

Node.js boilerplate / Authentication from scratch - (express, graphql, mongodb)


Vue + Expressのテンプレート作成メモ

$
0
0

はじめに

バックエンドをNode.js、フロントエンドをVue.jsでwebアプリを開発することが増えたので簡単にひな形を作る手順を残しておこうと思います。

手順

expressのプロジェクト作成

express プロジェクト名

vueのプロジェクトを作成

expressで作ったプロジェクトのルートディレクトリへ移動し
vue create public
publicというタイトルになっているので、気になる方はvueプロジェクトのindex.htmlのtittleタグを編集

vueプロジェクトでビルドする

publicディレクトリへ移動し
npm run build

expressのapp.jsに記述されているpublicのパスを変える

app.use(express.static(path.join(__dirname, 'public’)));

app.use(express.static(path.join(__dirname, 'public/dist’)));

一応この記述も消しておく

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade'); 

node.jsプロジェクトでサーバー起動

npm start

アクセスするとVueの初期画面が表示される

localhost:3000

おわりに

さくっと雛形を作ってすぐに開発に取り掛かりましょう!

grpc_tools から生成した gRPC クライアントを promisify してみた。

$
0
0

前提条件

% node --version
v12.13.0
% npm --version
6.13.2

目的

最近、grpc_toolsgrpc_tools_node_protoc_tsを併せて Typescript の型ファイルと node.js の gRPC のクライアントを生成する機会がありました。生成される GRPC クライアントなのですが、Node.js にありがちな callback にて行う非同期処理です。そのため、GraphQL のリゾルバ等と組み合わせて使用する場合、非常に使い勝手が悪いです。

今回は生成された gRPC クライアントの関数を promisify します。

gRPC クライアントの生成

例えば、次のような protocol buffer から pb ファイルを生成することを考えます。

user.proto
syntax = "proto3";

package user;

option go_package = "v1";

service UserService {
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}

message CreateUserRequest {
    int64 id = 1;
    string name = 2;
}

message CreateUserResponse {
    int64 id = 1;
    string name = 2;
}

grpc_tools_node_protocコマンドから pb ファイルを生成します。

dist='src/grpc/generated';\
grpc_tools_node_protoc \--js_out=import_style=commonjs,binary:${dist}\--ts_out=${dist}\--grpc_out=${dist}\-I ./proto
  ./proto/user.proto

生成された型ファイルの一部を抜粋します。

user_grpc_pb.d.ts
exportclassUserServiceClientextendsgrpc.ClientimplementsIUserServiceClient{constructor(address:string,credentials:grpc.ChannelCredentials,options?:object);publiccreateUser(request:user_pb.CreateUserRequest,callback:(error:grpc.ServiceError|null,response:user_pb.CreateUserResponse)=>void):grpc.ClientUnaryCall;publiccreateUser(request:user_pb.CreateUserRequest,metadata:grpc.Metadata,callback:(error:grpc.ServiceError|null,response:user_pb.CreateUserResponse)=>void):grpc.ClientUnaryCall;publiccreateUser(request:user_pb.CreateUserRequest,metadata:grpc.Metadata,options:Partial<grpc.CallOptions>,callback:(error:grpc.ServiceError|null,response:user_pb.CreateUserResponse)=>void):grpc.ClientUnaryCall;}

createUser関数の最後の引数がそえぞれ callback となっていることがわかります。

クライアントの実装

Promise を実装する。

まずは自力で Promise を実装します。

client.ts
import{UserServiceClient}from'./generated/user_grpc_pb';import{credentials}from'grpc';import{CreateUserRequest,CreateUserResponse}from'./generated/user_pb';exportconstcreateClient=(url:string)=>(request:CreateUserRequest):Promise<CreateUserResponse>=>{constclient=newUserServiceClient(url,credentials.createInsecure());returnnewPromise((resolve,reject)=>{client.createUser(request,(err,response)=>{err===null?resolve(response):reject(err);})});};

コレでも良いのですが、gRPC のエンドポイントが増えるたびに実装を行うのはなかなかツライです。

util.promisify を利用する。

次に Node.js の util.promisify を利用することを考えてみます。UserServiceClientクラスのメソッドを promisifyしているため、bind関数により thisを束縛しないとエラーが発生することに注意してください。

client.ts
import{UserServiceClient}from'./generated/user_grpc_pb';import{credentials}from'grpc';import{CreateUserRequest,CreateUserResponse}from'./generated/user_pb';import{promisify}from'util'exportconstcreateClient=(url:string)=>(request:CreateUserRequest):Promise<CreateUserResponse>=>{constclient=newUserServiceClient(url,credentials.createInsecure());returnpromisify<CreateUserRequest,CreateUserResponse>(client.createUser).bind(client)(request)};

エラーの有無に起因する分岐処理はなくなりましたが、ジェネリクスを指定する必要があります。

Bluebird.js を利用する。

Promise の実装である Bluebird.js を使用すると次のようになります。

client.ts
import{UserServiceClient}from'./generated/user_grpc_pb';import{credentials}from'grpc';import{CreateUserRequest,CreateUserResponse}from'./generated/user_pb';import{promisify}from'bluebird'exportconstcreateClient=(url:string)=>(request:CreateUserRequest):Promise<CreateUserResponse>=>{constclient=newUserServiceClient(url,credentials.createInsecure());returnpromisify(client.createUser,{context:client})(request)};

ジェネリクスがなくなり、thisの束縛も含めてシンプルになった印象です。promisifyにより生成される関数の戻り値の型が Bluebird<unknown>から Promise<CreateUserResponse>へ暗黙的にキャストが行われている点が少し気になりますが...

所感

今の所、Bluebird.js を利用した promisify がベターだと感じています。そもそものクライアントの実装をなんとかしてほしいところではあります。他の良い方法があれば是非とも教えて頂きたいです。

新入社員に適当にNode.jsのアプリ入門教えたら意外とウケたのでメモ

$
0
0

はじめに

細かいことは気にせずにNode.jsでwebアプリケーションを作る手順をまとめました。
新入社員の教育につかってみたら思ったより理解してくれて、開発の一歩目になってもらえたのでメモがてら置いておきます。
これから開発をする方がこの記事にたどり着いて少しでも開発への苦手意識などがなくなれば嬉しいです。

スクリーンショット 2019-12-18 22.20.07.png

SlideShareはこちら👇
https://www.slideshare.net/SoheiUchino1/nodejs-beginner

Node.jsのバージョンを上げた際のnode-sassのビルドエラー

$
0
0

ほんとにどうってこと無いメモです。

久々に開発しようとしたNuxtJSプロジェクトで、node-sassがコケました。
node-sassはv4.13.0です。

$ yarn dev

・
・
・

● Client █████████████████████████ building (61%) 431/466 modules 35 active
 node_modules/markdown-it/lib/rules_core/state_core.js

✖ Server
  Compiled with some errors in 9.34s


✖ Client
  Compiled with some errors in 13.93s

✖ Server
  Compiled with some errors in 9.34s


 ERROR  Failed to compile with 1 errors                                            friendly-errors 23:04:34


 ERROR  in ./layouts/blog.vue?vue&type=style&index=0&lang=scss&                    friendly-errors 23:04:34

Module build failed (from ./node_modules/sass-loader/dist/cjs.js):                 friendly-errors 23:04:34
Error: Missing binding /Users/n0bisuke/dotstudio/1_protooutstudio/node_modules/node-sass/vendor/darwin-x64-79/binding.node
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 13.x

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

・
・
・

みたいなコケかたをしました。

Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 13.x

この64-bitをみると一瞬カタリナにmacOSをアップデートしたから怒られるやつかと思いましたが、

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

こちらを見ると既に64bit Node.js v12でnode-sassが紐付けされてたみたいですね。

それにしてもエラー表示がすごい。

これで解決

yarn add node-sass

npmの人も入れ直せばOK。

Decorator Hell を解消する

$
0
0

これを解決します。

src/models/user.ts
import{IsNotEmpty,MaxLength}from'class-validator';import{Column,PrimaryGeneratedColumn}from'typeorm';import{ApiProperty}from'@nestjs/swagger';exportclassUser{@PrimaryGeneratedColumn()@ApiProperty({example:1})id!:number;@IsNotEmpty()@MaxLength(16)@Column()@ApiProperty({example:'alice07'})displayId!:string;@IsNotEmpty()@MaxLength(16)@Column()@ApiProperty({example:'alice'})name!:string;@MaxLength(140)@Column('text')@ApiProperty({example:`Hello, I'm NestJS Programmer!`})profileText?:string;@Column()createdAt!:number;@Column()updatedAt!:number;}

この記事は NestJS アドベントカレンダー 2019 18 日目の記事です。

はじめに

NestJS + ClassValidator + TypeORM 、という構成などのときに、上記のような Decorator Hell を想像してしまうことはあると思います。
動くものとしては十分ですが、メンテナンス性を高めるために、 Abstract Class と Interface を活用して分離し、依存関係を整理する一例を紹介します。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day18-avoid-decorator-hell

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。
また、この Decorator の挙動は ECMA Script 仕様として定義されていない Decorator に対して、TypeScript 3.7.x 時点での実装による挙動であるため、将来的に仕様の作成・変更に伴い TypeScript コンパイラの挙動が変更になる可能性があります。

現実装の Decorator の挙動については Decorator と継承にも書いていますので併せてお読み下さい。

Validator を分離する

exportclassValidatableUser{id!:number;@IsNotEmpty()@MaxLength(16)displayId!:string;@IsNotEmpty()@MaxLength(16)name!:string;@MaxLength(140)profileText?:string;createdAt!:number;updatedAt!:number;}exportclassUserextendsValidatableUser{@PrimaryGeneratedColumn()@ApiProperty({example:1})id!:number;@Column()@ApiProperty({example:'alice07'})displayId!:string;@Column()@ApiProperty({example:'alice'})name!:string;@Column('text')@ApiProperty({example:`Hello, I'm NestJS Programmer!`})profileText?:string;@Column()createdAt!:number;@Column()updatedAt!:number;}

class-validator が継承した Class でも validation ができることを利用し、 validation の定義を親クラスに移譲します。
以下のコードを実行すると、バリデーションエラーが発生します。

import{User}from'./src/models/user';import{validate}from'class-validator';asyncfunctionmain(){constuser=newUser();user.id=1;user.displayId='alice1234567890123456';user.name='alice';consterr=awaitvalidate(user,{skipMissingProperties:true});console.log(err);}main().catch(console.error);

API 層を分離する

API レスポンスとして使用される / Swagger のドキュメント生成に使用される Class を別に定義します。

import{IsNotEmpty,MaxLength}from'class-validator';import{Column,PrimaryGeneratedColumn}from'typeorm';import{ApiProperty}from'@nestjs/swagger';exportclassValidatableUser{id!:number;@IsNotEmpty()@MaxLength(16)displayId!:string;@IsNotEmpty()@MaxLength(16)name!:string;@MaxLength(140)profileText?:string;createdAt!:number;updatedAt!:number;}exportclassUserextendsValidatableUser{@PrimaryGeneratedColumn()id!:number;@Column()displayId!:string;@Column()name!:string;@Column('text')profileText?:string;@Column()createdAt!:number;@Column()updatedAt!:number;}typeTransferUserType=Omit<User,'createdAt'|'updatedAt'>;exportclassTransferUserextendsUserimplementsTransferUserType{@ApiProperty({example:1})id!:number;@ApiProperty({example:'alice07'})displayId!:string;@ApiProperty({example:'alice'})name!:string;@ApiProperty({example:`Hello, I'm NestJS Programmer!`})profileText?:string;}
src/app.controller.ts
import{Controller,Get,HttpException,Query}from'@nestjs/common';import{TransferUser}from'./models/user';import{ApiResponse}from'@nestjs/swagger';import{validate}from'class-validator';@Controller()exportclassAppController{@Get()@ApiResponse({status:200,type:TransferUser})@ApiResponse({status:400})asyncgetUser(@Query(){displayId,name}:{displayId:string;name:string},):Promise<TransferUser>{if(!displayId||!name){thrownewHttpException('displayId and name are required',400);}constuser=newTransferUser();user.id=123;user.displayId=displayId;user.name=name;consterrs=awaitvalidate(user,{skipMissingProperties:true});if(errs.length){console.error(errs);thrownewHttpException(errs,400);}console.log(user);returnuser;}}
$ curl localhost:3000\?displayId=alice07\&name=alice
{"id":123,"displayId":"alice07","name":"alice"}$ curl localhost:3000\?displayId=alice1234567890123456\&name=alice
[{"target":{"id":123,"displayId":"alice1234567890123456","name":"alice"},"value":"alice1234567890123456","property":"displayId","children":[],"constraints":{"maxLength":"displayId must be shorter than or equal to 16 characters"}}]

TypeORM 層を分離する

次に、 User Class から TypeORM の Decorator を分離します。

import{IsNotEmpty,MaxLength}from'class-validator';import{Column,PrimaryGeneratedColumn}from'typeorm';import{ApiProperty}from'@nestjs/swagger';exportclassValidatableUser{id!:number;@IsNotEmpty()@MaxLength(16)displayId!:string;@IsNotEmpty()@MaxLength(16)name!:string;@MaxLength(140)profileText?:string;createdAt!:number;updatedAt!:number;}exportclassUserextendsValidatableUser{id!:number;displayId!:string;name!:string;profileText?:string;createdAt!:number;updatedAt!:number;}typeSerializableUserType=Omit<User,'createdAt'|'updatedAt'>;exportclassSerializableUserextendsUserimplementsSerializableUserType{@ApiProperty({example:1})id!:number;@ApiProperty({example:'alice07'})displayId!:string;@ApiProperty({example:'alice'})name!:string;@ApiProperty({example:`Hello, I'm NestJS Programmer!`})profileText?:string;}exportclassUserEntityextendsUser{@PrimaryGeneratedColumn()id!:number;@Column()displayId!:string;@Column()name!:string;@Column('text')profileText?:string;@Column()createdAt!:number;@Column()updatedAt!:number;}

ロジックを持ち基底となる Pure な User を用意し、整理する

上記の手順で User Class は class-validator を継承しているため、基底とは言えません。
なので、基底となる、 Decorator のない Pure TypeScript な User Class として定義するよう、継承関係を整理します。
また、ここで実装される toObject メソッドは User を継承した全ての Class で使用できるメソッドになります。

exportclassUser{id:number;displayId:string;name:string;profileText?:string;createdAt?:number;updatedAt?:number;constructor({id,displayId,name,profileText,createdAt,updatedAt,}:User){this.id=id;this.displayId=displayId;this.name=name;this.profileText=profileText;this.createdAt=createdAt;this.updatedAt=updatedAt;}toObject(){return{id:this.id,displayId:this.displayId,name:this.name,profileText:this.profileText,createdAt:this.createdAt,updatedAt:this.updatedAt,};}}exportclassValidatableUserextendsUser{id!:number;@IsNotEmpty()@MaxLength(16)displayId!:string;@IsNotEmpty()@MaxLength(16)name!:string;@MaxLength(140)profileText?:string;createdAt!:number;updatedAt!:number;}typeTransferUserType=Omit<User,'createdAt'|'updatedAt'>;exportclassTransferUserextendsValidatableUserimplementsTransferUserType{@ApiProperty({example:1})id!:number;@ApiProperty({example:'alice07'})displayId!:string;@ApiProperty({example:'alice'})name!:string;@ApiProperty({example:`Hello, I'm NestJS Programmer!`})profileText?:string;toObject(){return{id:this.id,displayId:this.displayId,name:this.name,profileText:this.profileText,};}}exportclassUserEntityextendsValidatableUser{@PrimaryGeneratedColumn()id!:number;@Column()displayId!:string;@Column()name!:string;@Column('text')profileText?:string;@Column()createdAt!:number;@Column()updatedAt!:number;}

Abstract Class 、 Interface を活用し整理する

最後に、 インスタンス化しないものを Abstract Class 化します。
この Abstract Class も、 toObject された値も、ともに満たす Interface を定義し実装します。

exportinterfaceUserInterface{id:number;displayId:string;name:string;profileText?:string;createdAt?:number;updatedAt?:number;}exportabstractclassAbstractUserimplementsUserInterface{id:number;displayId:string;name:string;profileText?:string;createdAt?:number;updatedAt?:number;constructor({id,displayId,name,profileText,createdAt,updatedAt,}:UserInterface){this.id=id;this.displayId=displayId;this.name=name;this.profileText=profileText;this.createdAt=createdAt;this.updatedAt=updatedAt;}toObject():UserInterface{return{id:this.id,displayId:this.displayId,name:this.name,profileText:this.profileText,createdAt:this.createdAt,updatedAt:this.updatedAt,};}}exportabstractclassValidatableUserextendsAbstractUser{id!:number;@IsNotEmpty()@MaxLength(16)displayId!:string;@IsNotEmpty()@MaxLength(16)name!:string;@MaxLength(140)profileText?:string;createdAt?:number;updatedAt?:number;}exporttypeTransferUserType=Omit<UserInterface,'createdAt'|'updatedAt'>;exportclassUserextendsValidatableUser{@ApiProperty({example:1})id!:number;@ApiProperty({example:'alice07'})displayId!:string;@ApiProperty({example:'alice'})name!:string;@ApiProperty({example:`Hello, I'm NestJS Programmer!`})profileText?:string;toObject(){return{id:this.id,displayId:this.displayId,name:this.name,profileText:this.profileText,};}}exportclassUserEntityextendsValidatableUser{@PrimaryGeneratedColumn()id!:number;@Column()displayId!:string;@Column()name!:string;@Column('text')profileText?:string;@Column()createdAt?:number;@Column()updatedAt?:number;}

この状態でも、ロジック(Controller にロジックを書くべきではないとは思いますが例なので)側からは自然に見えるように思います。

src/app.controller.ts
@Controller()exportclassAppController{@Get()@ApiResponse({status:200,type:User})@ApiResponse({status:400})asyncgetUser(@Query(){displayId,name}:{displayId:string;name:string},):Promise<UserInterface>{if(!displayId||!name){thrownewHttpException('displayId and name are required',400);}constuser=newUser({id:123,displayId,name});consterrs=awaitvalidate(user,{skipMissingProperties:true});if(errs.length){console.error(errs);thrownewHttpException(errs,400);}console.log(user);returnuser.toObject();}}

ここまで分離する必要があるかどうかはケースバイケースかと思いますが、 Decorator を提供する複数のライブラリに同時に依存してしまうリスクをある程度排除し、同時にメンテナンス性もある程度担保できるかと思います。

おわりに

NestJS + ClassValidator + TypeORM 、という構成などのときに、 Abstract Class と Interface を活用して Decorator Hell を解消する方法の一例を紹介しました。
この方法が全てのプロジェクトに当てはまるわけではありませんが、参考にしていただければ幸いです。

Slack の Bolt フレームワークのチュートリアルを Heroku 上で実行する

$
0
0

最近 Slack のイベントにも参加したりした時に色々と聞いたので、自分でもやってみたものです。
ほとんど公式ドキュメントをなぞったものなので、難しい内容とかは特にないかと思います。
シンプルに 「Heroku で開発するならどうやるのか」 を試した感じです。

前提条件

これからやる作業の前提条件として、次のことはすでに済ませてある前提で進めていきます。

  1. Heroku のアカウントを作成済み
  2. Heroku CLI をインストール・設定済み(ログインとか)
  3. Node.js をインストール済み
  4. Git をインストール・設定済み

エディタはお好みのものをご利用ください。

これからやること

  1. Slack の Bolt フレームワークにあるアプリを作成
  2. 作成したアプリを Heroku 上にアップする
  3. 余力があればカスタマイズ

手順

アップロード先の Heroku アプリの準備

Slack の App を作る前に、次のコマンドであらかじめアップロード先の Heroku アプリを作成しておきます。

$ heroku apps:create <アプリの名前を半角英数字で>
Creating ⬢ <アプリ名>... done
https://<アプリ名>.herokuapp.com/ | https://git.heroku.com/<アプリ名>.git

heroku apps:createの後に何も入力せずに実行すると、Heroku が自動でランダムな名前を生成して設定しますが、分かりやすいように名前を指定しておくことをお勧めします。

アプリの作成が終わると、Web の URL と Git の URL の2つが表示されますので、どちらも控えておきます。
URL はこんな感じです。

Web の URL

https://<アプリ名>.herokuapp.com/

Git の URL

https://git.heroku.com/<アプリ名>.git

控え損ねた場合は、heroku apps:info <アプリ名>で確認もできます。(ブラウザからダッシュボードでもいけるはず)

Slack App の作成

今度は Slack 側のアプリの作成、設定をしていきます。

  1. Slack API のサイトの Your Appsの画面から Create New Appボタンをクリックします。
    New_App_Create_Screen.png

  2. ポップアップでアプリ名とインストール先のワークスペースを聞かれるので、必要事項を入力して Create Appボタンをクリックします。

  3. アプリが作成されると Basic Informationの 画面に遷移します。このページの App Credentialの欄には、後ほど使用する認証情報が記載されています。

  4. 左側のメニューから Bot Usersをクリックします

  5. Add A Bot Userをクリックして、表示名とユーザ名を設定し、Add Bot Userをクリックします

  6. 左側のメニューから Install Appをクリックします

  7. Install App to Workspaceをクリックしてワークスペースにインストールします

  8. OAuth トークンが2種類生成されます
    OAuth_Token_Info.png

この後で Bot User OAuth Access Tokenの方を使っていきます。

Slack App の開発

ここからは実際にサンプルの Slack App を作っていきます。
なお、Heroku アプリへのデプロイには Git を使用するため、途中で Git コマンドも使用します。

  1. プロジェクトディレクトリを作成し、中に移動します(ディレクトリの名前は任意)

    $ mkdir <プロジェクトディレクトリ名>
    $ cd <プロジェクトディレクトリ名>
    
  2. git initコマンドで Git の管理対象に設定します

    $ git init
    
  3. .gitignoreファイルを作成します(中身は Heroku の公式サンプルを参考にしました。)

    $ vi .gitignore
    
    .gitignore
    # Node build artifacts
    node_modules
    npm-debug.log
    
    # Local development*.env
    *.dev
    .DS_Store
    
    # Docker
    Dockerfile
    docker-compose.yml
    
  4. Procfileを作成します。Procfile は起動時にアプリが実行するコマンドを記載するファイルです。ここでは node app.jsコマンドを記載します。

    $ vi Procfile
    
    Procfile
    web: node app.js
    
  5. npm initコマンドでプロジェクトの設定を行います(私はこんな感じで実行しました)

    $ npm init
    This utility will walk you through creating a package.json file.
    It only covers the most common items, and tries to guess sensible defaults.
    
    See `npm help json`for definitive documentation on these fields
    and exactly what they do.
    
    Use `npm install<pkg>` afterwards to install a package and
    save it as a dependency in the package.json file.
    
    Press ^C at any time to quit.
    package name: (<プロジェクトディレクトリ名>) 
    version: (1.0.0) 
    description: 
    entry point: (index.js) app.js
    test command: 
    git repository: 
    keywords: 
    author: 
    license: (ISC) 
    About to write to <プロジェクトディレクトリの親のパス>/bolttest/package.json:
    
    {"name": "bolttest",
      "version": "1.0.0",
      "description": "",
      "main": "app.js",
      "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"},
      "author": "",
      "license": "ISC"}
    
    Is this OK? (yes)

    出来上がった package.jsonファイル

    package.json
    {"name":"bolttest","version":"1.0.0","description":"","main":"app.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"author":"","license":"ISC"}
  6. Bolt パッケージをインストールします

    $ npm install @slack/bolt
    
  7. app.js 内に次のコードを記述します。Heroku の場合、ポート番号は動的に割り振られるため、process.env.PORT の書き方で良いようです。

    $ vi app.js
    
    app.js
    const{App}=require('@slack/bolt');constapp=newApp({token:process.env.SLACK_BOT_TOKEN,signingSecret:process.env.SLACK_SIGNING_SECRET});(async()=>{// Start your appawaitapp.start(process.env.PORT||3000);console.log('⚡️ Bolt app is running!');})();
  8. git でローカルにコミットします

    $ git add .
    $ git commit -m "First Commit"
    
  9. ローカルリポジトリのリモートの設定を追加します。Heroku アプリへのデプロイは、ローカルのリポジトリを Heroku のリポジトリにそのままプッシュすることなので、あらかじめ登録しておきます。URL は アップロード先の Heroku アプリの準備の手順ででた Git の URL です。

    $ git remote add heroku <Git の URL>
    
  10. heroku リポジトリにプッシュします

    $ git push heroku master
    
  11. トークンを設定します。Heroku では config varsという仕組みがあるので、それを使用します

    $ heroku config:set SLACK_SIGNING_SECRET=<サインインシークレット>
    $ heroku config:set SLACK_BOT_TOKEN=xoxb-<Bot トークン>
    
  12. インスタンスを heroku コマンドで立ち上げます

    $ heroku ps:scale web=1
    
  13. ログを確認します。ログの確認には heroku logsコマンドを使用します

    $ heroku logs -t
    

    次のログが出ているかと思います。

    ⚡️ Bolt app is running!
    
  14. Slack api の左側のメニューから Event Subscriptionをクリックして、イベント URL を登録し Save Changeをクリックします。設定する URL は Heroku の Web の URL の末尾に /slack/eventsをつけたものとなります。

    https://bolttestbot.herokuapp.com/slack/events
    

    こんな感じになれば OK です。
    Event_Subscription.png

  15. 現状だと何も反応しないため、app.messageの処理を追加します。

    $ vi app.js
    
    app.js
    const{App}=require('@slack/bolt');constapp=newApp({token:process.env.SLACK_BOT_TOKEN,signingSecret:process.env.SLACK_SIGNING_SECRET});// Listens to incoming messages that contain "hello"app.message('hello',({message,say})=>{// say() sends a message to the channel where the event was triggeredsay(`Hey there <@${message.user}>!`);});(async()=>{// Start your appawaitapp.start(process.env.PORT||3000);console.log('⚡️ Bolt app is running!');})();
  16. ローカルにコミットします

    $ git add app.js
    $ git commit -m "Add hello message"
    
  17. Heroku にプッシュします

    $ git push heroku master
    

これで チャンネルに Bot ユーザを招待して、helloを含むメッセージを投稿すると反応してくれます。

Bot_Response.png

メッセージのカスタマイズ

メッセージがテキストのみなので、ボタンを追加するチュートリアルにしたがってやっていきます。

  1. Slack api の画面左側の Interactive Componentsをクリックして、Request URL を登録し、Save Changeをクリックします。登録する URL は Event Subscriptionのところで登録したものと同じものを登録します。

    Interactive_Components.png

  2. ボタン付きメッセージを返すようにコードを修正します

    $ vi app.js
    
    app.js
    const{App}=require('@slack/bolt');constapp=newApp({token:process.env.SLACK_BOT_TOKEN,signingSecret:process.env.SLACK_SIGNING_SECRET});// Listens to incoming messages that contain "hello"app.message('hello',({message,say})=>{// say() sends a message to the channel where the event was triggeredsay({blocks:[{"type":"section","text":{"type":"mrkdwn","text":`Hey there <@${message.user}>!`},"accessory":{"type":"button","text":{"type":"plain_text","text":"Click Me"},"action_id":"button_click"}}]});});(async()=>{// Start your appawaitapp.start(process.env.PORT||3000);console.log('⚡️ Bolt app is running!');})();
  3. ローカルにコミットします

    $ git add app.js
    $ git commit -m "change message to block"
    
  4. Heroku にプッシュします

    $ git push heroku master
    

    これで helloを含むメッセージを投稿した時にボタン付きのメッセージがボットから返されますが、ボタンをクリックした時の処理がないので何も起こりません。(正確には、ボタンを押すと右側にビックリマークが出ます)

  5. ボタンクリック時のアクションを app.actionで追加します

    $ vi app.js
    
    app.js
    const{App}=require('@slack/bolt');constapp=newApp({token:process.env.SLACK_BOT_TOKEN,signingSecret:process.env.SLACK_SIGNING_SECRET});// Listens to incoming messages that contain "hello"app.message('hello',({message,say})=>{// say() sends a message to the channel where the event was triggeredsay({blocks:[{"type":"section","text":{"type":"mrkdwn","text":`Hey there <@${message.user}>!`},"accessory":{"type":"button","text":{"type":"plain_text","text":"Click Me"},"action_id":"button_click"}}]});});app.action('button_click',({body,ack,say})=>{// Acknowledge the actionack();say(`<@${body.user.id}> clicked the button`);});(async()=>{// Start your appawaitapp.start(process.env.PORT||3000);console.log('⚡️ Bolt app is running!');})();
  6. ローカルにコミットします

    $ git add app.js
    $ git commit -m "Add button click action"
    
  7. Heroku にプッシュします

    $ git push heroku master
    

これでボタンをクリックした時にメッセージが投稿されるようになりました。

Button_Clicked.png

私が実施したソースコードは GitHub のリポジトリにあります。
silverskyvicto/bolttest

余談

Slack の瀬良さん @seratchがサンプルのものをすでに作成していて、それを Heroku Button として公開していることを後で知りました。

Slack Bolt app on Heroku

参考

Lambda + API GatewayでGithub上のアクションを検知してGithubに対してアクションする

$
0
0

はじめに

最近バックエンドの実装をメインに担当しているエンジニアです。
先日チームメンバー(@sen-higaさん)と共同で行った業務効率化タスクを通して、初めてWebhookやサーバレスアーキテクチャに触れたので、その時の備忘録です。

やったこと

Github上のアクションを検知してGithubに対してアクションするという仕組みをLambda + API Gatewayで実装しました。
GithubからGithubへの動線がわかりやすいように、タスクの実装時とは内容を変えて、イシューがOpenされた時に、作成者をイシューに自動アサインするというシンプルな仕組みにしました。

スクリーンショット 2019-12-19 8.56.08.png

もっとこうした方がいい、自分ならこう実装するというご意見があればぜひお願いします。

開発環境

os: mac High Sierra 10.13.6
npm: 6.11.3
Node: 12.12.0

利用技術

今回、利用したのはGithub Webhook Github API,AWS Lambda, Amazon API Gatewayになります。

Github webhook

Webhookとはあるサービスでのイベント発生時に、指定したURLにPOSTリクエストする仕組みです。(webhookとは?より)
Github webhookは、Githubが提供しているwebhookで、特定のアクションがリポジトリあるいは Organization で生じたときに外部の Web サーバーへ通知を配信する方法を提供しています。(公式ドキュメントより)

例えば、イシューのwebhookを登録しておくと、イシューがopenした時に登録したエンドポイントに以下のようなリクエストが送られます。
スクリーンショット 2019-12-18 16.59.54.png

(公式ドキュメントより)

Github API

Webhookとは逆にGithubにアクションする時に使います。APIを利用するには、アクションを実行させるアカウントでアクセストークンを発行する必要があります。

AWS Lambda

サーバーレスでコードを実行できるサービスです。管理画面で、コーディング、環境変数の管理、テストなど色々できます。

Amazon API Gateway

開発規模に応じたエンドポイントを簡単に用意できるサービスです。webhookがリクエストを投げるエンドポイントを作成するために利用しました。Lambdaと連携させると、ヘッダー、ボディをJSON形式でlambdaに渡してくれます。

実装の流れ(概要)

以下のような流れで実装しました。

  1. Lambda関数の作成
  2. エンドポイントの作成
  3. Github Webhookの追加
  4. Github API用のアクセストークンの取得
  5. Lambda関数の実装
    1. リクエストのバリデーション
    2. Webhookのリクエストを解釈
    3. APIを叩けるようにモジュールを追加
    4. Github APIにリクエストを投げる
  6. 動作確認

実装の流れ(詳細)

1. Lambda関数の作成

コンソールから言語をnode.jsに指定して作成しました。

2. エンドポイントの作成

Designerの「トリガーの追加」からエンドポイントを追加しました。
「トリガーの追加」 -> 「API Gateway」を選択し、新規APIの作成画面を表示します。
今回はシンプルな動作のみ実装するのでテンプレートは「HTTP API」を選択しました。

作成後リダイレクトされたLambdaの画面下部に、デプロイ済みのエンドポイントが表示されます。こちらのエンドポイントをGithub Webhookに登録します。

3. Github Webhookの追加

3.1 Webhookに登録するトークンを生成

先程作成したエンドポイントは任意のリクエストを受け付ける状態になっているので、Webhookの認証用ヘッダーを見てリクエストを制限します。認証用ヘッダーをWebhookに埋め込んでもらうために、トークンを事前に用意しておきます。

今回は公式ドキュメントに従って生成したトークンを利用しました。

こちらのトークンは実装時にも利用するので、Lambda関数の環境変数に任意のキー名(SECRET TOKEN等)で追加しておきます。

3.2 Webhookの追加

先程作成したエンドポイントを使ってwebhookを追加します。
アクションを検知したいリポジトリに行き、 Settings -> Webhooks -> Add Webhook
から追加します。この時、先程生成したトークンをSecret欄に記入します。

また、今回はイシューのみを対象としたいので
「Let me select individual events」 -> 「issues」 にチェックを入れました。

スクリーンショット 2019-12-19 7.42.57.png

4. Github API用のアクセストークンの取得

4.1 Tokenの取得

Github API用のトークンも取得しておきます。
自分のアイコンマークを押すと表示されるメニュー -> Setting -> Developer settings -> Personal access tokens -> Generate New Token から追加します。

今回は「repo」の権限を与えたトークンを発行しました。
スクリーンショット 2019-12-17 19.20.35.png

発行されたTokenはLambdaの「環境変数」に任意のキー名(ACCSESS_TOKEN等)で登録しておきます。

これで、Githubからのアクションを検知し、Github APIを叩く動線が整いました。

5. Lambda関数の実装

以下のような手順で実装しました。

  1. リクエストのバリデーション
  2. 対象のアクションか判定
  3. APIリクエスト用にモジュールを追加
  4. GithubAPIにリクエストを投げる

5.1 リクエストのバリデーション

リクエストのバリデーションには、X-Hub-Signatureヘッダー、リクエストボディ、環境変数に登録したSECRET_TOKENを利用します。

公式ドキュメントによると、X-Hub-Signatureは、鍵をSecret Token、データをボディとして算出した値(MAC値)と一致します。

バリデーション実装部分のコードはこちらです。

exports.handler=(event)=>{constheaders=event.headers;constbody=event.body;if(!isValid(body,headers)){constresponse={statusCode:500,body:'Given signatue is invalid',};returnresponse;}constresponse={statusCode:200,body:'OK',};returnresponse;};functionisValid(body,headers){constcrypto=require('crypto');consthmac=crypto.createHmac('sha1',process.env.SECRET_TOKEN);hmac.update(body,'utf8');constsignature='sha1='+hmac.digest('hex');returnsignature===headers['X-Hub-Signature'];}

2. 対象のアクションか判定する

次に、リクエストが対象のアクションのものか判定する部分を実装します。現状だとissueに関わるあらゆるアクション(edited, deleted, transferred等)に反応してしまうので、今回はopenedのみ反応するようにします。

exports.handler=(event)=>{...if(!isOpened(JSON.parse(body))){constresponse={statusCode:400,body:'Given action is invalid',};returnresponse;}...};functionisValid(body,headers){...}functionisOpened(body){returnbody.action==='opened';}

これで、Webhookのリクエストを解釈する部分の実装は完了です。

3. APIリクエスト用にモジュールを追加する

続いて、APIを叩く部分の実装に入る前に、リクエストを行えるようrequestモジュールを追加しておきます。
公式の注記に従って、Layerという機能を使ってmoduleを追加しました。

追加手順は、
1.ローカルPCでrequestモジュールをnpm install
2.node_modulesのzipファイルを用意(zipファイルのディレクトリは「nodejs」が先頭です

 {ファイル名}.zip
 └ nodejs/node_modules/...

3.lambda画面左側の「Layer」からレイヤーを追加します
4.追加したレイヤーをlambda画面「Designer」の「Layers」から今回のlambdaと紐づけます
Layerで紐付けたモジュールはlambda関数から自由に利用できます。

4. GithubAPIにリクエストを投げる

APIにリクエストを投げる部分を実装します。
イシュー作成者をイシューにアサインする場合、ヘッダー、メソッド、URI、ボディは以下の
ように設定します。

ヘッダー

環境変数に追加したトークンを利用してAuthenticationヘッダーを、トークンを発行したユーザ名をUser-Agentヘッダーに追加します

URI

イシューを更新する場合は
{ルートエンドポイント}/repos/:owner/:repo/issues/:issue_numberを指定します

 メソッド

イシューを更新する場合はPATCHを指定します

リクエストボディ

JSON形式でパラメータを記入します。(
今回はAssigneeを指定するパラメータ)

exports.handler=(event)=>{...constrequest=require('request');request(params(JSON.parse(body)),(error,response,body)=>{if(error){console.error('Issue assign failed');}else{console.log('Issue assign success');}});...};...functionparams(body){return{json:true,headers:{'Authorization':'token '+process.env.ACCESS_TOKEN,'User-Agent':'yanagimura'},method:'PATCH',uri:`${body.issue.repository_url}/issues/${body.issue.number}`,,json:{'assignee':'yanagimura'}};}

6. 動作確認

早速イシューを作成してみます。

イシューを立てた直後にはAssineesは空ですが
スクリーンショット 2019-12-18 17.37.02.png

🔽

スクリーンショット 2019-12-18 17.37.09.png

すぐにイシューを立てたユーザ(私)がアサインされました!

まとめ

 
チームメンバーと共同で実装した時から時間が経っていたため、忘れている部分も多々あり、今回記事を投稿することで復習できてよかったです。ひとつひとつの実装はシンプルでしたが、それを組み合わせて、ひとつの流れを作ろうとすると、サーバレスとはいえ結構複雑だと感じました。 

また、ヘッダーの署名やAPIリクエストの構造などの知見は、外部サービスの利用と構築の両方の観点から得るものが多かったです。

最後に

全体のコードを記載しておきます。

exports.handler=(event)=>{constheaders=event.headers;constbody=event.body;if(!isValid(body,headers)){constresponse={statusCode:500,body:'Given signatue is invalid',};}if(!isOpened(JSON.parse(body))){constresponse={statusCode:400,body:'Given action is invalid',};returnresponse;}constrequest=require('request');request(params(JSON.parse(body)),(error,response,body)=>{if(error){console.error('Issue assign failed');console.error(error);}else{console.log('Issue assign success');console.log(response);console.log(body);}});constresponse={statusCode:200,body:'OK',};returnresponse;};functionisValid(body,headers){constcrypto=require('crypto');consthmac=crypto.createHmac('sha1',process.env.SECRET_TOKEN);hmac.update(body,'utf8');constsignature='sha1='+hmac.digest('hex');returnsignature===headers['X-Hub-Signature'];}functionisOpened(body){returnbody.action==='opened';}functionparams(body){return{json:true,headers:{'Authorization':'token '+process.env.ACCESS_TOKEN,'User-Agent':'yanagimura'},method:'PATCH',uri:`${body.issue.repository_url}/issues/${body.issue.number}`,json:{'assignee':`${body.sender.login}`}};}

BlankAndroidTV向けアプリをReactNativeで起動するまで

$
0
0

Ateam Lifestyle Advent Calendar 2019の19日目は
株式会社エイチームライフスタイル 自動車事業部 の @mziyutが担当します :santa:

最近購入したテレビがAndroidTVをベースにしたものだったのもあり、
0.55からReactNativeでAndroidTV向けアプリの開発を行えるようになっていたので試してみました。
ちなみに、2019/12/16時点の最新バージョンは、v0.61.5でした。

ReactNativeとは :thinking:

簡単に言うと「Facebookが作成したReactをベースにネイティブアプリフレームワーク」です。

AndroidTVとは :thinking:

簡単に言うと、Googleが提供する、スマートテレビ向けプラットフォームです。

今回用いる環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.4
BuildVersion:   18E226
$ node -v
v10.16.3

ReactNativeを開発する準備 :construction:

AndroidStudioや、CocoaPodsなどは、
プロジェクト作成時に必要となるため事前に準備しておきましょう。

まず、ReactNativeのProjectを作成します。
今回は、「react_native_androidtv」といったプロジェクト名にします。
時間は、かかるので気長に待ちましょう。

npx react-native init react_native_androidtv

               ######                #########     ####        ####     #####          ###    ###          ####             ####             ####             ####             ####           ##    ##           ####         ###      ###         ####  ########################  ########    ###            ###    #########     ##    ##              ##    ##     ######         ## ###      ####      ### ##         #####           ####      ########      ####           ####             ###     ##########     ###             ####           ####      ########      ####           #####         ## ###      ####      ### ##         ######     ##    ##              ##    ##     #########    ###            ###    ########  ########################  ####         ###      ###         ####           ##    ##           ####             ####             ####             ####             ####          ###    ###          #####     ####        ####     #########                ######


                  Welcome to React Native!
                 Learn once, write anywhere

✔ Downloading template
✔ Copying template
✔ Processing template
✔ Installing CocoaPods dependencies (this may take a few minutes)

  Run instructions for iOS:
    • cd /Users/mziyut/Workspace/github.com/mziyut/react_native_androidtv && npx react-native run-ios
    - or -
    • Open react_native_androidtv/ios/react_native_androidtv.xcworkspace in Xcode or run "xed -b ios"• Hit the Run button

  Run instructions for Android:
    • Have an Android emulator running (quickest way to get started), or a device connected.
    • cd /Users/mziyut/Workspace/github.com/mziyut/react_native_androidtv && npx react-native run-android

起動

BlankのProjectが作成されたので、起動できるところまで確認を実施しておきます。
今回はAndroidが対象なので、Project作成時に出ていた以下コマンドを実行

$ cd /Users/mziyut/Workspace/github.com/mziyut/react_native_androidtv && npx react-native run-android

起動することを確認できました :tada:

image.png

AndroidTV向けにオプションの変更

AndroidTV向けにBuildできるように設定を変更しましょう。
AndroidManifest.xmlの一部を変更する必要があります。

  • react_native_androidtv/android/app/src/main/AndroidManifest.xml ※1
<!-- Add custom banner image to display as Android TV launcher icon --><application...android:banner="@drawable/tv_banner">
    ...
    <intent-filter>
      ...
      <!-- Needed to properly create a launch intent when running on Android TV --><categoryandroid:name="android.intent.category.LEANBACK_LAUNCHER"/></intent-filter>
    ...
  </application>

仮想デバイスの追加

デフォルトだと、通常のAndroidが起動してしまうためAndroidTVDeviceを追加しましょう。
image.png

↑のアイコンをAndroidStudioから探し 「AVD Manager」を立ち上げます。
標準であればツールバー内に存在します。
image.png

「Create Virtual Devise」をクリックします。

image.png

AndroidTVを選択し「Next」をクリック。

システムイメージを選択しましょう。
今回は、「Q」を選択します。
image.png

「Next」を押すと無事仮想デバイスが作成されます。

image.png

起動しましょう :clap:

先程立ち上がっていた、仮想デバイスを停止した後、改めてBuildを行います。

$ cd /Users/mziyut/Workspace/github.com/mziyut/react_native_androidtv && npx react-native run-android

image.png

まとめ

AndroidTV向けのBuildがReactNativeでサポートされており、少ない設定でアプリをBuildし立ち上げるところまで行えました。
しかし、TV アプリの品質  |  Android Developersに以下記載があります。

重要: 優れたユーザー エクスペリエンスを実現するには、TV 端末向けのアプリがユーザビリティの特定の要件に適合している必要があります。
次の品質基準に適合するアプリのみが Google Play で Android TV アプリとして認められます。

リモコン操作に対する設定だけではなくUIに関する制限をクリアするために多くの実装を追加で行う必要があります。
(同様に、AppleTV向けアプリケーションも同様に要件があります)

そのため、ReactNativeでスマートテレビ向けアプリを作成できると安易に飛びつくことなく、
実装に必要な内容を整理した上で判断したほうが良いと考えます。

Buildしただけですが、mziyut/react_native_androidtv - GithubにPushしておきました。

最後に

Ateam Lifestyle Advent Calendar 2019 20日目は @mgmg121がお送りします!!
どんな記事を書いてくれるかとても楽しみですね!

"挑戦"を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/

参考

Node.js+Selenium WebDriverでブラウザのエラーログを取得する話

$
0
0

はじめに

こんにちは。
surimi_panです。

本記事はNorth Detail Advent Calendar 2019の19日目の記事となります。
日付の数字(19)が西暦の下2桁(19)と一緒ですが、当記事に19に関わる要素は特にありません。

今回はNode.jsとSelenium WebDriverを利用して、
Google Chromeの開発者ツールにて出力されるログの中にエラーが無いかをチェックします。

要件

  • Google Chromeの開発者ツールに出力されるエラーを取得したい。
  • 取得するエラーログは深刻なものに限る。(WARNINGは含まない)

手段

Selenium WebDriverの、ログを取得する機能( selenium-webdriver/lib/logging )を利用します。
ブラウザのログをレベル(=ログの重要度)で絞り込んで取得することができます。

実装

require('chromedriver');constchrome=require('selenium-webdriver/chrome');constwebdriver=require('selenium-webdriver');constBuilder=webdriver.Builder;constcapabilities=webdriver.Capabilities.chrome();const{Preferences,Type,Level}=require('selenium-webdriver/lib/logging');// ログを調べたいWebページのURLconsttargetUrl='調べたいWebページのURL';constlogPrefs=newPreferences();logPrefs.setLevel(Type.BROWSER,Level.SEVERE);capabilities.setLoggingPrefs(logPrefs);constoptions=newchrome.Options(capabilities);// エラーログを出力する(asyncfunctionprintConsoleErrors(){constdriver=awaitnewBuilder().forBrowser('chrome').setChromeOptions(options).build();awaitdriver.get(targetUrl);awaitdriver.sleep(5000);leterrors=awaitdriver.manage().logs().get(Type.BROWSER);for(vari=0;i<errors.length;i++){console.log(errors[i].message);}awaitdriver.quit();})();

詳細

取得するログの種類の絞り込み

constlogPrefs=newPreferences();logPrefs.setLevel(Type.BROWSER,Level.SEVERE);capabilities.setLoggingPrefs(logPrefs);

取得するログの種類、レベルを絞り込みます。
今回はブラウザのエラーログを取得したいので、
取得したいログの対象をBROWSER(ブラウザ)、
取得したいログのレベルをSEVERE(最も深刻なもの→エラー)に設定します。

ログの絞り込みの設定を適用

constoptions=newchrome.Options(capabilities);
constdriver=awaitnewBuilder().forBrowser('chrome').setChromeOptions(options).build();

WebDriverインスタンスを作る際にログの絞り込み設定を反映させます。

ページの遷移と待機

awaitdriver.get(targetUrl);awaitdriver.sleep(5000);

チェックしたいページに遷移して一定時間待機します。
時間差でエラーが出るものを考慮して気持ち長めに待機時間を設定しています。

エラーログの取得

leterrors=awaitdriver.manage().logs().get(Type.BROWSER);

ブラウザのエラーログを取得します。
取得されるログのレベルはSEVEREに設定されていますので、エラーのみ取得します。

取得したエラーの出力

for(vari=0;i<errors.length;i++){console.log(awaiterrors[i].message);}

配列で返ってくるので、展開してコンソールに出力します。

まとめ

実行すると、エラーを吐くページではエラーログが次々出力されます。

Webページのスクレイピングやフォームのテスト等で利用されがちなSeleniumですが、
変わったこともできますよ!という記事でした。

資料が少なく、公式ドキュメントとにらめっこしながらの実装となりました。
不備不足あればご指摘頂けると大変助かります。

GitHub ActionsでNode.jsのテストカバレッジをCoverallsに登録する

$
0
0

はじめに

GitHub Actionsを触りたくてActionを作ってみました。Node.js(というかTypeScript)で書いているのですが、テストカバレッジをREADMEにバッジ表示したくて調べました。

前提

公式テンプレート1を利用してリポジトリを作成します。
https://github.com/actions/typescript-action

Coveralls側でAdd repoしてください。TOKENは使わないので放っておいてよいです。

サンプル

https://github.com/oke-py/npm-audit-action
https://coveralls.io/github/oke-py/npm-audit-action

設定ファイル修正

package.json

テストにはJestを使用しています。npm testの引数に-- --coverageを追加するだけでカバレッジを取得できます。

diff --git a/package.json b/package.json
index ec291cb..2e0a658 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
     "lint": "eslint src/**/*.ts",
     "pack": "ncc build",
     "test": "jest",
-    "all": "npm run build && npm run format && npm run lint && npm run pack && npm test"
+    "all": "npm run build && npm run format && npm run lint && npm run pack && npm test -- --coverage"
   },
   "repository": {
     "type": "git",

.github/workflows/test.yml

GitHub Actionsで実行されるテストの設定を変更します。Coveralls公式のAction1が提供されているので利用します。

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d62684f..fade74a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,10 +14,13 @@ jobs:
     - run: |
         npm install
         npm run all
+    - uses: coverallsapp/github-action@master
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
   test: # make sure the action works on a clean machine without building
     runs-on: ubuntu-latest
     steps:

以上です。あとはREADMEにバッジを表示するだけです(割愛)。

おわりに

Coveralls連携は簡単でした。カバレッジ上げよう(本記事が公開される頃にはマシになっているだろうか・・・)。
GitHub Actionsよいですね。楽しいです。

C#でJWTを発行して、Node.jsで検証する簡単なお仕事です

$
0
0

SC(非公式)Advent Calendar 2019の19日目です。

はじめに

最近JWT周りのなんやかんやを触る機会が多いです。
別の言語での取り回しなんかもできるのが、JWTでの検証の良いところだと思います。

今回は.NetCore3.0で追加された 暗号化キーのインポート/エクスポートで、
RSAではなくECDsa(楕円暗号方式)で署名/検証しました。

サーバー構成としては以下になります。
image.png

実行環境

OS: mac OS Mojave 10.14.6
IDE: VS2019 for Mac community 8.3.6
.NetCore: 3.1.100
node: 10.14.1
npm: 6.4.1
クライアント: POSTMAN

余談ですが、MacでわざわざC#を触る人ってキチガイですよね~。
と、後輩に言われました。

秘密鍵・公開鍵の作成

以下のコマンドで楕円曲線暗号方式で秘密鍵と公開鍵を作成します。

ssh-keygen -t ecdsa -b 256 -m PEM -f jwtES256.key
openssl ec -in jwtES256.key -pubout -outform PEM -out jwtES256.key.pub

C#でIDProviderを作成

JWTを発行するC#のプロジェクトを立ち上げます。
必要なパッケージとして
Microsoft.AspNetCore.Authentication.JwtBearer
を追加しています。

# ワークフォルダ
mkdir JwtSample
cd JwtSample
# ソリューションの作成
dotnet new sln
# WebAPIテンプレートのプロジェクト作成
dotnet new webapi -o ./CSharpIDP
# ソリューションにプロジェクトを追加
dotnet sln add ./CSharpIDP
# JWTでの認証をするためにNugetパッケージを追加
dotnet add ./CSharpIDP package Microsoft.AspNetCore.Authentication.JwtBearer

いざJWTを生成

まずはAuthenticationControllerを新しく作成します。
全貌がこちら。

AuthenticationController.cs
usingCSharpIDP.Utils;usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.Extensions.Logging;usingMicrosoft.IdentityModel.Tokens;usingSystem;usingSystem.IdentityModel.Tokens.Jwt;usingSystem.Security.Claims;usingSystem.Security.Cryptography;usingSystem.Threading.Tasks;namespaceCSharpIDP.Controllers{[ApiController][Route("[controller]")]publicclassAuthenticationController:ControllerBase{privatereadonlyILogger<AuthenticationController>_logger;publicAuthenticationController(ILogger<AuthenticationController>logger){_logger=logger;}[HttpPost]publicasyncTask<IActionResult>Token([FromBody]LoginModelmodel){vartokenString=awaitAuthenticateAsync(model);if(tokenString!=""){returnOk(new{token=tokenString});}returnUnauthorized();}privateasyncTask<string>AuthenticateAsync([FromBody]LoginModelmodel){_logger.LogInformation("AuthenticateAsync");varuser=awaitFetchUserAsync(model.Email);// DB接続などを想定if(user.Email==model.Email&&user.Password==model.Password){vartokenString=GenerateToken(user);returntokenString;}return"";}privateasyncTask<UserInfo>FetchUserAsync(stringemail){_logger.LogInformation($"fetch user data by email={email}");returnawaitTask.Run(()=>newUserInfo{UserId=888,UserName="jwtSignningUser",Email="aaa@gmail.com",Password="password",Groups=newint[]{1,2,3}});}privatestringGenerateToken(UserInfouser){varclaims=new[]{newClaim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),newClaim(JwtRegisteredClaimNames.Sid,user.UserId.ToString()),newClaim(JwtRegisteredClaimNames.Sub,"JWT Sample for node.js"),newClaim(JwtRegisteredClaimNames.Email,user.Email)};varpemStr=System.IO.File.ReadAllText(@"./jwtES256.key");varder=StringUtil.ConvertX509PemToDer(pemStr);usingvarecdsa=ECDsa.Create();ecdsa.ImportECPrivateKey(der,out_);varkey=newECDsaSecurityKey(ecdsa);varcreds=newSigningCredentials(key,SecurityAlgorithms.EcdsaSha256);varjwtHeader=newJwtHeader(creds);varjwtPayload=newJwtPayload(issuer:"https://localhost:5001/",audience:"https://localhost:3000/",claims:claims,notBefore:DateTime.Now,expires:DateTime.Now.AddMinutes(600),issuedAt:DateTime.Now);vartoken=newJwtSecurityToken(jwtHeader,jwtPayload);returnnewJwtSecurityTokenHandler().WriteToken(token);}}publicclassLoginModel{publicstringEmail{get;set;}="";publicstringPassword{get;set;}="";}publicclassUserInfo{publicintUserId{get;set;}publicstring?UserName{get;set;}publicstring?Email{get;set;}publicstring?Password{get;set;}publicint[]?Groups{get;set;}}}

ではでは、GenerateTokenメソッドの解説をしていきます

JWTのスキーマ

JWTは、RFC7519で定義されているスキーマを持っていて、大きく以下の3種類のスキーマ定義があります。詳しくはここのサイトが大変参考になります。(JSON Web Token(JWT)のClaimについて)

  • Registered Claim Names
  • Public Claim Names
  • Private Claim Names

Registered Claim Names

Registered Claim Namesはあらかじめ決められた、「JWTならこれ持ってますよね」という定義です。

予約語意味役割
issIssuerJWTの発行者。文字列かURIの形式
subSubjectJWTの用途。文字列かURIの形式
audAudienceJWTの利用者。文字列かURIの形式
expExpiration TimeJWTの失効する日時
nbfNot BeforeJWTが有効になる日時
iatIssued AtJWTの発行日時
jtiJWT IDJWTを一意な識別子。UUIDなどを入れるのが一般的

これらのスキーマ定義に則って、JwtPayloadクラスの設定をしているのが、以下の部分です。

varclaims=new[]{newClaim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),newClaim(JwtRegisteredClaimNames.Sid,user.UserId.ToString()),newClaim(JwtRegisteredClaimNames.Sub,"JWT Sample for node.js"),newClaim(JwtRegisteredClaimNames.Email,user.Email)};// ... 略varjwtPayload=newJwtPayload(issuer:"https://localhost:5001/",audience:"http://localhost:3000/",claims:claims,notBefore:DateTime.Now,expires:DateTime.Now.AddMinutes(60),issuedAt:DateTime.Now);

現在から有効な、http://localhost:3000向けのJWTを発行しています。
有効期限は現在から1時間です。
実際のアプリではユーザーIDなどを入れると思いますので、Private Claim NamesとしてSid属性に入れています。

ECDsaでの署名

今回は、どの言語でも汎用的に使用できるように、opensslで秘密鍵と公開鍵のファイルを作成しました。
もちろんC#のプログラムからキーの生成を行うこともできますが、PEMファイルを読み込むとき少しハマったので、ご紹介。

// 秘密鍵ファイルの内容を取得varpemStr=System.IO.File.ReadAllText(@"./jwtES256.key");// PEM形式からbase64にデコードvarder=StringUtil.ConvertX509PemToDer(pemStr);// ECDsaのインスタンス化usingvarecdsa=ECDsa.Create();// der形式のデータをインポートecdsa.ImportECPrivateKey(der,out_);// SecurityKeyインスタンス生成varkey=newECDsaSecurityKey(ecdsa);

ECDsaのImportECPrivateKeyメソッドはこんな定義になっているので、
ファイルの余分な部分を削除して、base64デコードして渡してあげないとダメです。

image.png

ですので、Utilクラスでこんな泥くさいことをやっています。

publicstaticbyte[]ConvertX509PemToDer(stringpemContents){varbase64=pemContents.Replace("-----BEGIN EC PRIVATE KEY-----",string.Empty).Replace("-----END EC PRIVATE KEY-----",string.Empty).Replace("\r\n",string.Empty).Replace("\n",string.Empty);// Windowsだったらこの行は不要かもreturnConvert.FromBase64String(base64);}

メールアドレス・パスワードでトークンを取得

POSTMANからメールアドレスとパスワードでアクセストークンを取得します。

定義
URLhttps://localhost:5001/authentication
メソッドPOST
ヘッダーContent-Type:application/json
BODY{ "Email": "aaa@gmail.com", "Password": "password"}

image.png

DECsaの形式で署名されたJWTを取得できました。

Node.jsで検証サーバーを作成

つづいてはアクセストークンを検証するサーバーをNode.jsで作っていきます。
Expressのテンプレートを作成するexpress-generatorをグローバルインストールして、
適当なアプリを作成します。

npm install -g express-generator
# ワークディレクトリ
mkdir Express
cd Express
# verifyappという名前で作成
express verifyapp -e
cd verifyapp
# パッケージをインストール
npm i
# JWTを扱うためのパッケージもインストール
npm i jsonwebtoken
# サーバー起動
npm start

これでhttp://localhost:3000でサーバーが立つはずです。
app.jsは以下のコードを追加します。

app.js
// ... 略+varjwt=require("jsonwebtoken");+varfs=require('fs');// ... 略// ... 略-app.use("/users",usersRouter);+app.use("/users",Authorize,usersRouter);+functionAuthorize(req,_,next){+constauthHeader=ParseAuthHeader(req.headers);+if(!authHeader)next(createError(401));+consttoken=authHeader.value;+constpublicKey=fs.readFileSync("./jwtES256.key.pub",{encoding:"utf8"+});+constoptions={+algorithms:["ES256"]// 署名オプション+};+constdecodedToken=jwt.verify(token,publicKey,options);+if(typeofdecodedToken!=="object")next(createError(401));+req.token=decodedToken;+next();+}+functionParseAuthHeader(headers){+constAUTH_HEADER="authorization";+constregex=/(\S+)\s+(\S+)/;+if(!headers[AUTH_HEADER])returnundefined;+if(typeofheaders[AUTH_HEADER]!=="string")returnundefined;+constmatches=headers[AUTH_HEADER].match(regex);+returnmatches&&{scheme:matches[1],value:matches[2]};+}// ... 略module.exports=app;

Authorizeというミドルウェアを追加しています。
何をやっているか詳しく見ていくと、

app.js
functionAuthorize(req,_,next){// リクエストヘッダーのauthorizationヘッダーからベアラートークンを取得constauthHeader=ParseAuthHeader(req.headers);if(!authHeader)next(createError(401));consttoken=authHeader.value;// 公開鍵ファイルを読み込みconstpublicKey=fs.readFileSync("./jwtES256.key.pub",{encoding:"utf8"});constoptions={algorithms:["ES256"]// 署名アルゴリズムを指定};constdecodedToken=jwt.verify(token,publicKey,options);if(typeofdecodedToken!=="object")next(createError(401));// トークンからペイロードの情報が取れたら、reqにtokenとして保存req.token=decodedToken;next();}

userRouterの先のuser.jsファイルはこんな感じになっています。

user.js
varexpress=require('express');varrouter=express.Router();/* GET users listing. */router.get('/',function(req,res,next){res.send(`${req.token.sub}さんからのリクエストです(${req.token.sid})`);});module.exports=router;

では先ほど取得したトークン情報をauthorizationヘッダーに載せてアクセスしてみます。
POSTMANでAuthorizationタブからBaerer Tokenを選択して、Tokenに先ほど取得したToken値を入れてGetでSendするだけです。

image.png

Tokenがなかった場合、きちんと401が返ってきます。

image.png

以上です。

C#での検証もためしてみたので、雑に載せときます。

Startup.cs
usingCSharpIDP.Utils;usingMicrosoft.AspNetCore.Authentication.JwtBearer;usingMicrosoft.AspNetCore.Builder;usingMicrosoft.AspNetCore.Hosting;usingMicrosoft.Extensions.Configuration;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Hosting;usingMicrosoft.IdentityModel.Tokens;usingSystem.IO;usingSystem.Security.Cryptography;namespaceCSharpIDP{publicclassStartup{publicStartup(IConfigurationconfiguration){Configuration=configuration;Ecdsa=ECDsa.Create();}~Startup(){Ecdsa.Dispose();}publicIConfigurationConfiguration{get;}privateECDsaEcdsa{get;}// This method gets called by the runtime. Use this method to add services to the container.publicvoidConfigureServices(IServiceCollectionservices){varpemStr=File.ReadAllText(@"./jwtES256.key.pub");varder=StringUtil.ConvertPubKeyToDer(pemStr);// 秘密鍵と同じことやってますEcdsa.ImportSubjectPublicKeyInfo(der,out_);services.AddAuthentication(options=>{options.DefaultAuthenticateScheme=JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme=JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(options=>{options.TokenValidationParameters=newTokenValidationParameters(){ValidateIssuer=true,ValidIssuer="https://localhost:5001/",ValidateIssuerSigningKey=true,IssuerSigningKey=newECDsaSecurityKey(Ecdsa),ValidateAudience=false,ValidateLifetime=false,};});services.AddControllers();}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.publicvoidConfigure(IApplicationBuilderapp,IWebHostEnvironmentenv){if(env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseHttpsRedirection();app.UseRouting();app.UseAuthentication();// 追加app.UseAuthorization();app.UseEndpoints(endpoints=>{endpoints.MapControllers();});}}}

これで、認可したいControllerに[Authorize]属性つければ、
認証のフィルターができるようになります。

参考

.NET Core 3.0 の新機能
Embracing nullable reference types
JWT Signing using ECDSA in .NET Core

Aurora Serverless DB を作って Node.js(TS) から使う

$
0
0

概要

Aurora Serverless DB を作成して、
Node.js (TypeScript) からアクセスしてみます。

実行時の環境 2019/12/04

  • MacOS 10.14.4
  • node v10.15.0
  • npm 6.6.0
  • ts-node v8.5.4
  • aws-sdk 2.584.0

DB の作成

Data API 公式ドキュメントを見ると、
現在、Data API が有効なリージョンは限られているらしいので注意
東京リージョンでつくる。

  • DB 作成方法
項目
テータベース作成方法標準作成
  • エンジンのオプション
項目
エンジンのタイプAmazon Aurora
エディションMySQL 互換
バージョン現行最新: Aurora (MySQL)-5.6.10a
データベースロケーションリージョン別
  • データベースの機能
項目
データベースの機能サーバーレス
  • 設定

    • マスターパスワードはあとでテストに出るのでノートにとること
  • キャパシティーの設定

    • ACU = Aurora キャパシティーユニット
      • 使用する ACU x 時間に応じて課金が発生する。デフォルト最大値 128 とかいってて怖いから 8 に下げて様子見る。
    • コールドスタート
      • 使ってない時間帯は勝手に止まってくれる
      • 使い始めは 1 分かけてゆるく起動するらしい
      • 開発環境とか社内向けサービスなのでゆるくていい
項目
最小 ACU1
最大 ACU8
追加設定アイドルの場合、コンピューティングを一時停止: 15 分
  • 接続
    • Data APIを有効にする
    • そのほかはてきとう
項目
ウェブサービスデータ APIData API
  • 追加設定
    • 基本的にデフォルトのままにした。
    • 最初のデータベース名…DB 名とは違うのか?
      • MySQL でいう データベーススキーマと同じと考えていいらしい
      • ややこしいわ
      • 複数のサービスで DB を利用する予定なので、はじめにのせるサービス名にした

DB ができた

  • 作成中ステータスで表示された
  • しばらくまつと 利用可能になった

Query Editor を使って DB にユーザーを作る

Query Editor への接続

  • RDS メニューから Query Editorへアクセス
    • 作成した DB を選択
    • user: admin
    • password: さっきメモっといたマスターパスワード
      • メモっとかなかったおバカさん(俺)は、DB の設定変更から再設定
  • データベースに接続
  • コンソールが開き、デフォルトのクエリが実行できたら OK
  • せっかくなので、 SHOW DATABASES;してみる
    • DB 作成時に最初のデータベース名に入力していたデータベース(スキーマ)が表示されるはず

開発用ユーザーを作り、データベースへのアクセス権を与える

in-QueryEditor
CREATEUSER'devuser'@'%'IDENTIFIEDBY'YOUR_PASSWORD';GRANTALLON(最初のデータベース名).*TOdevuser;

成功を確認したら、作ったユーザーでアクセスしてみる

  • 「データベースを変更する」
    • user: devuser
    • password: YOUR_PASSWORD
    • データベースまたはスキーマ: (最初のデータベース名)
  • 接続してクエリが実行できたら OK

Node.js から DB に接続する

これがやりたかった

Data API 公式ドキュメントから必要な部分を実行していく

Data API にアクセスするためのシークレットを作る

  • Secret Managerから、MySQL ユーザーに対応する Secret を発行する必要がある。
  • しかし、なんと Query Editor からアクセスした時点で Secret が勝手に作られている。便利。

アクセスするサンプルコードを書いて実行してみる (TypeScript)

src/aurora-test.ts
import{RDSDataService}from"aws-sdk";import{ExecuteStatementRequest}from"aws-sdk/clients/rdsdataservice";(functiontestQuery(){constrds=newRDSDataService({region:"ap-northeast-1",accessKeyId:"***",secretAccessKey:"***"});constparams:ExecuteStatementRequest={resourceArn:"***",// RDS > データベース > 設定 から参照secretArn:"***",// SecretManager > 追加したユーザーのSecret > シークレットのARNdatabase:"(最初のデータベース名)",sql:"select * from information_schema.tables",includeResultMetadata:true};rds.executeStatement(params,(err,data)=>{if(err){console.error(err,err.stack);}else{console.log(`Fetch ${data.records!.length} rows!`);console.log(data.columnMetadata!.map(col=>col.name).join(","));for(constrecordofdata.records!){console.log(record.map(col=>Object.values(col)[0]).join(","));}}});})();

ts-nodeで実行

ts-node src/aurora-test.ts

> Fetch 69 rows!
> TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,TABLE_TYPE,ENGINE,VERSION,ROW_FORMAT,TABLE_ROWS,AVG_ROW_LENGTH,DATA_LENGTH,MAX_DATA_LENGTH,INDEX_LENGTH,DATA_FREE,AUTO_INCREMENT,CREATE_TIME,UPDATE_TIME,CHECK_TIME,TABLE_COLLATION,CHECKSUM,CREATE_OPTIONS,TABLE_COMMENT
> def,information_schema,CHARACTER_SETS,SYSTEM VIEW,MEMORY,10,Fixed,true,384,0,16434816,0,0,true,2019-12-04 07:35:09,true,true,utf8_general_ci,true,max_rows=43690,
> def,information_schema,COLLATIONS,SYSTEM VIEW,MEMORY,10,Fixed,true,231,0,16704765,0,0,true,2019-12-04 07:35:09,true,true,utf8_general_ci,true,max_rows=72628,
> ...

UTF-8 を指定してUnicodeを扱えるようにする

デフォルトの character set が latinとかいうやつで、
日本語が全部 ???になって困ったので設定を変える。
別記事に切り出した

SSH Tunnel (ec2踏み台) を使って直接接続する

Aurora Serverless はpublic ipを持てない。
ふつーに自由にクエリ書きたいときに困るよねってことで、
@hhrrwwttrrさんにおねがいして、踏み台EC2を作ってもらった。
踏み台を準備してもらうと、Sequel Proなどのクライアントからも直接SSH経由で接続できて便利。

作る手順とかは @hhrrwwttrrさんがわかりやすく書いてくれるって言ってた。

できあがり

認証情報の扱いにはきをつけてつかおうね

Viewing all 8936 articles
Browse latest View live