| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005 |
- 'use strict';
- var EventEmitter = require('events').EventEmitter;
- var _ = require('lodash');
- var Promise = require('bluebird');
- var util = require('./util');
- var Document = require('./document');
- var Query = require('./query');
- var Schema = require('./schema');
- var Types = require('./types');
- var WarehouseError = require('./error');
- var PopulationError = require('./error/population');
- var Mutex = require('./mutex');
- var parseArgs = util.parseArgs;
- var reverse = util.reverse;
- var shuffle = _.shuffle;
- var getProp = util.getProp;
- var setGetter = util.setGetter;
- var extend = _.assign;
- var isArray = Array.isArray;
- /**
- * Model constructor.
- *
- * @class
- * @param {string} name Model name
- * @param {Schema|object} [schema] Schema
- * @extends EventEmitter
- */
- function Model(name, schema_) {
- EventEmitter.call(this);
- var schema, i, len, key;
- // Define schema
- if (schema_ instanceof Schema) {
- schema = schema_;
- } else if (typeof schema_ === 'object') {
- schema = new Schema(schema_);
- } else {
- schema = new Schema();
- }
- // Set `_id` path for schema
- if (!schema.path('_id')) {
- schema.path('_id', {type: Types.CUID, required: true});
- }
- this.name = name;
- this.data = {};
- this._mutex = new Mutex();
- this.schema = schema;
- this.length = 0;
- var _Document = this.Document = function(data) {
- Document.call(this, data);
- // Apply getters
- schema._applyGetters(this);
- };
- util.inherits(_Document, Document);
- _Document.prototype._model = this;
- _Document.prototype._schema = schema;
- var _Query = this.Query = function(data) {
- Query.call(this, data);
- };
- util.inherits(_Query, Query);
- _Query.prototype._model = this;
- _Query.prototype._schema = schema;
- // Apply static methods
- var statics = schema.statics;
- var staticKeys = Object.keys(statics);
- for (i = 0, len = staticKeys.length; i < len; i++) {
- key = staticKeys[i];
- this[key] = statics[key];
- }
- // Apply instance methods
- var methods = schema.methods;
- var methodKeys = Object.keys(methods);
- for (i = 0, len = methodKeys.length; i < len; i++) {
- key = methodKeys[i];
- _Document.prototype[key] = methods[key];
- }
- }
- util.inherits(Model, EventEmitter);
- /**
- * Creates a new document.
- *
- * @param {object} data
- * @return {Document}
- */
- Model.prototype.new = function(data) {
- return new this.Document(data);
- };
- /**
- * Finds a document by its identifier.
- *
- * @param {*} id
- * @param {object} options
- * @param {boolean} [options.lean=false] Returns a plain JavaScript object
- * @return {Document|object}
- */
- Model.prototype.findById = function(id, options_) {
- var raw = this.data[id];
- if (!raw) return;
- var options = extend({
- lean: false
- }, options_);
- var data = _.cloneDeep(raw);
- return options.lean ? data : this.new(data);
- };
- Model.prototype.get = Model.prototype.findById;
- /**
- * Checks if the model contains a document with the specified id.
- *
- * @param {*} id
- * @return {boolean}
- */
- Model.prototype.has = function(id) {
- return Boolean(this.data[id]);
- };
- function execHooks(schema, type, event, data) {
- var hooks = schema.hooks[type][event];
- if (!hooks.length) return Promise.resolve(data);
- return Promise.each(hooks, function(hook) {
- return hook(data);
- }).thenReturn(data);
- }
- /**
- * Acquires write lock.
- *
- * @param {*} id
- * @return {Promise}
- * @private
- */
- Model.prototype._acquireWriteLock = function(id) {
- var mutex = this._mutex;
- return new Promise(function(resolve, reject) {
- mutex.lock(resolve);
- }).disposer(function() {
- mutex.unlock();
- });
- };
- /**
- * Inserts a document.
- *
- * @param {Document|object} data
- * @return {Promise}
- * @private
- */
- Model.prototype._insertOne = function(data_) {
- var self = this;
- var schema = this.schema;
- // Apply getters
- var data = data_ instanceof self.Document ? data_ : self.new(data_);
- var id = data._id;
- // Check ID
- if (!id) {
- return Promise.reject(new WarehouseError('ID is not defined', WarehouseError.ID_UNDEFINED));
- }
- if (this.has(id)) {
- return Promise.reject(new WarehouseError('ID `' + id + '` has been used', WarehouseError.ID_EXIST));
- }
- // Apply setters
- var result = data.toObject();
- schema._applySetters(result);
- // Pre-hooks
- return execHooks(schema, 'pre', 'save', data).then(function(data) {
- // Insert data
- self.data[id] = result;
- self.length++;
- self.emit('insert', data);
- return execHooks(schema, 'post', 'save', data);
- });
- };
- /**
- * Inserts a document.
- *
- * @param {object} data
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.insertOne = function(data, callback) {
- var self = this;
- return Promise.using(this._acquireWriteLock(), function() {
- return self._insertOne(data);
- }).asCallback(callback);
- };
- /**
- * Inserts documents.
- *
- * @param {object|array} data
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.insert = function(data, callback) {
- if (isArray(data)) {
- var self = this;
- return Promise.mapSeries(data, function(item) {
- return self.insertOne(item);
- }).asCallback(callback);
- }
- return this.insertOne(data, callback);
- };
- /**
- * Inserts the document if it does not exist; otherwise updates it.
- *
- * @param {object} data
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.save = function(data, callback) {
- var id = data._id;
- var self = this;
- if (!id) return this.insertOne(data, callback);
- return Promise.using(this._acquireWriteLock(), function() {
- if (self.has(id)) {
- return self._replaceById(id, data);
- }
- return self._insertOne(data);
- }).asCallback(callback);
- };
- /**
- * Updates a document with a compiled stack.
- *
- * @param {*} id
- * @param {array} stack
- * @return {Promise}
- * @private
- */
- Model.prototype._updateWithStack = function(id, stack) {
- var self = this;
- var schema = self.schema;
- var data = self.data[id];
- if (!data) {
- return Promise.reject(new WarehouseError('ID `' + id + '` does not exist', WarehouseError.ID_NOT_EXIST));
- }
- // Clone data
- var result = _.cloneDeep(data);
- // Update
- for (var i = 0, len = stack.length; i < len; i++) {
- stack[i](result);
- }
- // Apply getters
- var doc = self.new(result);
- // Apply setters
- result = doc.toObject();
- schema._applySetters(result);
- // Pre-hooks
- return execHooks(schema, 'pre', 'save', doc).then(function(data) {
- // Update data
- self.data[id] = result;
- self.emit('update', data);
- return execHooks(schema, 'post', 'save', data);
- });
- };
- /**
- * Finds a document by its identifier and update it.
- *
- * @param {*} id
- * @param {object} update
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.updateById = function(id, update, callback) {
- var self = this;
- return Promise.using(this._acquireWriteLock(), function() {
- var stack = self.schema._parseUpdate(update);
- return self._updateWithStack(id, stack);
- }).asCallback(callback);
- };
- /**
- * Updates matching documents.
- *
- * @param {object} query
- * @param {object} data
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.update = function(query, data, callback) {
- return this.find(query).update(data, callback);
- };
- /**
- * Finds a document by its identifier and replace it.
- *
- * @param {*} id
- * @param {object} data
- * @return {Promise}
- * @private
- */
- Model.prototype._replaceById = function(id, data_) {
- var self = this;
- var schema = this.schema;
- if (!this.has(id)) {
- return Promise.reject(new WarehouseError('ID `' + id + '` does not exist', WarehouseError.ID_NOT_EXIST));
- }
- data_._id = id;
- // Apply getters
- var data = data instanceof self.Document ? data_ : self.new(data_);
- // Apply setters
- var result = data.toObject();
- schema._applySetters(result);
- // Pre-hooks
- return execHooks(schema, 'pre', 'save', data).then(function(data) {
- // Replace data
- self.data[id] = result;
- self.emit('update', data);
- return execHooks(schema, 'post', 'save', data);
- });
- };
- /**
- * Finds a document by its identifier and replace it.
- *
- * @param {*} id
- * @param {object} data
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.replaceById = function(id, data, callback) {
- var self = this;
- return Promise.using(this._acquireWriteLock(), function() {
- return self._replaceById(id, data);
- }).asCallback(callback);
- };
- /**
- * Replaces matching documents.
- *
- * @param {object} query
- * @param {object} data
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.replace = function(query, data, callback) {
- return this.find(query).replace(data, callback);
- };
- /**
- * Finds a document by its identifier and remove it.
- *
- * @param {*} id
- * @param {function} [callback]
- * @return {Promise}
- * @private
- */
- Model.prototype._removeById = function(id) {
- var self = this;
- var schema = this.schema;
- var data = this.data[id];
- if (!data) {
- return Promise.reject(new WarehouseError('ID `' + id + '` does not exist', WarehouseError.ID_NOT_EXIST));
- }
- // Pre-hooks
- return execHooks(schema, 'pre', 'remove', data).then(function(data) {
- // Remove data
- self.data[id] = null;
- self.length--;
- self.emit('remove', data);
- return execHooks(schema, 'post', 'remove', data);
- });
- };
- /**
- * Finds a document by its identifier and remove it.
- *
- * @param {*} id
- * @param {function} [callback]
- * @return {Promise}
- */
- Model.prototype.removeById = function(id, callback) {
- var self = this;
- return Promise.using(this._acquireWriteLock(), function() {
- return self._removeById(id);
- }).asCallback(callback);
- };
- /**
- * Removes matching documents.
- *
- * @param {object} query
- * @param {object} [callback]
- * @return {Promise}
- */
- Model.prototype.remove = function(query, callback) {
- return this.find(query).remove(callback);
- };
- /**
- * Deletes a model.
- */
- Model.prototype.destroy = function() {
- this._database._models[this.name] = null;
- };
- /**
- * Returns the number of elements.
- *
- * @return {number}
- */
- Model.prototype.count = function() {
- return this.length;
- };
- Model.prototype.size = Model.prototype.count;
- /**
- * Iterates over all documents.
- *
- * @param {function} iterator
- * @param {object} [options] See {@link Model#findById}.
- */
- Model.prototype.forEach = function(iterator, options) {
- var keys = Object.keys(this.data);
- var num = 0;
- var data;
- for (var i = 0, len = keys.length; i < len; i++) {
- data = this.findById(keys[i], options);
- if (data) iterator(data, num++);
- }
- };
- Model.prototype.each = Model.prototype.forEach;
- /**
- * Returns an array containing all documents.
- *
- * @param {Object} [options] See {@link Model#findById}.
- * @return {Array}
- */
- Model.prototype.toArray = function(options) {
- var result = new Array(this.length);
- this.forEach(function(item, i) {
- result[i] = item;
- }, options);
- return result;
- };
- /**
- * Finds matching documents.
- *
- * @param {Object} query
- * @param {Object} [options]
- * @param {Number} [options.limit=0] Limits the number of documents returned.
- * @param {Number} [options.skip=0] Skips the first elements.
- * @param {Boolean} [options.lean=false] Returns a plain JavaScript object.
- * @return {Query|Array}
- */
- Model.prototype.find = function(query, options_) {
- var options = options_ || {};
- var filter = this.schema._execQuery(query);
- var keys = Object.keys(this.data);
- var len = keys.length;
- var limit = options.limit || this.length;
- var skip = options.skip;
- var data = this.data;
- var arr = [];
- var key, item;
- for (var i = 0; limit && i < len; i++) {
- key = keys[i];
- item = data[key];
- if (item && filter(item)) {
- if (skip) {
- skip--;
- } else {
- arr.push(this.findById(key, options));
- limit--;
- }
- }
- }
- return options.lean ? arr : new this.Query(arr);
- };
- /**
- * Finds the first matching documents.
- *
- * @param {Object} query
- * @param {Object} [options]
- * @param {Number} [options.skip=0] Skips the first elements.
- * @param {Boolean} [options.lean=false] Returns a plain JavaScript object.
- * @return {Document|Object}
- */
- Model.prototype.findOne = function(query, options_) {
- var options = options_ || {};
- options.limit = 1;
- var result = this.find(query, options);
- return options.lean ? result[0] : result.data[0];
- };
- /**
- * Sorts documents. See {@link Query#sort}.
- *
- * @param {String|Object} orderby
- * @param {String|Number} [order]
- * @return {Query}
- */
- Model.prototype.sort = function(orderby, order) {
- var sort = parseArgs(orderby, order);
- var fn = this.schema._execSort(sort);
- return new this.Query(this.toArray().sort(fn));
- };
- /**
- * Returns the document at the specified index. `num` can be a positive or
- * negative number.
- *
- * @param {Number} i
- * @param {Object} [options] See {@link Model#findById}.
- * @return {Document|Object}
- */
- Model.prototype.eq = function(i_, options) {
- var index = i_ < 0 ? this.length + i_ : i_;
- var data = this.data;
- var keys = Object.keys(data);
- var key, item;
- for (var i = 0, len = keys.length; i < len; i++) {
- key = keys[i];
- item = data[key];
- if (!item) continue;
- if (index) {
- index--;
- } else {
- return this.findById(key, options);
- }
- }
- };
- /**
- * Returns the first document.
- *
- * @param {Object} [options] See {@link Model#findById}.
- * @return {Document|Object}
- */
- Model.prototype.first = function(options) {
- return this.eq(0, options);
- };
- /**
- * Returns the last document.
- *
- * @param {Object} [options] See {@link Model#findById}.
- * @return {Document|Object}
- */
- Model.prototype.last = function(options) {
- return this.eq(-1, options);
- };
- /**
- * Returns the specified range of documents.
- *
- * @param {Number} start
- * @param {Number} [end]
- * @return {Query}
- */
- Model.prototype.slice = function(start_, end_) {
- var total = this.length;
- var start = start_ | 0;
- if (start < 0) start += total;
- if (start > total - 1) return new this.Query([]);
- var end = end_ | 0 || total;
- if (end < 0) end += total;
- var len = start > end ? 0 : end - start;
- if (len > total) len = total - start;
- if (!len) return new this.Query([]);
- var arr = new Array(len);
- var keys = Object.keys(this.data);
- var keysLen = keys.length;
- var num = 0;
- var data;
- for (var i = 0; num < len && i < keysLen; i++) {
- data = this.findById(keys[i]);
- if (!data) continue;
- if (start) {
- start--;
- } else {
- arr[num++] = data;
- }
- }
- return new this.Query(arr);
- };
- /**
- * Limits the number of documents returned.
- *
- * @param {Number} i
- * @return {Query}
- */
- Model.prototype.limit = function(i) {
- return this.slice(0, i);
- };
- /**
- * Specifies the number of items to skip.
- *
- * @param {Number} i
- * @return {Query}
- */
- Model.prototype.skip = function(i) {
- return this.slice(i);
- };
- /**
- * Returns documents in a reversed order.
- *
- * @return {Query}
- */
- Model.prototype.reverse = function() {
- return new this.Query(reverse(this.toArray()));
- };
- /**
- * Returns documents in random order.
- *
- * @return {Query}
- */
- Model.prototype.shuffle = function() {
- return new this.Query(shuffle(this.toArray()));
- };
- Model.prototype.random = Model.prototype.shuffle;
- /**
- * Creates an array of values by iterating each element in the collection.
- *
- * @param {Function} iterator
- * @param {Object} [options]
- * @return {Array}
- */
- Model.prototype.map = function(iterator, options) {
- var result = new Array(this.length);
- this.forEach(function(item, i) {
- result[i] = iterator(item, i);
- }, options);
- return result;
- };
- /**
- * Reduces a collection to a value which is the accumulated result of iterating
- * each element in the collection.
- *
- * @param {Function} iterator
- * @param {*} [initial] By default, the initial value is the first document.
- * @return {*}
- */
- Model.prototype.reduce = function(iterator, initial) {
- var arr = this.toArray();
- var len = this.length;
- var i, result;
- if (initial === undefined) {
- i = 1;
- result = arr[0];
- } else {
- i = 0;
- result = initial;
- }
- for (; i < len; i++) {
- result = iterator(result, arr[i], i);
- }
- return result;
- };
- /**
- * Reduces a collection to a value which is the accumulated result of iterating
- * each element in the collection from right to left.
- *
- * @param {Function} iterator
- * @param {*} [initial] By default, the initial value is the last document.
- * @return {*}
- */
- Model.prototype.reduceRight = function(iterator, initial) {
- var arr = this.toArray();
- var len = this.length;
- var i, result;
- if (initial === undefined) {
- i = len - 2;
- result = arr[len - 1];
- } else {
- i = len - 1;
- result = initial;
- }
- for (; i >= 0; i--) {
- result = iterator(result, arr[i], i);
- }
- return result;
- };
- /**
- * Creates a new array with all documents that pass the test implemented by the
- * provided function.
- *
- * @param {Function} iterator
- * @param {Object} [options]
- * @return {Query}
- */
- Model.prototype.filter = function(iterator, options) {
- var arr = [];
- this.forEach(function(item, i) {
- if (iterator(item, i)) arr.push(item);
- }, options);
- return new this.Query(arr);
- };
- /**
- * Tests whether all documents pass the test implemented by the provided
- * function.
- *
- * @param {Function} iterator
- * @return {Boolean}
- */
- Model.prototype.every = function(iterator) {
- var keys = Object.keys(this.data);
- var len = keys.length;
- var num = 0;
- var data;
- if (!len) return true;
- for (var i = 0; i < len; i++) {
- data = this.findById(keys[i]);
- if (data) {
- if (!iterator(data, num++)) return false;
- }
- }
- return true;
- };
- /**
- * Tests whether some documents pass the test implemented by the provided
- * function.
- *
- * @param {Function} iterator
- * @return {Boolean}
- */
- Model.prototype.some = function(iterator) {
- var keys = Object.keys(this.data);
- var len = keys.length;
- var num = 0;
- var data;
- if (!len) return false;
- for (var i = 0; i < len; i++) {
- data = this.findById(keys[i]);
- if (data) {
- if (iterator(data, num++)) return true;
- }
- }
- return false;
- };
- /**
- * Returns a getter function for normal population.
- *
- * @param {Object} data
- * @param {Model} model
- * @param {Object} options
- * @return {Function}
- * @private
- */
- Model.prototype._populateGetter = function(data, model, options) {
- var hasCache = false;
- var cache;
- return function() {
- if (!hasCache) {
- cache = model.findById(data);
- hasCache = true;
- }
- return cache;
- };
- };
- /**
- * Returns a getter function for array population.
- *
- * @param {Object} data
- * @param {Model} model
- * @param {Object} options
- * @return {Function}
- * @private
- */
- Model.prototype._populateGetterArray = function(data, model, options) {
- var Query = model.Query;
- var hasCache = false;
- var cache;
- return function() {
- if (!hasCache) {
- var arr = [];
- for (var i = 0, len = data.length; i < len; i++) {
- arr.push(model.findById(data[i]));
- }
- if (options.match) {
- cache = new Query(arr).find(options.match, options);
- } else if (options.skip) {
- if (options.limit) {
- arr = arr.slice(options.skip, options.skip + options.limit);
- } else {
- arr = arr.slice(options.skip);
- }
- cache = new Query(arr);
- } else if (options.limit) {
- cache = new Query(arr.slice(0, options.limit));
- } else {
- cache = new Query(arr);
- }
- if (options.sort) {
- cache = cache.sort(options.sort);
- }
- hasCache = true;
- }
- return cache;
- };
- };
- /**
- * Populates document references with a compiled stack.
- *
- * @param {Object} data
- * @param {Array} stack
- * @return {Object}
- * @private
- */
- Model.prototype._populate = function(data, stack) {
- var models = this._database._models;
- var item, model, path, prop;
- for (var i = 0, len = stack.length; i < len; i++) {
- item = stack[i];
- model = models[item.model];
- if (!model) {
- throw new PopulationError('Model `' + item.model + '` does not exist');
- }
- path = item.path;
- prop = getProp(data, path);
- if (isArray(prop)) {
- setGetter(data, path, this._populateGetterArray(prop, model, item));
- } else {
- setGetter(data, path, this._populateGetter(prop, model, item));
- }
- }
- return data;
- };
- /**
- * Populates document references.
- *
- * @param {String|Object} path
- * @return {Query}
- */
- Model.prototype.populate = function(path) {
- if (!path) throw new TypeError('path is required');
- var stack = this.schema._parsePopulate(path);
- var arr = new Array(this.length);
- var self = this;
- this.forEach(function(item, i) {
- arr[i] = self._populate(item, stack);
- });
- return new Query(arr);
- };
- /**
- * Imports data.
- *
- * @param {Array} arr
- * @private
- */
- Model.prototype._import = function(arr) {
- var len = arr.length;
- var data = this.data;
- var schema = this.schema;
- var item;
- for (var i = 0; i < len; i++) {
- item = arr[i];
- data[item._id] = schema._parseDatabase(item);
- }
- this.length = len;
- };
- /**
- * Exports data.
- *
- * @return {String}
- * @private
- */
- Model.prototype._export = function() {
- var arr = new Array(this.length);
- var schema = this.schema;
- this.forEach(function(item, i) {
- arr[i] = schema._exportDatabase(item);
- }, {lean: true});
- return JSON.stringify(arr);
- };
- module.exports = Model;
|