src/models/hooks.js

/**
 * 一些常用的Mongoose钩子(中间件)函数。具体可以参见[Mongoose Middleware](http://mongoosejs.com/docs/middleware.html)。
 *
 * @module models/hook
 */
const fs = require('fs');
const path = require('path');
const logger = require('winston');

/**
 * 向schema对象添加一个自动初始化新文档对象某字段为当前时间的钩子。这个钩子对于`save`、`update`
 * 和`findOneAndUpdate`操作有效。注意:该钩子对`insertMany`无效。
 *
 * @param schema {mongoose.Schema} schema对象
 * @param field {string} 可选,字段名称,默认为`createdAt`
 */
function addCreatedAt(schema, field) {
  if (!field)
    field = 'createdAt';
  schema.pre('save', function (next) {
    if (this.isNew)
      this[field] = new Date();
    next();
  });
  schema.pre('findOneAndUpdate', function () {
    const update = this.getUpdate();
    update['$setOnInsert'] = update['$setOnInsert'] || {};
    update['$setOnInsert'][field] = new Date();
  });
  schema.pre('update', function () {
    const update = this.getUpdate();
    update[field] = new Date();
    update['$setOnInsert'] = update['$setOnInsert'] || {};
    update['$setOnInsert'][field] = new Date();
  });
}

/**
 * 向schema对象添加一个在文档更新时,把某字段设为当前时间的钩子。这个钩子对于`save`、`update`
 * 和`findOneAndUpdate`操作有效。注意:该钩子对`insertMany`无效。
 *
 * @param schema {mongoose.Schema} schema对象
 * @param field {string} 可选,字段名称,默认为`updatedAt`
 */
function addUpdatedAt(schema, field) {
  if (!field)
    field = 'updatedAt';
  schema.pre('save', function (next) {
    if (this.isModified())
      this[field] = new Date();
    next();
  });
  schema.pre('findOneAndUpdate', function () {
    const update = this.getUpdate();
    update[field] = new Date();
  });
  schema.pre('update', function () {
    const update = this.getUpdate();
    update[field] = new Date();
  });
}

/**
 * 向schema对象添加一个软删除的钩子。这个钩子对于`save`、`update`和`findOneAndUpdate`操作有效。
 * 注意:该钩子对`insertMany`无效。此外添加了`deleted`和`notDeleted`的query helper,并添加了
 * delete方法。
 *
 * @param schema {mongoose.Schema} schema对象
 * @param field {string} 可选,字段名称,默认为`deleted`
 */
function addDeleted(schema, field) {
  if (!field)
    field = 'deleted';
  schema.pre('save', function (next) {
    if (this.isNew && !this[field])
      this[field] = false;
    next();
  });
  schema.pre('findOneAndUpdate', function () {
    const update = this.getUpdate();
    update['$setOnInsert'] = update['$setOnInsert'] || {};
    if (!update['$setOnInsert'][field])
      update['$setOnInsert'][field] = false;
  });
  schema.pre('update', function () {
    const update = this.getUpdate();
    update['$setOnInsert'] = update['$setOnInsert'] || {};
    if (!update['$setOnInsert'][field])
      update['$setOnInsert'][field] = false;
  });
  schema.query.deleted = function (deleted) {
    return this.where(field).eq(true);
  };
  schema.query.notDeleted = function () {
    return this.where(field).ne(true);
  };
  schema.methods.delete = function () {
    this[field] = true;
    return this.save();
  };
}

/**
 * 向schema对象添加一个在文档某些对应于文件的字段发生变化时,自动删除旧文件的钩子。该钩子仅对于
 * `save`和`remove`操作有效。
 *
 * @param schema {mongoose.Schema} schema对象
 * @param fields {Array<string>} 字段集合
 * @param uploadDir {string} 可选,上传的目录,默认为当前目录
 */
function addFileFields(schema, fields, uploadDir) {
  if (fields.length === 0)
    return;
  if (uploadDir === undefined)
    uploadDir = '';
  function errLogger(filename) {
    return err => {
      if (err) {
        logger.error(`Failed to delete file "${filename}".`);
        logger.error(err);
      }
    };
  }
  schema.post('init', doc => {
    for (let field of fields)
      doc['_' + field] = doc[field];
  });
  schema.post('save', doc => {
    for (let field of fields) {
      const oldFilename = doc['_' + field];
      if (oldFilename && oldFilename !== doc[field])
        fs.unlink(path.join(uploadDir, oldFilename), errLogger(oldFilename));
    }
  });

  schema.post('remove', doc => {
    for (let field of fields) {
      const oldFilename = doc['_' + field];
      if (oldFilename)
        fs.unlink(path.join(uploadDir, oldFilename), errLogger(oldFilename));
    }
  });
}

module.exports = {
  addCreatedAt,
  addUpdatedAt,
  addDeleted,
  addFileFields
};