過去記事「Loopbackで簡単なRESTサービスを作ってみる」で簡単にRESTなサービスを作ってみましたが、
その後、ちょっとしたことでハマってしまったので対応などなどメモ。
そもそもハマったこととは
loopbackでもともと用意されているいくつかのクラスのうち、
CRUDサポートを持っており、ほとんどのbuilt-inクラスの親クラスになっている
「PersistedModel」には、もともと幾つかのREST API用の機能が用意されています。→詳しくはこちら。
これを使って、私は以下のようなmodelを定義しました。
{
"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してあるものは、下記のとおり指定してあります。
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
のロジックコードの中に下記のように埋め込みました。
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"ではこんな感じで書いてある。
(略)
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への質問
書いてはみたけど、回答もらえず。英語が悪かったかな・・・(--;
とはいえ、いい勉強になりました。