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

More than 1 year has passed since last update.

過去記事「Loopbackで簡単なRESTサービスを作ってみる」で簡単にRESTなサービスを作ってみましたが、
その後、ちょっとしたことでハマってしまったので対応などなどメモ。

そもそもハマったこととは

loopbackでもともと用意されているいくつかのクラスのうち、
CRUDサポートを持っており、ほとんどのbuilt-inクラスの親クラスになっている
「PersistedModel」には、もともと幾つかのREST API用の機能が用意されています。→詳しくはこちら

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

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を自分で追加する場合、この考え方に従って、
REST APIを定義することができます。

このうち、optionsとして記載のあるacceptsの部分で、
PersistedModelでbuilt-inしてあるものは、下記のとおり指定してあります。

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'}
  ],

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

解決策

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

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

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"ではこんな感じで書いてある。

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.