Murayama blog.

プログラミング教育なブログ

Parse - Cloud Code - Modules

Modules

Cloud CodeはJavasScriptコードのモジュール化をサポートしています。ローディングモジュールから想定外の副作用を避けるために、Cloud CodeのモジュールにはCommonJSモジュールのような仕組みを用意しています。モジュールがロードされると、JavaScriptファイルがロードされて、ソースコードが実行され、グローバルなexportsオブジェクトが返されます。たとえば、cloud/name.jsモジュールは次のようになります。

var coolNames = ['Ralph', 'Skippy', 'Chip', 'Ned', 'Scooter'];
exports.isACoolName = function(name) {
  return coolNames.indexOf(name) !== -1;
}

cloud/main.jsは次のようになります。

var name = require('cloud/name.js');
name.isACoolName('Fred'); // returns false
name.isACoolName('Skippy'); // returns true;
name.coolNames; // undefined.

変数nameはisACoolNameという名前のファンクションを持ちます。requireに指定したパスは、Parseプロジェクトのルートからの指定になります。cloud/ディレクトリ内のモジュールのみロードできます。

Parse - Cloud Code - Networking

Networking

Cloud CodeではParse.Cloud.httpRequestを使うことで、HTTPサーバに対してHTTPリクエストを送信できます。このファンクションはオプションオブジェクトを設定値として呼び出します。シンプルなGETリクエストは次のようになります。

Parse.Cloud.httpRequest({
  url: 'http://www.parse.com/',
  success: function(httpResponse) {
    console.log(httpResponse.text);
  },
  error: function(httpResponse) {
    console.error('Request failed with response code ' + httpResponse.status);
  }
});

successは正常なHTTPのステータスコードが戻ったときに呼ばれます。そうでない場合はerrorが呼ばれます。

Query Parameters

オプションオブジェクトにparamsを設定することで、URLの後に付与するクエリパラメータを指定できます。JSONオブジェクトで指定する場合は次のようになります。

Parse.Cloud.httpRequest({
  url: 'http://www.google.com/search',
  params: {
    q : 'Sean Plott'
  },
  success: function(httpResponse) {
    console.log(httpResponse.text);
  },
  error: function(httpResponse) {
    console.error('Request failed with response code ' + httpResponse.status);
  }
});

文字列で指定する場合は次のようになります。

Parse.Cloud.httpRequest({
  url: 'http://www.google.com/search',
  params: 'q=Sean Plott',
  success: function(httpResponse) {
    console.log(httpResponse.text);
  },
  error: function(httpResponse) {
    console.error('Request failed with response code ' + httpResponse.status);
  }
});

Setting Headers

オプションオブジェクトにheaders属性を設定すれば、HTTPヘッダを送信できます。Content-Typeヘッダを送信する場合は次のようになります。

Parse.Cloud.httpRequest({
  url: 'http://www.example.com/',
  headers: {
    'Content-Type': 'application/json'
  },
  success: function(httpResponse) {
    console.log(httpResponse.text);
  },
  error: function(httpResponse) {
    console.error('Request failed with response code ' + httpResponse.status);
  }
});

Sending a POST Request

オプションオブジェクトにmethod属性を設定すればPOSTリクエストを送信できます。POSTのボディは、bodyを使って指定します。シンプルなPOSTリクエストは次のようになります。

Parse.Cloud.httpRequest({
  method: 'POST',
  url: 'http://www.example.com/create_post',
  body: {
    title: 'Vote for Pedro',
    body: 'If you vote for Pedro, your wildest dreams will come true'
  },
  success: function(httpResponse) {
    console.log(httpResponse.text);
  },
  error: function(httpResponse) {
    console.error('Request failed with response code ' + httpResponse.status);
  }
});

これはhttp://www.example.com/create_postに対して、url-form-encoded形式のボディをPOSTリクエストで送信します。ボディをJSON形式にする場合は次のようにします。

Parse.Cloud.httpRequest({
  method: 'POST',
  url: 'http://www.example.com/create_post',
  headers: {
    'Content-Type': 'application/json'
  },
  body: {
    title: 'Vote for Pedro',
    body: 'If you vote for Pedro, your wildest dreams will come true'
  },
  success: function(httpResponse) {
    console.log(httpResponse.text);
  },
  error: function(httpResponse) {
    console.error('Request failed with response code ' + httpResponse.status);
  }
});

body属性の値に文字列で指定することもできます。

The Response Object

successやerrorの際、レスポンスオブジェクトが戻ります。レスポンスオブジェクトは以下のプロパティを含みます。

  • status - HTTPレスポンスステータス
  • headers - レスポンスヘッダ
  • text - レスポンスボディ
  • data - 解析したレスポンス。ただし、送信されたcontent-typeがCloud Codeによって解析できた場合に限る

Parse - Cloud Code - Logging from Cloud Code

Logging from Cloud Code

parse logによって表示されるログファイルにメッセージを残したいなら、console.log, console.error, or console.warnを使用します。console.errorとconsole.warnはエラーログに書き込みます。

Parse.Cloud.define("Logger", function(request, response) {
  console.log(request.params);
  response.success();
});

Parse - Cloud Code - Development vs Production

Development vs Production

公開配布用のプロダクションアプリケーションとは別に、新しいコードを試すための開発用アプリケーションを使用できます。

Adding a New App to a Project

プロジェクトにリンクした複数のアプリケーションが必要になります。parse newコマンドは、プロジェクトに最初のアプリケーションをリンクさせます。parse add [alias]コマンドを使えばアプリケーションを追加することができます。

$ parse add production
Email: pirate@gmail.com
Password:
1:PiecesOfEightCounterProd
2:PiecesOfEightCounterDev
Select an App: 1

上記のサンプルでは、Cloud CodeプロジェクトにPiecesOfEightCounterProdアプリケーションをリンクさせています。また、アプリケーションを参照するproductionという新しい名前でエイリアスを作成しました*1

Developing Cloud Code

新しいコードを開発する間、コマンドラインツールのdevelopコマンドを利用すると、プロジェクトの更新を検知して差分をCloud Codeにアップロードするようになります。コマンドは次のようになります。

$ parse develop development
E2013-03-19:20:17:01.423Z] beforeSave handler in release 'v1' ran for GameScore with the input:
  {"original": null, "update":{"score": 1337}}
 and failed validation with Each GamesScore must have a playerName
New release is named v58
I2013-03-19T20:17:10.343Z] Deployed v58 with triggers:
  GameScore:
    before_save

更新をPUSHするdevelopmentコマンドは、明示的に使用する必要があります。これは誤ってテストしていないコードをプロダクションアプリケーションにアップロードしてしまうのを避けるためです。コマンドラインツールは、更新の都度メッセージを表示します。終了するにはCtrl-Cをタイプします。

Deploying Code to Production

コードのアップデートとテストが完了したら、deployコマンドを使ってプロダクションアプリケーションにデプロイできます。

$ parse deploy production
New release is named v2

*1:試してみたけどコマンドの引数にエイリアスが使えない。。

Parse - Cloud Code - Custom Webhooks

Custom Webhooks

フレキシブルなフォーマットのデータを処理するカスタムWebフックを構築するためにCloud Code上でExpressを利用できます。これにより、あなたの記述したWebフックを他のWebサービスから呼び出すことができます。非JSON形式のデータのやりとりが必要な場合やParseのREST APIヘッダをサポートしないエンドポイントを呼び出す場合は、Cloudファンクションの代用として利用できます。記述したロジックはCloud Code上で動作しているので、カスタムWebフック処理内でもParse JavaScript SDKを利用できます。

カスタムWebフックでは、リクエストヘッダやボディを直接操作します。JSON、form-encoded、raw bytesなどのデータを受け取り、任意のパーサーで解析します。またHTTPのBasic認証によって、Webフックを保護することもできます。次のサンプルはメッセージをParse Cloud上に保存する例です。

var express = require('express');
var app = express();
 
// Global app configuration section
app.use(express.bodyParser());  // Populate req.body
 
app.post('/notify_message', 
         express.basicAuth('YOUR_USERNAME', 'YOUR_PASSWORD'),
         function(req, res) {
  // Use Parse JavaScript SDK to create a new message and save it.
  var Message = Parse.Object.extend("Message");
  var message = new Message();
  message.save({ text: req.body.text }).then(function(message) {
    res.send('Success');
  }, function(error) {
    res.status(500);
    res.send('Error');
  });
});
 
app.listen();

上記のコードでは、リクエストボディを解析するためにexpress.bodyParserミドルウェアを使用しています。app.use(express.basicAuth(…))をグローバルなコンフィギュレーションセクションに記述していないことに気をつけてください。 これはHTTPのBasic認証をエンドポイントにだけ適用したいからです。アプリケーション内の他のエンドポイントは、publicなアクセスを受け付けることになります。

カスタムエンドポイントをテストするために、次のコマンドを実行します。form-encodedボディを含むリクエストを送信します*1

$ curl -X POST \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    -d 'text=hi'
    http://YOUR_USERNAME:YOUR_PASSWORD@example.parseapp.com/notify_message

リクエストボディのraw bytesにアクセスしたいなら、express.bodyParserではなく、parseExpressRawBodyミドルウェアを利用できます。JSONやwww-form-encodedをサポートしつつraw bytesも扱いたいなら、両方のミドルウェアをインクルードすることもできます。

var express = require('express');
var parseExpressRawBody = require('parse-express-raw-body');
var app = express();
 
// Global app configuration section
app.use(express.bodyParser());
app.use(parseExpressRawBody());
 
app.post('/receive_raw_data', 
         express.basicAuth('YOUR_USERNAME', 'YOUR_PASSWORD'),
         function(req, res) {
  // If you send this endpoint JSON or www-form-encoded data, then 
  // express.bodyParser will fill req.body with the corresponding data.
  // Otherwise, parseExpressRawBody will fill req.body with a Buffer 
  // object containing the request body.  You can also convert this 
  // Buffer to a string using req.body.toString().
});
 
app.listen();

*1:URLに/notify_messageを追加しました。

Parse - Cloud Code - Background Jobs

Background Jobs

Parseにはバックグラウンドで動作するジョブをセットすることができます。バックグラウンドジョブは、レスポンスタイムのかかる外部サイトとの連携や、プッシュノーティフィケーションのバッチ処理といった時間のかかるタスクに有効です。Cloudファンクションの実行でタイムアウトが発生した場合は、バックグラウンドジョブの実行を検討してください。

バックグラウンドジョブを実行するには、いくつかの制約に従う必要があります。

  • ジョブの実行時間が15分を超えると終了する
  • Basic planのユーザは並行処理できるジョブは1つだけとなる。他のスケジュールされたジョブはキューに登録される
  • Pro planのユーザは並行処理できるジョブは2つとなる。 他のスケジュールされたジョブはキューに登録される

Writing a Background Job

バックグラウンドジョブの記述はCloudファンクションと似ています。Parse.Userオブジェクトにplanフィールドを追加するユーザマイグレーションを実行するなら次のようになります。

Parse.Cloud.job("userMigration", function(request, status) {
  // Set up to modify user data
  Parse.Cloud.useMasterKey();
  var counter = 0;
  // Query for all users
  var query = new Parse.Query(Parse.User);
  query.each(function(user) {
      // Update to plan value passed in
      user.set("plan", request.params.plan);
      if (counter % 100 === 0) {
        // Set the  job's progress status
        status.message(counter + " users processed.");
      }
      counter += 1;
      return user.save();
  }).then(function() {
    // Set the job's success status
    status.success("Migration completed successfully.");
  }, function(error) {
    // Set the job's error status
    status.error("Uh oh, something went wrong.");
  });
});

Cloundファンクションではsuccess、errorをハンドリングしていました。バックグラウンドジョブでは、処理の完了時にstatus.success() かstatus.error()を呼び出すことで、ジョブの実行結果が設定されます。もし、これらのメソッドを呼び出さなかった場合、ジョブは持ち時間の15分を経過してタイムアウトする形になります*1。status.message()を呼び出せば処理の途中経過を出力できます。ただし、status.success()を呼び出した後にstatus.message()を呼び出しても無視されます。

上記のコードをデプロイしたら、次のコマンドを実行してテストしてみましょう。

 curl -X POST \
  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"plan":"paid"}' \
  https://api.parse.com/1/jobs/userMigration

Setting up a Schedule

バックグラウンドジョブをデプロイできたら、DashboardのCloud Codeタブからジョブをスケジューリングできるようになります。Scheduled Jobsペインでは、スケジュール済みのジョブの確認と新たなジョブを追加することができます。新たなジョブを登録したら、ジョブの説明、必要なパラメータ、実行時間、実行頻度を指定します。スケジューリングされたジョブはその場で実行することもできます。また、ジョブスケジュールからの削除も可能です。Job Statusペインでは、ジョブの実行結果がリスト表示されます。ジョブの開始時間、最新のステータスメッセージ、実行結果を確認することができます。

*1:と書いてるけどsuccessとerrorを呼ばないようにすると15分待たずにFailedとなるみたい。

Parse - Cloud Code - Cloud Functions

Cloud Functions

Cloud Codeの役に立つもう少し複雑な例をみてみましょう。ちょっとした演算結果を取得するのに、膨大なオブジェクトをデバイス上にダウンロードしなければならないというようなケースは、クラウド上で演算すべきでしょう。たとえば、映画(movie)のレビュー(review)を扱うアプリケーションを考えてみましょう。Reviewオブジェクトは次のように表現されるでしょう。

{
  "movie": "The Matrix",
  "stars": 5,
  "comment": "Too bad they never made any sequels."
}

仮に"The Matrix"のstarsのアベレージが欲しいなら、デバイス上ですべてのReviewを問い合わせて平均値を演算する必要があるでしょう。多くのデータを取得したにも関わらず必要なのは平均値だけなのです。このような場合、Cloud Codeを使えば、映画の名前を渡すだけでstarのアベレージだけを返すことができます。

Cloudファンクションは、requestオブジェクトにJSONパラメータを含むことができます。これを使えば映画の名前を渡すことができます。Parse JavaScript SDKクラウド環境でも利用できるので、Reviewオブジェクトを取得するために使用することができます。averageStarsファンクションの実装は次のようになります。

Parse.Cloud.define("averageStars", function(request, response) {
  var query = new Parse.Query("Review");
  query.equalTo("movie", request.params.movie);
  query.find({
    success: function(results) {
      var sum = 0;
      for (var i = 0; i < results.length; ++i) {
        sum += results[i].get("stars");
      }
      response.success(sum / results.length);
    },
    error: function() {
      response.error("movie lookup failed");
    }
  });
});

averageStarsファンクションと前回のhelloファンクションの違いは、Cloudファンクションを呼び出すときにパラメータを渡しているところです。リクエストパラメータはrequest.params.movieのようにアクセスできます。次にCloudファンクションがどのように呼び出されるのかを見ていきましょう。

Calling a Cloud Function

Cloudファンクションは、REST APIのようなクライアントSDKから呼び出されます。たとえば、映画の名前をパラメータに指定してaverageStarsファンクションを呼び出す場合は次のようになります。

 curl -X POST \
  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"movie":"The Matrix"}' \
  https://api.parse.com/1/functions/averageStars

通常、Cloudファンクションには2つの引数が渡されます。

  1. request - リクエスト情報が格納されたオブジェクトです。次のフィールドが設定されています。
    1. params - クライアントから送信されたパラメータ
    2. user - リクエストを発行したParse.User。ログインしていない場合は設定されない。
  2. response - レスポンス情報は次の2つのファンクションを含みます。
    1. success - このファンクションは、クライアントに返す任意のパラメータを引数に取ります。このオブジェクトはJSONオブジェクト/配列、あるいはParse.Objectを指定できます。
    2. error - エラーが発生した場合に呼び出します。クライアントに返すための任意のエラーメッセージを渡します。


ファンクションが正常に完了したら、次のようなレスポンスがクライアントに返ります。

{
  "result": 4.8
}

エラーが発生した場合のレスポンスは次のようになります。

{
  "code": 141,
  "error": "movie lookup failed"
}

Running Code On Save

特定のデータフォーマットに強制するようなケースもクラウド上でコードを実行すべきでしょう。たとえば、AndroidiOS両方のアプリケーションがある場合、両方のデバイスでデータを検証することになるでしょう。個別の環境でコードを書くよりも、Cloud Codeで実装すれば1つの実装で済みます。

映画レビューの例を見てみましょう。starsの有効範囲を1, 2, 3, 4, 5に限定するものとします。-6や1337starsはReviewに指定できないようにします。有効範囲外のデータをリジェクトする場合、beforeSaveメソッドを使うことができます。

Parse.Cloud.beforeSave("Review", function(request, response) {
  if (request.object.get("stars") < 1) {
    response.error("you cannot give less than one star");
  } else if (request.object.get("stars") > 5) {
    response.error("you cannot give more than five stars");
  } else {
    response.success();
  }
});

response.errorが呼び出されると、Reviewオブジェクトは保存せず、クライアントにエラーが返ります。response.successが呼び出されると、オブジェクトの保存は完了します。この2つのコードのいずれかを呼び出すように実装します。

モバイルアプリケーションの場合は異なるバージョンが存在しがちですが、Cloud Codeはすべてのユーザーに同じバージョンを提供できるという利点もあります。したがって、入力チェックを実装していないバージョンのアプリケーションを起動したとしても、Cloud Code上のbeforeSaveメソッドによって問題を防ぐことができるでしょう。

Parse JavaScript SDKて定義済みのクラス(Parse.Userなど)に対してbeforeSaveを使う場合は、第1引数に文字列を渡すのではなく、クラス自身を指定するようにしてください。

Modifying Objects On Save

不正なデータであってもスローするのではなく、保存する前に適切に処理したいこともあるでしょう。beforeSaveは、このようなケースにも利用することができます。オブジェクトを適切に処理してからresponse.successを呼び出すようにします。

映画レビューの例の場合、長過ぎるコメントも上手く処理したいというケースがあるでしょう。コメントを1行で表示することが難しいかもしれません。そのようなときは、beforeSaveメソッドを使えば140文字以内に切り詰めることができます。

Parse.Cloud.beforeSave("Review", function(request, response) {
  var comment = request.object.get("comment");
  if (comment.length > 140) {
    // Truncate and add a ...
    request.object.set("comment", comment.substring(0, 137) + "...");
  }
  response.success();  
});

Performing Actions After a Save

オブジェクトの保存が完了したら、PUSHノーティフィケーションを送るなど、後処理を追加したいこともあるでしょう。afterSaveメソッドを使えばこのような要件を達成できます。たとえば、ブログポストのコメント数を記録するような場合は、次のようになるでしょう。

Parse.Cloud.afterSave("Comment", function(request) {
  query = new Parse.Query("Post");
  query.get(request.object.get("post").id, {
    success: function(post) {
      post.increment("comments");
      post.save();
    },
    error: function(error) {
      console.error("Got an error " + error.code + " : " + error.message);
    }
  });
});

クライアントはどのように終了したのかに関係なく、保存リクエストに対しての正常なレスポンスを受信します。例えば、ハンドラが例外をスローしたとしても、クライアントは正常なレスポンスとして受け取ります。ハンドラの実行中に発生した例外はCloud Codeログの中で見つけることができます。

Running Code On Delete

オブジェクトの削除前にカスタムCloud Codeを実行できます。これにはbeforeDeleteメソッドを使います。これにより、ACLでの表現よりもより洗練された形で削除ポリシーを実装することができます。たとえば、フォトアルバムアプリがあるとして、写真(Photo)はアルバム(Album)と関連を持っています。ユーザが写真の残っているアルバムを削除しようとするご操作を防ぐためには、次のように実装することができます。

Parse.Cloud.beforeDelete("Album", function(request, response) {
  query = new Parse.Query("Photo");
  query.equalTo("album", request.object.id);
  query.count({
    success: function(count) {
      if (count > 0) {
        response.error("Can't delete album if it still has photos.");
      } else {
        response.success();
      }
    },
    error: function(error) {
      response.error("Error " + error.code + " : " + error.message + " when getting photo count.");
    }
  });
});

response.errorが呼び出されると、Albumオブジェクトは削除されません。また、クライアントにはエラーが返却されます。response.successが呼び出されると、正常にAlbumの削除が完了します。コードの中でいずれかのメソッドを呼び出すようにしてください。
Parseの事前定義済みのクラス(Parse.Userなど)に対してbeforeDeleteメソッドを使う場合は、第1引数に文字列ではなく、クラス自身を渡すようにしてください。

Performing Actions After a Delete

オブジェクトの削除が完了したら、PUSHノーティフィケーションを送るなど、後処理を追加したいこともあるでしょう。afterDeleteメソッドを使えばこのような要件を達成できます。たとえば、ブログポストの削除時に関連するコメントを削除するような場合は、次のようになるでしょう。

Parse.Cloud.afterDelete("Post", function(request) {
  query = new Parse.Query("Comment");
  query.equalTo("post", request.object.id);
  query.find({
    success: function(comments) {
      Parse.Object.destroyAll(comments, {
        success: function() {},
        error: function(error) {
          console.error("Error deleting related comments " + error.code + ": " + error.message);
        }
      });
    },
    error: function(error) {
      console.error("Error finding related comments " + error.code + ": " + error.message);
    }
  });
});

afterDeleteハンドラはrequest.objectを通じて削除済みのオブジェクトにアクセスできます。このフェッチ済みのオブジェクトは、再度フェッチしたり、保存したりすることはできません。

クライアントはどのように終了したのかに関係なく、保存リクエストに対しての正常なレスポンスを受信します。例えば、ハンドラが例外をスローしたとしても、クライアントは正常なレスポンスとして受け取ります。ハンドラの実行中に発生した例外はCloud Code logの中で見つけることができます。

Resource Limits

Timeouts

Cloudファンクションは、15秒を超えるとタイムアウトとなります。beforeSaveファンクション、afterSaveファンクションは3秒を超えるとタイムアウトとなります。CloudファンクションやbeforeSave/afterSaveファンクションが、他のCloud Codeから呼び出された場合、呼び出し側のファンクションに残された時間に制限されます。例えば、beforeSaveファンクションが、13秒経過済みのCloudファンクションから呼ばれた場合、通常の3秒よりも短い残りの2秒でタイムアウトとなります。

Network requests

successやerrorの後の進行中のネットワークリクエストは、キャンセルされます。通常、successを呼ぶ前に、すべてのネットワークリクエストが完了するように待つべきです。afterSaveファンクションはsuccessやerrorを呼びません。Cloud Codeはすべてのネットワークリクエストが終了するのを待ちます。