カテゴリー : Mongoose

[Mongoose][timekeeper] TypeError: Undefined type `FakeDate` at `fieldName`

Node.js + Mongoose な構成のウェブサービスのテストコードで timekeeper という時間操作モジュールを利用していてハマったことをメモ。

timekeeper.freeze と new mongoose.Schema でエラーの事例

timekeeper.freeze と new mongoose.Schema でエラーが発生するテストコード

'use strict';
 
const mongoose = require("mongoose");
const timekeeper = require("timekeeper");
 
describe("FakeDate error", () => {
  timekeeper.freeze(new Date());
  let schema = new mongoose.Schema({
    createdAt: {
      type: Date
    }
  });
  timekeeper.reset();
});

エラーメッセージ TypeError: Undefined type `FakeDate`

$ mocha test.js
 
/Users/bakorer/works/myapp/node_modules/mongoose/lib/schema.js:483
    throw new TypeError('Undefined type `' + name + '` at `' + path +
    ^
 
TypeError: Undefined type `FakeDate` at `createdAt`
  Did you try nesting Schemas? You can only nest using refs or arrays.
    at Function.Schema.interpretAsType (/Users/bakorer/works/myapp/node_modules/mongoose/lib/schema.js:483:11)
    at Schema.path (/Users/bakorer/works/myapp/node_modules/mongoose/lib/schema.js:415:29)
    at Schema.add (/Users/bakorer/works/myapp/node_modules/mongoose/lib/schema.js:310:12)
    at new Schema (/Users/bakorer/works/myapp/node_modules/mongoose/lib/schema.js:88:10)
    at Suite.<anonymous> (/Users/bakorer/works/myapp/test.js:8:16)
    at context.describe.context.context (/Users/bakorer/works/myapp/node_modules/mocha/lib/interfaces/bdd.js:73:10)
    at Object.<anonymous> (/Users/bakorer/works/myapp/test.js:6:1)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (/Users/bakorer/works/myapp/node_modules/coffee-script/lib/coffee-script/coffee-script.js:211:36)
    at Function.Module._load (module.js:300:12)
    at Module.require (module.js:353:17)
    at require (internal/module.js:12:17)
    at /Users/bakorer/works/myapp/node_modules/mocha/lib/mocha.js:157:27
    at Array.forEach (native)
    at Mocha.loadFiles (/Users/bakorer/works/myapp/node_modules/mocha/lib/mocha.js:154:14)
    at Mocha.run (/Users/bakorer/works/myapp/node_modules/mocha/lib/mocha.js:326:31)
    at Object.<anonymous> (/Users/bakorer/works/myapp/node_modules/mocha/bin/_mocha:350:7)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:974:3

FakeDate は timekeeper が用意した Date

timekeeper が以下のように JavaScript の Date オブジェクトと、timekeeper で独自定義した FakeDate オブジェクトを入れ替える処理をしています。

  /**
   * Replace the `Date` with `FakeDate`.
   */
  function useFakeDate() {
    Date = FakeDate
  }
 
  /**
   * Restore the `Date` to `NativeDate`.
   */
  function useNativeDate() {
    Date = NativeDate
  }

Date が Date でなく FakeDate に入れ替わってるタイミングで、new mongoose.Schema を呼び出すが FakeDate は MongooseTypes に定義されていない SchemaType なので例外が投げられてるっていう感じです。ザックリ説明すると。

new mongoose.Schema で SchemaType チェックしている Mongoose のコードはこの辺です lib/schema.js#L652-L671

テストコードの書き方次第でエラーは避けられる

timekeeper.freeze して timekeeper.reset するまでの間に、明示的に new mongoose.Schema したいテストコードを書くことはほぼないと思ってます。なので、new mongoose.Schema の処理を書いてる Model を定義してる module などは事前に require() しておくことでエラーを退避したいですね。

Mongoose documents を JSON に変換して response を返す

Express.js + Mongoose で documents を JSON に変換して response を返すには、

UserModel.find().lean().exec(function (err, users) {
  return res.json(users)
});

というような感じで lean() オプションを付けて plain javascript object を取得した値を res.json で返せばよいです。


参考情報

[Mongoose] index が未作成な field を sort に指定して stream で取得すると default batchSize だけ処理して正常終了するので困った

Mongoose で index が作成されていない field を sort に指定して stream で取得すると、 default batchSize の 1000 件だけ処理して正常終了するので困ったというお話です。

find() で全件一括取得するケース

find() で全件一括取得すると callback の第一引数 err には値は入らず、配列の末尾の要素に $err, code が含まれていました。

User.find().sort({
  createdAt: -1
}).setOptions({
  noCursorTimeout: true
}).exec(function(err, docs) {
  console.log(err);
  console.log(docs.length);
  console.log(docs[docs.length - 1]);
 
/**
以下、console.log の出力結果
 
null
1001
{ '$err': 'getMore runner error: Overflow sort stage buffered data usage of 33555763 bytes exceeds internal limit of 33554432 bytes',
  code: 17406,
...
**/
});

stream() で全件取得するケース

stram() で取得する場合も同じで、最後に取得した document に $err, code が含まれていました。

var stream = User.find()
.sort({ createdAt: -1 })
.setOptions({ noCursorTimeout: true })
.stream();
 
stream.on('data', function(user){
/**
default batchSize 1000 件だけ取得して、1001 件目に $err, code を含むデータを取得して、正常終了する
{ '$err': 'getMore runner error: Overflow sort stage buffered data usage of 33555763 bytes exceeds internal limit of 33554432 bytes',
  code: 17406,
...
**/
});
 
stream.on('error', function(err){
  // エラーイベントは呼び出されない
});

解決方法

sort に指定する field の index を作成する

index が作成されていないのが元々の問題なので、index 作成すれば解決します。

User.path('createdAt').index(true);

index が存在する field を sort に指定する

index size が気になるので作成したくないという場合は、default で用意されている _id を sort に指定する対応でもいいと思います。

User.find().sort({
  _id: -1
}).setOptions({
  noCursorTimeout: true
}).exec(function(err, docs) {});

所感

document が 1000 件しか取得できてないことに気付かない限り、エラーが発生していることに気付けないので Mongoose のエラーハンドリングの実装が微妙なのでは…


参考情報

[Node.js] Mongoose の virtual attributes を sinon で stub 化する

Mongoose の virtual attributes を sinon で stub 化する方法をご紹介します。

まず、下記のような Product model が定義されているとします。

# define Mongoose Product model
mongoose = require 'mongoose'
 
productSchema = new mongoose.Schema({
  # something
})
 
productSchema.virtual('discountPercentage').get ()->
  # calculate discountPercentage
  return discountPercentage
 
Product = mongoose.model 'Product', productSchema

そして、テストコードで discountPercentage virtual をスタブ化するコードは下記のような感じです。

# in test code
sinon = require 'sinon'
 
product = new Product()
 
console.log product.discountPercentage
# discountPercentage virtual の計算結果を返す
 
stub = sinon.stub product, 'get'
stub.withArgs('discountPercentage').returns 50
 
console.log product.discountPercentage
# 必ず 50 を返す
 
# 使い終わったら、忘れずに restore しましょう
stub.restore()

ポイントは sinon.stub mongooseInstance, ‘get’stub.withArgs(‘virtualName’).returns の部分で virtualName の返り値をスタブ化しているという点だけです。


参考情報

[Node.js] MONGOOSE WARNING がでないように stable version を使おう

Node.js + Mongoose のアプリ開発で MONGOOSE WARNING という警告メッセージが表示されたら、 unstable バージョンを利用していると思われるので stable なバージョンを使いましょう。

まず、アプリを起動しようとすると !!! MONGOOSE WARNING !!! と派手に怒られます。

$ node-dev app.js     
 
##############################################################
#
#   !!! MONGOOSE WARNING !!!
#
#   This is an UNSTABLE release of Mongoose.
#   Unstable releases are available for preview/testing only.
#   DO NOT run this in production.
#
##############################################################
 
Error: Cannot find module 'knox'
    at Function.Module._resolveFilename (module.js:338:15)
    at Function.Module._load (module.js:280:25)
    at Module.require (module.js:364:17)
    at require (module.js:380:17)
    at Object.<anonymous> (/Users/bakorer/works/myapp/routes/admin/content.js:5:12)
    at Module._compile (module.js:456:26)
    at Module._extensions..js (module.js:474:10)
    at Object.nodeDevHook [as .js] (/Users/bakorer/.nvm/v0.10.28/lib/node_modules/node-dev/lib/hook.js:43:7)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
[ERROR] 10:45:41 Error

package.json に “mongoose”: “^3.8.13”, と記述されていたので npm install したら Mongoose version 3.9.7 がインストールされておりました。

$ npm list | grep mongoose
├─┬ mongoose@3.9.7 invalid
├─┬ mongoose-file@0.0.2
├─┬ mongoose-paginate@3.1.5
│ └─┬ mongoose@4.0.6
├── mongoose-query-paginate@1.0.1
npm ERR! invalid: mongoose@3.9.7 /Users/bakorer/myapp/node_modules/mongoose
npm ERR! not ok code 0

Mongoose の version を確認して使えそうな stable version に package.json を書き換えて、再度 npm install すれば OK なはず。

$ git diff
 
diff --git a/package.json b/package.json
index e56cf62..6b49881 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
-    "mongoose": "^3.8.13",
+    "mongoose": "3.8.13",

package.json の書き方は下記のサイトが詳しいので、あわせて読んでおくといいかもです。


参考情報