0%

actionhero.js入門 - Part2: チュートリアル (アクションの作成)

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.mysqlapi.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 = {

// constants
separator: ";",
postPrefix: "posts",
commentPrefix: "comments:",

// posts
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.jspackage.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 = {
// constants
usersHash: "users",

// methods
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を削除したときは関連するpostscommentsも削除する必要があります。
  • 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.jsactions/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'