Edited at

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

More than 3 years have 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