vue-carousel-3d
Version:
Beautiful, flexible and touch supported 3D Carousel for Vue.js
1,006 lines (844 loc) • 21.5 kB
JavaScript
'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;