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); } });
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 - 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
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ペインでは、ジョブの実行結果がリスト表示されます。ジョブの開始時間、最新のステータスメッセージ、実行結果を確認することができます。
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つの引数が渡されます。
- request - リクエスト情報が格納されたオブジェクトです。次のフィールドが設定されています。
- params - クライアントから送信されたパラメータ
- user - リクエストを発行したParse.User。ログインしていない場合は設定されない。
- response - レスポンス情報は次の2つのファンクションを含みます。
ファンクションが正常に完了したら、次のようなレスポンスがクライアントに返ります。
{ "result": 4.8 }
エラーが発生した場合のレスポンスは次のようになります。
{ "code": 141, "error": "movie lookup failed" }
Running Code On Save
特定のデータフォーマットに強制するようなケースもクラウド上でコードを実行すべきでしょう。たとえば、AndroidとiOS両方のアプリケーションがある場合、両方のデバイスでデータを検証することになるでしょう。個別の環境でコードを書くよりも、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の中で見つけることができます。