[Loopback] build-inなREST APIが想定通りに動かなかったのでやったことまとめ

過去記事「[Loopbackで簡単なRESTサービスを作ってみる](http://qiita.com/yosi-q/items/b1640b89ab03e608935a)」で簡単にRESTなサービスを作ってみましたが、
その後、ちょっとしたことでハマってしまったので対応などなどメモ。

# そもそもハマったこととは
loopbackでもともと用意されているいくつかのクラスのうち、
CRUDサポートを持っており、ほとんどのbuilt-inクラスの親クラスになっている
「PersistedModel」には、もともと幾つかのREST API用の機能が用意されています。→詳しくは[こちら](http://docs.strongloop.com/display/public/LB/PersistedModel+REST+API)。

これを使って、私は以下のようなmodelを定義しました。

```json:sample-model.json
{
  "name": "SampleModel",
  "base": "PersistedModel",
  "idInjection": false,
  "properties": {
    "id": {
      "type": "string",
      "id": "true",
      "required": true,
      "doc": "MODEL ID"
    },
    "prop1": {
      "type": "string",
      "required": true
    }
  },
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": []
}
```

"id"は、名前の通りモデルのIDであり、ユニークになるよう管理されます。
また、"type"は記載のとおり、"string"ですが**すべて数字で構成された文字列**とします。

この時、build-in REST APIである`findById`を使用してモデルの情報を取得しようとした際、
**数値で見て「9007199254740992」を超える場合に、情報が存在するにも関わらす、モデルが取得できない自体が発生しました。**

さぁ困ったということで、解決に向けていろいろとやった次第です。

# 原因確認
## DEBUGによる内容確認の実施
いろいろとやり方はあると思いますが、RESTを使いながらDEBUGしたいなぁと考えたので、
DEBUGモードでNode.jsを起動しました。
具体的には、コマンドラインから下記のように起動するだけです。

% cd (アプリケーションのHOMEDIRに当たるところ）
% DEBUG=* node .

これで、loopback内に指定されているdebugメッセージは大体Getできる。（詳しくはおまけの「DEBUG指定のお話」で。）

すると、こんなメッセージが出てきた。

```
  strong-remoting:shared-method - findById - invoke with +11ms [ 9007199254740992, undefined, [Function: callback] ]
  strong-remoting:shared-method - findById - result null +25ms
  strong-remoting:rest-adapter Invoking rest.after for SampleModel.findById +6ms
  express:router restRemoteMethodNotFound  : /api/SampleModels/9007199254740993 +143ms
  express:router restUrlNotFound  : /api/SampleModels/9007199254740993 +8ms
  express:router restErrorHandler  : /api/SampleModels/9007199254740993 +2ms
  strong-remoting:rest-adapter Error in GET /SampleModels/9007199254740993: Error: Unknown "SampleModel" id "9007199254740993".
```

順番前後しますが、4行目にある「*/api/SampleModels/9007199254740993*」がアクセスして、
**ID: 9007199254740993 のモデルと取りに来たつもりなのに**、
1行目、`findById`が動く時点では、**ID: 9007199254740992 のモデル取得に置き換わっている**
ことがわかりました。

このIDの数値で周辺値ではどうなるか傾向を見たところ、
数値で見て、「9007199254740992」以下の場合は正しく取得できます。
偶数はそのままの値として渡るのに、奇数だと+1 or -1の値が渡るようでした。

さらに、**ID: hogehoge** を与えるとどうなるかを見たところ、下記のようになる。

```
  strong-remoting:shared-method - findById - invoke with +11ms [ 'hogehoge', undefined, [Function: callback] ]
```

ここまででわかったのは下記のようなお話。

 * 数字のみのIDの場合は、RESTコール時は内部で「数値として」扱かわれていること
 * 「9007199254740992」を境に、RESTコール時に指定した値が正しく渡されないこと
 * 数値であっても、stringのIDを取得はできること（文字か数値かは関係ない）

**要は正しく値が渡ればいいってことなのですね。**

## 「9007199254740992」の謎
これはぐぐったらすぐでした。

http://so-zou.jp/web-app/tech/programming/javascript/grammar/data-type/number/
要は、**「整数として精度が保証される限界」= 「9,007,199,254,740,992」**なのです。

## じゃあ、どうだったらいい？
じゃあどうするって言えば、「数値」で渡すんじゃなく「文字列」として渡す必要があるし、
そもそもそれが正しい形だと思う。

なぜ「文字列」として渡らないのかを調べてみました。

# 問題調査 - なぜ文字列として渡らないのか
## 調査に際してやったこと
プログラム達がScriptでよかった（笑
直接プログラムにdebugコメントを入れながら、
どこから文字列で、どこからが数値なのかを確認して、下記の結論に至りました。

## わかったこと： 定義内容
REST APIを自分で追加する場合、[この考え方](http://docs.strongloop.com/display/public/LB/Remote+methods#Remotemethods-Argumentdescriptions)に従って、
REST APIを定義することができます。

このうち、[optionsとして記載のあるaccepts](http://docs.strongloop.com/display/public/LB/Remote+methods#Remotemethods-Options)の部分で、
PersistedModelでbuilt-inしてあるものは、下記のとおり指定してあります。

```js:persisted-model.js
  accepts: [
    { arg: 'id', type: 'any', description: 'Model id', required: true,
      http: {source: 'path'}},
    { arg: 'filter', type: 'object',
      description: 'Filter defining fields and include'}
  ],
```

上記のとおり、``id``の``type``は「``any``」として指定してあるわけですが、
**HttpContext.coerce関数の中で``id``が数値に変換されていることがわかりました。(実際、そういう実装になってる。)**
しかも、その実装は、``type``を「``string``」で指定した場合は、文字列として扱うようになっていました。

# 解決策
ほぼいろんなModelのベースとなっている``PersistedModel``を変更するわけにはいかず、
しかも、``model.remoteMethod``で上書きできないかとも思ったのですが、うまくいかず。

結局、``SampleModel``のロジックコードの中に下記のように埋め込みました。

```js:sample-model.js
SampleModel.findByIdCustom = function(id, filter, cb) {
  SampleModel.findById(id, filter, cb);
}

//Define remote method
SampleModel.remoteMethod(
  'findByIdCustom',
  {
    description: 'Find a model instance by id from the data source.',
    accessType: 'READ',
    accepts: [
        { arg: 'id', type: 'string', description: 'Model id', required: true,
          http: {source: 'path'}},
        { arg: 'filter', type: 'object',
          description: 'Filter defining fields and include'}
    ],
    returns: {arg: 'data', type: 'user', root: true},
    http: {verb: 'get', path: '/:id'},
    rest: {after: SampleModel.convertNullToNotFoundError},
    isStatic: true
  }
);

//disable built-in remote method
SampleMethod.disableRemoteMethod('findById', true);
```

上書きできないbuilt-inは使い勝手いいといえるかどうか。。。

とりあえずは、こんな感じで解決に至ったのでした。

# おまけ
## DEBUG指定のお話
ここで指定している`DEBUG=*`ですが、loopbackアプリのなかでは**debug**モジュールを使っており、
プログラムの随所でdebugメッセージを出すように仕込んであります。

例えば、built-inモデルである"user"のロジックがある"user.js"ではこんな感じで書いてある。

```js:user.js
（略）
var debug = require('debug')('loopback:user');  //POINT(1)
（略）
user.hasPassword(credentials.password, function(err, isMatch) {
   if (err) {
     debug('An error is reported from User.hasPassword: %j', err);  //POINT(2)
     fn(defaultError);
（略）
```
これで行くと、debug logとして、下記のようなメッセージがでます。

```
loopback:user An error is reported from User.hasPassword: hogehoge
```
POINT(1)では、メッセージの分類というか、キーワードを。
POINT(2)では、実際のメッセージを指定してます。

"loopback:user"のメッセージのみ見たい場合はNode.js起動時に下記のとおりにするとよいです。

```
% DEBUG=loopback:user node .
```

## StackOverflowへの質問
書いてはみたけど、回答もらえず。英語が悪かったかな・・・(--;
とはいえ、いい勉強になりました。

http://stackoverflow.com/questions/30255524/loopback-rest-findbyid-doesnt-work-well