6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-05-25

過去記事「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への質問

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?