Getting Started で作成したプロジェクトにactionhero-tutorial を読みながらチュートリアルを実行していきます。写経していたらチュートリアルの翻訳みたいになりました。技術翻訳は楽しい作業なので気分転換になります。
プロジェクトの準備 今回の作業はこちらのリポジトリ にpushしています。
プロジェクト 前回 作成したプロジェクトに移動します。
$ cd ~/node_apps/docker_actionhero
雛形のpackage.jsonをblog用に編集します。
~/node_apps/docker_actionhero/package.json { "author" : "Masato Shimizu <ma6ato@gmail.com>" , "name" : "my-blog" , "description" : "my blog project" , "version" : "0.0.1" , ...
プロジェクトをここでコミットしておきます。
$ git init $ git add -A $ git commit -m 'first commit'
チュートリアルのRedisとasync Getting Started のリポジトリ とジェネレートされるコードに差分がありますが、ジェネレートに自分で写経したコードを使っていきます。
actionheroのコードはLoopBack のようなオートマジックはないので結構がりがりと書きますが、Express でスクラッチから書くよりは安全です。Redisをデータストアに使うサンプルとしても勉強になります。asyncもRedisもちょっとしたデータモデルを設計するとすぐに複雑になります。
またイニシャライザやアクションのコードはasync で書きます。チュートリアルasyncの実践にもなります。
Redisのリンク アプリの構成管理はDocker Composeを使います。serverサービスでサーバーを起動します。
~/node_apps/docker-actionhero/docker-compose.yml server: &defaults image: masato/actionhero volumes: - .:/app - /etc/localtime:/etc/localtime:ro environment: - REDIS_HOST=redisdb ports: - 8089 :8080 links: - redis:redisdb actionhero: <<: *defaults entrypoint: ["./node_modules/.bin/actionhero" ] npm: <<: *defaults entrypoint: ["npm" ] bash: <<: *defaults entrypoint: ["bash" ] redis: image: redis restart: always volumes: - ./redis:/data - /etc/localtime:/etc/localtime:ro rediscli: image: redis links: - redis
serverサービスとredisサービスのリンクしているので、environmentにredis
を指定しても動作しそうに見えます。
environment: - REDIS_HOST=redis links: - redis
なぜか以下のようなエラーが発生してしまいます。
server_1 | 2015-06-22 20:45:03 - emerg: Redis Error (client): Error: Redis connection to tcp://172.17.6.22:6379 failed - connect ENOENT
サービス名にエイリアスを作成するとRedisに接続できるようになりました。REDIS_*
の環境変数が何か問題を起こしているようです。
environment: - REDIS_HOST=redisdb ports: - 8089 :8080 links: - redis:redisdb
docker-composeのエイリアスの作成 Docker Composeを使いワンショットでサービスを実行するコマンドがノイズになってきたのでエイリアスを書きます。
~/.bashrc alias actionhero='docker-compose run --rm actionhero'
~/.bashrc
を再読込してactionhero
のエイリアスをテストします。
$ source ~/.bashrc $ actionhero help info: actionhero >> help info: actionhero - a node.js API framework for both tcp sockets, web sockets, and http clients. Binary options: * help (default) * start * startCluster * generate * generateAction * generateTask * generateInitializer * generateServer ...
イニシャライザ blog このセクションではinitializers/blog.js を作成します。ドキュメントはInitializers を参照します。
イニシャライザにはアプリの共通コードを書きます。データベースに接続するモデルやミドルウェアなどです。ここで作成したクラスはapiオブジェクトに追加して使います。例をあげるとapi.mysql
やapi.game
はアクションやタスクのスコープで利用することができます。別の方法ではサーバーを起動したとき_start
メソッド内でコードを実行することもできます。
今回はblogを構築しているので、最初に投稿やコメントの保存場所が必要です。actionheroでは最初からRedisがapi.redis.client
を通して使えるのでさっそくデータを保存してみます。blog
イニシャライザを新規作成します。
$ cd ~/node_apps/docker-actionhero $ actionhero generateInitializer --name=blog info: actionhero >> generateInitializer info: - wrote file '/app/initializers/blog.js'
/app/initializers/blog.js
にblogの共通関数を定義します。
~/node_apps/docker-actionhero/initializers/blog.js 'use strict' ;module .exports = { loadPriority: 1000 , startPriority: 1000 , stopPriority: 1000 , initialize: function (api, next ) { var redis = api.redis.client; api.blog = { separator: ";" , postPrefix: "posts" , commentPrefix: "comments:" , postAdd: function (userName, title, content, next ) { var key = this .buildTitleKey(userName, title); var data = { content: content, title: title, userName: userName, createdAt: new Date ().getTime(), updatedAt: new Date ().getTime(), }; redis.hmset(key, data, function (error ) { next(error); }); }, ... }
コードの補足説明です。
posts
はRedisのHash型です。contentといくつかのmeta dataを持ちます。
comments
もRedisのHash型です。comment毎のkeyを持ちます。
すべて非同期関数を作成します。asyncの慣例として常に`callback(error,data0)を返します。
このレイヤーでは認証やバリデーションは気にしません。
ユーザーと認証 このセクションではinitializers/users.js とpackage.json を作成します。
blogは通常ユーザー認証が必要です。先ほどと同様にコマンドを実行してinitializerを作成します。
$ actionhero generateInitializer --name=users info: actionhero >> generateInitializer info: - wrote file '/app/initializers/users.js'
initializers/users.js
は以下のように作成されました。
~/node_apps/docker-actionhero/initializers/users.js 'use strict' ;var crypto = require ('crypto' );var salt = "asdjkafhjewiovnjksdv" ;module .exports = { loadPriority: 1000 , startPriority: 1000 , stopPriority: 1000 , initialize: function (api, next ) { var redis = api.redis.client; api.users = { usersHash: "users" , add: function (userName, password, next ) { var self = this ; redis.hget(self.usersHash, userName, function (error, data ) { if (error){ next(error); }else if (data){ next("userName already exists" ); }else { self.cryptPassword(password, function (error, hashedPassword ) { if (error){ next(error); }else { var data = { userName: userName, hashedPassword: hashedPassword, createdAt: new Date ().getTime(), }; redis.hset(self.usersHash, userName, JSON .stringify(data), function (error ) { next(error); }); } }); } }); }, ... }
いくつか注意事項です。
先ほどと同様にデータは全てRedisのHash型に保存します。
user
を削除したときは関連するposts
とcomments
も削除する必要があります。
md5だけを使ってuser
のパスワードをハッシュ化します。プロダクションではよりセキュアな方法を選択すべきで、たとえばBCrypt などを使います。
ミドルウェアのpublicとprivateなアクション 次にinitializers/middleware.js を作成します。ドキュメントはMiddleware です。
上記のステップでapi.users.authenticate
メソッドを作成しましたがまだ使っていません。このメソッドは明らかに保護が必要なメソッドです。ポストを追加したりユーザーを削除したりするメソッドも同様です。ここには何らかのセーフガードを導入する必要があります。
actionheroではラップしてusers
の全てのイニシャライザのメソッドはアクション内で利用します。さっそくミドルウェアを作成してアクションに追加してみます。
$ actionhero generateInitializer --name=middleware info: actionhero >> generateInitializer info: - wrote file '/app/initializers/middleware.js'
actionheroでは関数の配列をアクションの前後で実行することができます。今回必要なのは事前チェックでアクションを実行してよいか判断する関数です。この関数からアクション自体にアクセスもできます。データベースのコネクションも使えます。ミドルウェアはauthenticated = true
をアクションの定義に追加すると有効になって実行されます。ミドルウェア側ではapi.actions.addPreProcessor(authenticationMiddleware)
のようにしてアクションに追加します。
アクションの作成 このセクションではactions/users.js とactions/blog.js を作成します。ドキュメントはActions です。
posts
イニシャライザにgetter/setterなどヘルパー関数を定義しました。この関数はアクション内で使います。1つのファイルに複数のアクションを定義します。comments
を使うアクションとposts
を使うアクションを作成します。
$ actionhero generateAction --name=users $ actionhero generateAction --name=blog
アクションにauthenticated = true
を追加して必要なセキュリティ機能を有効にします。
テスト Docker Composeからserverサービスをupします。
$ docker-compose up server Recreating dockeractionhero_redis_1... Creating dockeractionhero_server_1... Attaching to dockeractionhero_server_1 server_1 | server_1 | > my-blog@0.0.1 start /app server_1 | > actionhero start server_1 | server_1 | info: actionhero >> start server_1 | 2015-06-22 21:42:31 - notice: *** starting actionhero *** server_1 | 2015-06-22 21:42:31 - notice: pid: 15 server_1 | 2015-06-22 21:42:31 - notice: server ID: 172.17.6.72 server_1 | 2015-06-22 21:42:31 - info: ensuring the existence of the chatRoom: defaultRoom server_1 | 2015-06-22 21:42:31 - info: ensuring the existence of the chatRoom: anotherRoom server_1 | 2015-06-22 21:42:31 - info: actionhero member 172.17.6.72 has joined the cluster server_1 | 2015-06-22 21:42:31 - notice: starting server: web server_1 | 2015-06-22 21:42:31 - notice: starting server: websocket server_1 | 2015-06-22 21:42:32 - notice: environment: development server_1 | 2015-06-22 21:42:32 - notice: *** Server Started @ 2015-06-22 21:42:32 ***
ユーザーの追加 ユーザーの登録をします。
$ curl -X POST \ -d "userName=evan" \ -d "password=password" \ "http://localhost:8089/api/userAdd"
以下のレスポンスが返ります。
{ "serverInformation" : { "serverName" : "actionhero API" , "apiVersion" : "0.0.1" , "requestDuration" : 9 , "currentTime" : 1434977027506 }, "requesterInformation" : { "id" : "137e30e7478dac9cd57c13b4bc91a62eb1b14d2b-b67044b1-93fc-4541-9f87-8299f1b8704a" , "fingerprint" : "137e30e7478dac9cd57c13b4bc91a62eb1b14d2b" , "remoteIP" : "172.17.42.1" , "receivedParams" : { "userName" : "evan" , "password" : "password" , "action" : "userAdd" , "apiVersion" : 1 } } }
ログは以下のように出力されます。
server_1 | 2015-06-22 21:43:47 - info: [ action @ web ] to=172.17.42.1, action=userAdd, params={"userName" :"evan" ,"password" :"password" ,"action" :"userAdd" ,"apiVersion" :1}, duration=3
Redisのレコードを確認してみます。Hash型のusersにレコードが作成されました。
$ docker-compose run --rm rediscli bash -c 'redis-cli -h $REDIS_PORT_6379_TCP_ADDR' 172.17.6.28:6379> keys "*" 172.17.6.71:6379> keys "*" 1) "actionhero:chatRoom:rooms" 2) "actionhero:stats" 3) "users" 172.17.6.71:6379> type "users" hash 172.17.6.71:6379> hgetall "users" 1) "evan" 2) "{\"userName\":\"evan\",\"hashedPassword\":\"8c0efc73baa95ec0b43f2f9c6515e3a7\",\"createdAt\":1434977027504}"
ログイン ログインをしてみます。
$ curl -X POST \ -d "userName=evan" \ -d "password=password" \ "http://localhost:8089/api/authenticate"
以下のレスポンスが返ります。"authenticated": true
が出力されて成功しました。
{ "authenticated" : true , "serverInformation" : { "serverName" : "actionhero API" , "apiVersion" : "0.0.1" , "requestDuration" : 5 , "currentTime" : 1434977341898 }, "requesterInformation" : { "id" : "98a942efd4527a03678ce104adc6d9d9d89cd35b-018bde25-ebfa-420a-8c68-714f3749a473" , "fingerprint" : "98a942efd4527a03678ce104adc6d9d9d89cd35b" , "remoteIP" : "172.17.42.1" , "receivedParams" : { "userName" : "evan" , "password" : "password" , "action" : "authenticate" , "apiVersion" : 1 } } }
投稿する blogに投稿してみます。
$ curl -X POST \ -d "userName=evan" \ -d "password=password" \ -d "title=first-post" \ -d "content=My%20first%20post.%20%20Yay." \ "http://localhost:8089/api/postAdd"
以下のレスポンスが返ります。特に成功を示す値は返らないようです。エラーになっていないので成功しているようです。
{ "serverInformation" : { "serverName" : "actionhero API" , "apiVersion" : "0.0.1" , "requestDuration" : 3 , "currentTime" : 1434978106847 }, "requesterInformation" : { "id" : "e66f4f3ff7b52631513053f2c93c268153d0f591-315a8670-8dc4-4101-b520-81501da95e48" , "fingerprint" : "e66f4f3ff7b52631513053f2c93c268153d0f591" , "remoteIP" : "172.17.42.1" , "receivedParams" : { "userName" : "evan" , "password" : "password" , "title" : "first-post" , "content" : "My first post. Yay." , "action" : "postAdd" , "apiVersion" : 1 } }
Redisには"posts;evan;first-post"
のキーでHash型のレコードが作成されました。
172 .17 .6 .71 :6379 > keys "*" 1 ) "actionhero:chatRoom:rooms" 2 ) "posts;evan;first-post" 3 ) "actionhero:stats" 4 ) "users" 172 .17 .6 .71 :6379 > type "posts;evan;first-post" hash 172 .17 .6 .71 :6379 > hgetall "posts;evan;first-post" 1 ) "content" 2 ) "My first post. Yay." 3 ) "title" 4 ) "first-post" 5 ) "userName" 6 ) "evan" 7 ) "createdAt" 8 ) "1434978106846" 9 ) "updatedAt" 10 ) "1434978106846"
投稿を取得する 認証があるので投稿はPOSTメソッドで取得します。
$ curl -X POST \ -d "userName=evan" \ -d "title=first-post" \ "http://localhost:8089/api/postView"
レスポンスは以下です。
{ "post" : { "content" : "My first post. Yay." , "title" : "first-post" , "userName" : "evan" , "createdAt" : "1434978106846" , "updatedAt" : "1434978106846" }, "serverInformation" : { "serverName" : "actionhero API" , "apiVersion" : "0.0.1" , "requestDuration" : 2 , "currentTime" : 1434978387670 }, "requesterInformation" : { "id" : "138ec5166fe759881affdc39c99e3cfde8164898-86704c11-8001-41c6-badc-83a06243ee5d" , "fingerprint" : "138ec5166fe759881affdc39c99e3cfde8164898" , "remoteIP" : "172.17.42.1" , "receivedParams" : { "userName" : "evan" , "title" : "first-post" , "action" : "postView" , "apiVersion" : 1 } } }
コメントを追加する $ curl -X POST \ -d "userName=evan" \ -d "title=first-post" \ -d "comment=cool%20post" \ -d "commenterName=someoneElse" \ "http://localhost:8089/api/commentAdd"
以下のレスポンスが返ります。
{ "serverInformation" : { "serverName" : "actionhero API" , "apiVersion" : "0.0.1" , "requestDuration" : 2 , "currentTime" : 1434978527198 }, "requesterInformation" : { "id" : "854e96b72fa0341efb5b408af852a6d210a83e31-0466bf62-a83b-4f9a-b431-b7543baa6f05" , "fingerprint" : "854e96b72fa0341efb5b408af852a6d210a83e31" , "remoteIP" : "172.17.42.1" , "receivedParams" : { "userName" : "evan" , "title" : "first-post" , "comment" : "cool post" , "commenterName" : "someoneElse" , "action" : "commentAdd" , "apiVersion" : 1 } } }
Redisに"comments:;evan;first-post"
のキーでHash型のレコードが作成されました。コメントはJSONの文字列が保存されています。
172.17.6.71:6379> keys "*" 1) "actionhero:stats" 2) "users" 3) "actionhero:chatRoom:rooms" 4) "posts;evan;first-post" 5) "comments:;evan;first-post" 172.17.6.71:6379> type "comments:;evan;first-post" hash 172.17.6.71:6379> hgetall "comments:;evan;first-post" 1) "someoneElse1434978527197" 2) "{\"comment\":\"cool post\",\"createdAt\":1434978527197,\"commentId\":\"someoneElse1434978527197\"}"
コメントを見る コメントも認証が入るのでPOSTで取得します。
$ curl -X POST \ -d "userName=evan" \ -d "title=first-post" \ "http://localhost:8089/api/commentsView"
レスポンスのJSONからコメントを取得するときは、comments
のキーで取得します。
{ "comments" : [ { "comment" : "cool post" , "createdAt" : 1434978527197 , "commentId" : "someoneElse1434978527197" } ], "serverInformation" : { "serverName" : "actionhero API" , "apiVersion" : "0.0.1" , "requestDuration" : 2 , "currentTime" : 1434978785461 }, "requesterInformation" : { "id" : "9fff33d8178275b64e168c1cc26bf7e1e9fb3bd8-df377114-716b-4558-9dc0-385f40a7efb4" , "fingerprint" : "9fff33d8178275b64e168c1cc26bf7e1e9fb3bd8" , "remoteIP" : "172.17.42.1" , "receivedParams" : { "userName" : "evan" , "title" : "first-post" , "action" : "commentsView" , "apiVersion" : 1 } } }
テストも通ったのでコミットしておきます。
$ git add . $ git commit -m 'actions created'