カテゴリー : Node.js

[Mongoose] timestamp options で createdAt だけ使う

Mongoose の timestamp options で createdAt だけ使う方法を調べたけど、自分で pre hook に実装するしかなかったというお話です。

options.timestamps では createdAt, updatedAt どちらも有効になる

まず、Mongoose version 4.8.5 時点では、ソースコードを読んだ限りでは options.timestamps には createdAt, updatedAt のどちらかだけ使うという option はありませんでした。

以下、Mongoose のソースコードを一部抜粋しました。

/**
 * Setup updatedAt and createdAt timestamps to documents if enabled
 *
 * @param {Boolean|Object} timestamps timestamps options
 * @api private
 */
Schema.prototype.setupTimestamp = function(timestamps) {
  if (timestamps) {
    var createdAt = timestamps.createdAt || 'createdAt';
    var updatedAt = timestamps.updatedAt || 'updatedAt';
// ...
  }
};

createdAt だけ有効にするサンプルコード

というわけで .pre(“save”) .pre(“findOneAndUpdate”) .pre(“update”) とかに options.timestamps 的な hook を追加するサンプルコードをご紹介します。

var yourSchema = new Schema({
  createdAt: {
    type: Date
  }
});
 
yourSchema.pre("save", function(next) {
  var now = new Date;
  if (this.isNew) {
    this.createdAt = now;
  }
  return next();
});
 
yourSchema.pre("findOneAndUpdate", function(next) {
  var now = new Date;
  this.findOneAndUpdate({}, {
    $setOnInsert: {
      createdAt: now
    }
  });
  return next();
});
 
yourSchema.pre("update", function(next) {
  var now = new Date;
  this.update({}, {
    $setOnInsert: {
      createdAt: now
    }
  });
  return next();
});

最後に、小ネタですが timestamp options を削除するという issue があるので確認してみると version 4.11 のマイルストーンで削除されるかもしれません。

[Mongoose] model.save({ validateBeforeSave: false }) で validate をスキップできる

Mongoose 4.2.0 から model.save() のときにバリデーションをスキップするためのオプション validateBeforeSave が追加されています。

validateBeforeSave のテストコード test/document.test.js#L462-L473

validateBeforeSave オプションを利用するサンプルコードですが、mongoose のテストコードが直感的に理解できそうでした。

it('allows you to skip validation on save (gh-2981)', function(done) {
  var db = start();
 
  var MyModel = db.model('gh2981',
      {name: {type: String, required: true}});
 
  var doc = new MyModel();
  doc.save({validateBeforeSave: false}, function(error) {
    assert.ifError(error);
    db.close(done);
  });
});

Mongoose 4.2.0 からしか使えないので古いバージョンを利用しているなら早くアップデートすることをオススメします。

参考情報

[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() しておくことでエラーを退避したいですね。

[Node.js] socket.io ではなくて socketio を npm install して Cannot find module ‘socket.io’ エラーでた

socket.io を使いたくて npm install socketio –save でインストール後に require(‘socket.io’) したら Error: Cannot find module ‘socket.io’ が発生して「なんでだろう…?」って数十分ぐらい悩みました。

$ node index.js
 
module.js:327
    throw err;
    ^
 
Error: Cannot find module 'socket.io'
    at Function.Module._resolveFilename (module.js:325:15)
    at Function.Module._load (module.js:276:25)
    at Module.require (module.js:353:17)
    at require (internal/module.js:12:17)
    at Object.<anonymous> (/Users/bakorer/works/sample/index.js:9:16)
    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)

結果としては socketio が使うべきモジュールではなくて正しくは socket.io というオチでした。package.json を下記のように修正して npm install し直したら無事に require(‘socket.io’) できました。

diff --git a/package.json b/package.json
index 30cdfac..84192a7 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,6 @@
-    "socketio": "^1.0.0"
+    "socket.io": "^1.5.1"
   }
 }

socketio という紛らわしいパッケージ名はつけないで頂きたいですね。

[Node.js] npm package の実行ファイルは node_modules/.bin にシンボリックリンクで存在する

Node.js で npm install した package の実行ファイルは node_modules/.bin ディレクトリ配下にシンボリックリンクが自動作成されます。

npm logo

(例) とある node_modules/.bin 配下

$ ls -l ./node_modules/.bin
lrwxr-xr-x  1 codenote  staff  22 Apr 21 18:11 express -> ../express/bin/express
lrwxr-xr-x  1 codenote  staff  22 Apr 21 18:11 grunt -> ../grunt-cli/bin/grunt
lrwxr-xr-x  1 codenote  staff  16 Apr 21 18:11 jade -> ../jade/bin/jade
lrwxr-xr-x  1 codenote  staff  21 Apr 21 18:12 npm -> ../npm/bin/npm-cli.js

Node.js で開発して4年以上も経つのに知らなくて、自分でもびっくりしました。

ちなみに、モジュールにコマンドが用意されていた場合のみ /path/your_project/node_modules/.bin にシンボリックリンクが作成されます。