app/factories/model.js

/**
 * @module
 */

var _ = require('lodash');
var mongoose = require('app/lib/mongoose');
var mongooseAutopopulate = require('mongoose-autopopulate');
var pluralize = require('pluralize');
var unpopulatedProperties = require('app/lib/unpopulatedProperties');

module.exports = {
  /**
   * Return new Mongoose model
   * @param {string} name - Name of model (in camel case with uppercase first letter e.g. "BlogPost")
   * @param {Object} properties - Mongoose model properties
   * @param {Object} statics - Static methods and properties for new model
   * @param {Object} methods - Instance methods for new model
   * @param {function} schemaMods - Function that receives Schema as parameter and makes modifications to it (e.g. addition of middleware) before model creation
   * @returns {Object} Mongoose model
   */
  new: function(name, properties, statics, methods, schemaMods) {
    // Add default definition to properties with references and load reference schemas
    Object.keys(properties).forEach(function(key) {
      var modifiedProperty = (property) => {
        if (property.ref) {
          property.autopopulate = true;
          property.type = mongoose.Schema.Types.ObjectId;
        }

        return property;
      };

      if (Array.isArray(properties[key]) && properties[key].length === 1) {
        properties[key][0] = modifiedProperty(properties[key][0]);
      } else {
        properties[key] = modifiedProperty(properties[key]);
      }
    });

    var Schema = mongoose.Schema(properties, {
      timestamps: true
    });

    Schema.set('toObject', { getters: true });
    Schema.options.toObject.transform = mongoose.transform;

    Schema.statics.findOrCreate = function(properties, done) {
      properties = unpopulatedProperties(properties);

      this.findOne(properties, (error, document) => {
        if (error) {
          return done(error);
        }

        if (document) {
          done(undefined, document);
        } else {
          this.create(properties, (error, document) => {
            if (error) { return done(error); }

            this.findOne({
              _id: document.id
            }, (error, document) => {
              done(error, document);
            });
          });
        }
      });
    };

    /**
     * Returns pluralized model name in camel case with lowercase first letter (e.g. "blogPosts")
     * @returns {string} Model type
     */
    Schema.statics.modelType = Schema.methods.modelType = function() {
      var name = this.modelName ? this.modelName : this.constructor.modelName;

      if (name) {
        return pluralize(_.lowerFirst(name));
      }
    };

    /**
     * Returns singular model name in camel case with lowercase first letter (e.g. "blogPost")
     * @returns {string} Model ID
     */
    Schema.statics.modelId = Schema.methods.modelId = function() {
      var name = this.modelName ? this.modelName : this.constructor.modelName;

      if (name) {
        return _.lowerFirst(name);
      }
    };

    var key;

    if (statics) {
      for (key in statics) {
        Schema.statics[key] = statics[key];
      }
    }

    if (methods) {
      for (key in methods) {
        Schema.methods[key] = methods[key];
      }
    }

    Schema.pre('save', function(next) {
      this.wasNew = this.isNew;
      next();
    });

    if (schemaMods) {
      schemaMods(Schema);
    }

    Schema.plugin(mongooseAutopopulate);

    return mongoose.model(name, Schema);
  }
};