UNPKG

iridium

Version:

A custom lightweight ORM for MongoDB designed for power-users

541 lines (539 loc) 22.2 kB
var MongoDB = require('mongodb'); var Bluebird = require('bluebird'); var _ = require('lodash'); var Core_1 = require('./Core'); var Instance_1 = require('./Instance'); var Cursor_1 = require('./Cursor'); var ModelCache_1 = require('./ModelCache'); var ModelHelpers_1 = require('./ModelHelpers'); var ModelHandlers_1 = require('./ModelHandlers'); var ModelSpecificInstance_1 = require('./ModelSpecificInstance'); /** * An Iridium Model which represents a structured MongoDB collection. * Models expose the methods you will generally use to query those collections, and ensure that * the results of those queries are returned as {TInstance} instances. * * @param TDocument The interface used to determine the schema of documents in the collection. * @param TInstance The interface or class used to represent collection documents in the JS world. * * @class */ var Model = (function () { /** * Creates a new Iridium model representing a given ISchema and backed by a collection whose name is specified * @param core The Iridium core that this model should use for database access * @param instanceType The class which will be instantiated for each document retrieved from the database * @constructor */ function Model(core, instanceType) { this._hooks = {}; if (!(core instanceof Core_1.Core)) throw new Error("You failed to provide a valid Iridium core for this model"); if (typeof instanceType != 'function') throw new Error("You failed to provide a valid instance constructor for this model"); if (typeof instanceType.collection != 'string' || !instanceType.collection) throw new Error("You failed to provide a valid collection name for this model"); if (!_.isPlainObject(instanceType.schema) || instanceType.schema._id === undefined) throw new Error("You failed to provide a valid schema for this model"); this._core = core; this.loadExternal(instanceType); this.onNewModel(); this.loadInternal(); } /** * Loads any externally available properties (generally accessed using public getters/setters). */ Model.prototype.loadExternal = function (instanceType) { this._collection = instanceType.collection; this._schema = instanceType.schema; this._hooks = instanceType; this._cacheDirector = instanceType.cache; this._transforms = instanceType.transforms || {}; this._validators = instanceType.validators || []; this._indexes = instanceType.indexes || []; if (!this._schema._id) this._schema._id = MongoDB.ObjectID; if (this._schema._id === MongoDB.ObjectID && !this._transforms['_id']) this._transforms['_id'] = { fromDB: function (value) { return value._bsontype == 'ObjectID' ? new MongoDB.ObjectID(value.id).toHexString() : value; }, toDB: function (value) { return value && typeof value === 'string' ? new MongoDB.ObjectID(value) : value; } }; if (instanceType.prototype instanceof Instance_1.Instance) this._Instance = ModelSpecificInstance_1.ModelSpecificInstance(this, instanceType); else this._Instance = instanceType.bind(undefined, this); }; /** * Loads any internally (protected/private) properties and helpers only used within Iridium itself. */ Model.prototype.loadInternal = function () { this._cache = new ModelCache_1.ModelCache(this); this._helpers = new ModelHelpers_1.ModelHelpers(this); this._handlers = new ModelHandlers_1.ModelHandlers(this); }; /** * Process any callbacks and plugin delegation for the creation of this model. * It will generally be called whenever a new Iridium Core is created, however is * more specifically tied to the lifespan of the models themselves. */ Model.prototype.onNewModel = function () { var _this = this; this._core.plugins.forEach(function (plugin) { return plugin.newModel && plugin.newModel(_this); }); }; Object.defineProperty(Model.prototype, "helpers", { /** * Provides helper methods used by Iridium for common tasks * @returns A set of helper methods which are used within Iridium for common tasks */ get: function () { return this._helpers; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "handlers", { /** * Provides helper methods used by Iridium for hook delegation and common processes * @returns A set of helper methods which perform common event and response handling tasks within Iridium. */ get: function () { return this._handlers; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "hooks", { /** * Gets the even hooks subscribed on this model for a number of different state changes. * These hooks are primarily intended to allow lifecycle manipulation logic to be added * in the user's model definition, allowing tasks such as the setting of default values * or automatic client-side joins to take place. */ get: function () { return this._hooks; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "schema", { /** * Gets the schema dictating the data structure represented by this model. * The schema is used by skmatc to validate documents before saving to the database, however * until MongoDB 3.1 becomes widely available (with server side validation support) we are * limited in our ability to validate certain types of updates. As such, these validations * act more as a data-integrity check than anything else, unless you purely make use of Omnom * updates within instances. * @public * @returns The defined validation schema for this model */ get: function () { return this._schema; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "core", { /** * Gets the Iridium core that this model is associated with. * @public * @returns The Iridium core that this model is bound to */ get: function () { return this._core; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "collection", { /** * Gets the underlying MongoDB collection from which this model's documents are retrieved. * You can make use of this object if you require any low level access to the MongoDB collection, * however we recommend you make use of the Iridium methods whereever possible, as we cannot * guarantee the accuracy of the type definitions for the underlying MongoDB driver. * @public * @returns {Collection} */ get: function () { if (!this.core.connection) throw new Error("Iridium Core not connected to a database."); return this.core.connection.collection(this._collection); }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "collectionName", { /** * Gets the name of the underlying MongoDB collection from which this model's documents are retrieved * @public */ get: function () { return this._collection; }, /** * Sets the name of the underlying MongoDB collection from which this model's documents are retrieved * @public */ set: function (value) { this._collection = value; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "cacheDirector", { /** * Gets the cache controller which dictates which queries will be cached, and under which key * @public * @returns {CacheDirector} */ get: function () { return this._cacheDirector; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "cache", { /** * Gets the cache responsible for storing objects for quick retrieval under certain conditions * @public * @returns {ModelCache} */ get: function () { return this._cache; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "Instance", { /** * Gets the constructor responsible for creating instances for this model */ get: function () { return this._Instance; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "transforms", { /** * Gets the transforms which are applied whenever a document is received from the database, or * prior to storing a document in the database. Tasks such as converting an ObjectID to a string * and vice versa are all listed in this object. */ get: function () { return this._transforms; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "validators", { /** * Gets the custom validation types available for this model. These validators are added to the * default skmatc validators, as well as those available through plugins, for use when checking * your instances. */ get: function () { return this._validators; }, enumerable: true, configurable: true }); Object.defineProperty(Model.prototype, "indexes", { /** * Gets the indexes which Iridium will manage on this model's database collection. */ get: function () { return this._indexes; }, enumerable: true, configurable: true }); Model.prototype.find = function (conditions, fields) { conditions = conditions || {}; fields = fields || {}; if (!_.isPlainObject(conditions)) conditions = { _id: conditions }; conditions = this._helpers.convertToDB(conditions); var cursor = this.collection.find(conditions, { fields: fields }); return new Cursor_1.Cursor(this, conditions, cursor); }; Model.prototype.get = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i - 0] = arguments[_i]; } return this.findOne.apply(this, args); }; Model.prototype.findOne = function () { var _this = this; var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i - 0] = arguments[_i]; } var conditions = null; var options = null; var callback = null; for (var argI = 0; argI < args.length; argI++) { if (typeof args[argI] == 'function') callback = callback || args[argI]; else if (_.isPlainObject(args[argI])) { if (conditions) options = args[argI]; else conditions = args[argI]; } else conditions = { _id: args[argI] }; } conditions = conditions || {}; options = options || {}; _.defaults(options, { cache: true }); return Bluebird.resolve().bind(this).then(function () { conditions = _this._helpers.convertToDB(conditions); return _this._cache.get(conditions); }).then(function (cachedDocument) { if (cachedDocument) return cachedDocument; return new Bluebird(function (resolve, reject) { _this.collection.findOne(conditions, { fields: options.fields, skip: options.skip, sort: options.sort, limit: options.limit }, function (err, result) { if (err) return reject(err); return resolve(result); }); }); }).then(function (document) { if (!document) return null; return _this._handlers.documentReceived(conditions, document, function (document, isNew, isPartial) { return _this._helpers.wrapDocument(document, isNew, isPartial); }, options); }).nodeify(callback); }; Model.prototype.create = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i - 0] = arguments[_i]; } return this.insert.apply(this, args); }; Model.prototype.insert = function (objs) { var _this = this; var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } var objects; var options = {}; var callback = null; if (typeof args[0] == 'function') callback = args[0]; else { options = args[0]; callback = args[1]; } if (Array.isArray(objs)) objects = objs; else objects = [objs]; options = options || {}; _.defaults(options, { w: 'majority', forceServerObjectId: true }); return Bluebird.resolve().then(function () { var queryOptions = { w: options.w, upsert: options.upsert, new: true }; if (options.upsert) { var docs = _this._handlers.creatingDocuments(objects); return docs.map(function (object) { return new Bluebird(function (resolve, reject) { _this.collection.findAndModify({ _id: object._id }, ["_id"], object, queryOptions, function (err, result) { if (err) return reject(err); return resolve(result); }); }); }); } else return _this._handlers.creatingDocuments(objects).then(function (objects) { return _.chunk(objects, 1000); }).map(function (objects) { return new Bluebird(function (resolve, reject) { _this.collection.insertMany(objects, queryOptions, function (err, result) { if (err) return reject(err); return resolve(result.ops); }); }); }).then(function (results) { return _.flatten(results); }); }).map(function (inserted) { return _this._handlers.documentReceived(null, inserted, function (document, isNew, isPartial) { return _this._helpers.wrapDocument(document, isNew, isPartial); }, { cache: options.cache }); }).then(function (results) { if (Array.isArray(objs)) return results; return results[0]; }).nodeify(callback); }; Model.prototype.update = function (conditions, changes, options, callback) { var _this = this; if (typeof options == 'function') { callback = options; options = {}; } options = options || {}; if (!_.isPlainObject(conditions)) conditions = { _id: conditions }; _.defaults(options, { w: 'majority', multi: true }); return Bluebird.resolve().then(function () { conditions = _this._helpers.convertToDB(conditions); return new Bluebird(function (resolve, reject) { _this.collection.updateMany(conditions, changes, options, function (err, response) { if (err) return reject(err); // New MongoDB 2.6+ response type if (response.result && response.result.nModified !== undefined) return resolve(response.result.nModified); // Legacy response type return resolve(response.result.n); }); }); }).nodeify(callback); }; Model.prototype.count = function (conds, callback) { var _this = this; var conditions = conds; if (typeof conds == 'function') { callback = conds; conditions = {}; } conditions = conditions || {}; if (!_.isPlainObject(conditions)) conditions = { _id: conditions }; return Bluebird.resolve().then(function () { conditions = _this._helpers.convertToDB(conditions); return new Bluebird(function (resolve, reject) { _this.collection.count(conditions, function (err, results) { if (err) return reject(err); return resolve(results); }); }); }).nodeify(callback); }; Model.prototype.remove = function (conds, options, callback) { var _this = this; var conditions = conds; if (typeof options === 'function') { callback = options; options = {}; } if (typeof conds == 'function') { callback = conds; options = {}; conditions = {}; } conditions = conditions || {}; options = options || {}; _.defaults(options, { w: 'majority' }); if (!_.isPlainObject(conditions)) conditions = { _id: conditions }; return Bluebird.resolve().then(function () { conditions = _this._helpers.convertToDB(conditions); return new Bluebird(function (resolve, reject) { _this.collection.remove(conditions, options, function (err, response) { if (err) return reject(err); return resolve(response.result.n); }); }); }).then(function (count) { if (count === 1) _this._cache.clear(conditions); return Bluebird.resolve(count); }).nodeify(callback); }; Model.prototype.aggregate = function (pipeline) { var _this = this; return new Bluebird(function (resolve, reject) { _this.collection.aggregate(pipeline, function (err, results) { if (err) return reject(err); return resolve(results); }); }); }; Model.prototype.ensureIndex = function (specification, options, callback) { var _this = this; if (typeof options == 'function') { callback = options; options = {}; } return new Bluebird(function (resolve, reject) { _this.collection.ensureIndex(specification, options, function (err, name) { if (err) return reject(err); return resolve(name); }); }).nodeify(callback); }; /** * Ensures that all indexes defined in the model's options are created * @param {function(Error, String[])} callback A callback which is triggered when the operation completes * @returns {Promise<String[]>} The names of the indexes */ Model.prototype.ensureIndexes = function (callback) { var _this = this; return Bluebird.resolve(this._indexes).map(function (index) { return _this.ensureIndex(index.spec || index, index.options || {}); }).nodeify(callback); }; Model.prototype.dropIndex = function (specification, callback) { var _this = this; var index; if (typeof (specification) === 'string') index = specification; else { index = _(specification).map(function (direction, key) { return key + '_' + direction; }).reduce(function (x, y) { return x + '_' + y; }); } return new Bluebird(function (resolve, reject) { _this.collection.dropIndex(index, function (err, result) { if (err) return reject(err); return resolve(!!result.ok); }); }).nodeify(callback); }; /** * Removes all indexes (except for _id) from the collection * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes * @returns {Promise<Boolean>} Whether the indexes were dropped */ Model.prototype.dropIndexes = function (callback) { var _this = this; return new Bluebird(function (resolve, reject) { _this.collection.dropAllIndexes(function (err, count) { if (err) return reject(err); return resolve(count); }); }).nodeify(callback); }; return Model; })(); exports.Model = Model; //# sourceMappingURL=Model.js.map