UNPKG

warehouse

Version:
812 lines 24.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const rfdc_1 = __importDefault(require("rfdc")); const cloneDeep = (0, rfdc_1.default)(); const bluebird_1 = __importDefault(require("bluebird")); const util_1 = require("./util"); const document_1 = __importDefault(require("./document")); const query_1 = __importDefault(require("./query")); const schema_1 = __importDefault(require("./schema")); const Types = __importStar(require("./types/index")); const error_1 = __importDefault(require("./error")); const population_1 = __importDefault(require("./error/population")); const mutex_1 = __importDefault(require("./mutex")); class Model extends events_1.EventEmitter { /** * Model constructor. * * @param {string} name Model name * @param {Schema|object} [schema_] Schema */ constructor(name, schema_) { super(); this.name = name; this._mutex = new mutex_1.default(); this.data = {}; this.length = 0; this._dataKeys = []; this.dirty = false; let schema; // Define schema if (schema_ instanceof schema_1.default) { schema = schema_; } else if (typeof schema_ === 'object') { schema = new schema_1.default(schema_); } else { schema = new schema_1.default(); } // Set `_id` path for schema if (!schema.path('_id')) { schema.path('_id', { type: Types.CUID, required: true }); } this.schema = schema; class _Document extends document_1.default { constructor(data) { super(data); // Apply getters schema._applyGetters(this); } } this.Document = _Document; _Document.prototype._model = this; _Document.prototype._schema = schema; class _Query extends query_1.default { } this.Query = _Query; _Query.prototype._model = this; _Query.prototype._schema = schema; // Apply static methods Object.assign(this, schema.statics); // Apply instance methods Object.assign(_Document.prototype, schema.methods); } /** * Returns the cached data keys. */ get dataKeys() { if (this.dirty) { this._dataKeys = Object.keys(this.data); this.dirty = false; } return this._dataKeys; } /** * Creates a new document. * * @param {object} data * @return {Document} */ new(data) { return new this.Document(data); } findById(id, options_) { const raw = this.data[id]; if (!raw) return; const options = Object.assign({ lean: false }, options_); const data = cloneDeep(raw); return options.lean ? data : this.new(data); } /** * Checks if the model contains a document with the specified id. * * @param {*} id * @return {boolean} */ has(id) { return Boolean(this.data[id]); } /** * Acquires write lock. * * @return {BluebirdPromise} * @private */ _acquireWriteLock() { const mutex = this._mutex; return new bluebird_1.default((resolve, reject) => { mutex.lock(resolve); }).disposer(() => { mutex.unlock(); }); } /** * Inserts a document. * * @param {Document|object} data * @return {BluebirdPromise} * @private */ _insertOne(data_) { const schema = this.schema; // Apply getters const data = (data_ instanceof this.Document ? data_ : this.new(data_)); const id = data._id; // Check ID if (!id) { return bluebird_1.default.reject(new error_1.default('ID is not defined', error_1.default.ID_UNDEFINED)); } if (this.has(id)) { return bluebird_1.default.reject(new error_1.default('ID `' + id + '` has been used', error_1.default.ID_EXIST)); } // Apply setters const result = data.toObject(); schema._applySetters(result); // Pre-hooks return execHooks(schema, 'pre', 'save', data).then(data => { // Insert data this.dirty = true; this.data[id] = result; this.length++; this.emit('insert', data); return execHooks(schema, 'post', 'save', data); }); } /** * Inserts a document. * * @param {object} data * @param {function} [callback] * @return {BluebirdPromise} */ insertOne(data, callback) { return bluebird_1.default.using(this._acquireWriteLock(), () => this._insertOne(data)).asCallback(callback); } /** * Inserts documents. * * @param {object|array} data * @param {function} [callback] * @return {BluebirdPromise} */ insert(data, callback) { if (Array.isArray(data)) { return bluebird_1.default.mapSeries(data, item => this.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 {BluebirdPromise} */ save(data, callback) { const id = data._id; if (!id) return this.insertOne(data, callback); return bluebird_1.default.using(this._acquireWriteLock(), () => { if (this.has(id)) { return this._replaceById(id, data); } return this._insertOne(data); }).asCallback(callback); } /** * Updates a document with a compiled stack. * * @param {*} id * @param {array} stack * @return {BluebirdPromise} * @private */ _updateWithStack(id, stack) { const schema = this.schema; const data = this.data[id]; if (!data) { return bluebird_1.default.reject(new error_1.default('ID `' + id + '` does not exist', error_1.default.ID_NOT_EXIST)); } // Clone data let result = cloneDeep(data); // Update for (let i = 0, len = stack.length; i < len; i++) { stack[i](result); } // Apply getters const doc = this.new(result); // Apply setters result = doc.toObject(); schema._applySetters(result); // Pre-hooks return execHooks(schema, 'pre', 'save', doc).then(data => { // Update data this.dirty = true; this.data[id] = result; this.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 {BluebirdPromise} */ updateById(id, update, callback) { return bluebird_1.default.using(this._acquireWriteLock(), () => { const stack = this.schema._parseUpdate(update); return this._updateWithStack(id, stack); }).asCallback(callback); } /** * Updates matching documents. * * @param {object} query * @param {object} data * @param {function} [callback] * @return {BluebirdPromise} */ update(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 {BluebirdPromise} * @private */ _replaceById(id, data_) { const schema = this.schema; if (!this.has(id)) { return bluebird_1.default.reject(new error_1.default('ID `' + id + '` does not exist', error_1.default.ID_NOT_EXIST)); } data_._id = id; // Apply getters const data = (data_ instanceof this.Document ? data_ : this.new(data_)); // Apply setters const result = data.toObject(); schema._applySetters(result); // Pre-hooks return execHooks(schema, 'pre', 'save', data).then(data => { // Replace data this.dirty = true; this.data[id] = result; this.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 {BluebirdPromise} */ replaceById(id, data, callback) { return bluebird_1.default.using(this._acquireWriteLock(), () => this._replaceById(id, data)).asCallback(callback); } /** * Replaces matching documents. * * @param {object} query * @param {object} data * @param {function} [callback] * @return {BluebirdPromise} */ replace(query, data, callback) { return this.find(query).replace(data, callback); } /** * Finds a document by its identifier and remove it. * * @param {*} id * @return {BluebirdPromise} * @private */ _removeById(id) { const schema = this.schema; const data = this.data[id]; if (!data) { return bluebird_1.default.reject(new error_1.default('ID `' + id + '` does not exist', error_1.default.ID_NOT_EXIST)); } // Pre-hooks return execHooks(schema, 'pre', 'remove', data).then(data => { // Remove data this.dirty = true; this.data[id] = null; this.length--; this.emit('remove', data); return execHooks(schema, 'post', 'remove', data); }); } /** * Finds a document by its identifier and remove it. * * @param {*} id * @param {function} [callback] * @return {BluebirdPromise} */ removeById(id, callback) { return bluebird_1.default.using(this._acquireWriteLock(), () => this._removeById(id)).asCallback(callback); } /** * Removes matching documents. * * @param {object} query * @param {function} [callback] * @return {BluebirdPromise} */ remove(query, callback) { return this.find(query).remove(callback); } /** * Deletes a model. */ destroy() { this._database._models[this.name] = null; } /** * Returns the number of elements. * * @return {number} */ count() { return this.length; } forEach(iterator, options) { const keys = this.dataKeys; let num = 0; for (let i = 0, len = keys.length; i < len; i++) { const data = this.findById(keys[i], options); if (data) iterator(data, num++); } } toArray(options) { const result = new Array(this.length); this.forEach((item, i) => { result[i] = item; }, options); return result; } find(query, options = {}) { const filter = this.schema._execQuery(query); const keys = this.dataKeys; const len = keys.length; let limit = options.limit || this.length; let skip = options.skip; const data = this.data; const arr = []; for (let i = 0; limit && i < len; i++) { const key = keys[i]; const 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); } findOne(query, options_ = {}) { const options = Object.assign(options_, { limit: 1 }); const result = this.find(query, options); return options.lean ? result[0] : result.toArray()[0]; } sort(orderby, order) { const sort = (0, util_1.parseArgs)(orderby, order); const fn = this.schema._execSort(sort); return new this.Query(this.toArray().sort(fn)); } eq(i_, options) { let index = i_ < 0 ? this.length + i_ : i_; const data = this.data; const keys = this.dataKeys; for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; const item = data[key]; if (!item) continue; if (index) { index--; } else { return this.findById(key, options); } } } first(options) { return this.eq(0, options); } last(options) { return this.eq(-1, options); } /** * Returns the specified range of documents. * * @param {Number} start * @param {Number} [end] * @return {Query} */ slice(start_, end_) { const total = this.length; let start = start_ | 0; if (start < 0) start += total; if (start > total - 1) return new this.Query([]); let end = end_ | 0 || total; if (end < 0) end += total; let len = start > end ? 0 : end - start; if (len > total) len = total - start; if (!len) return new this.Query([]); const arr = new Array(len); const keys = this.dataKeys; const keysLen = keys.length; let num = 0; for (let i = 0; num < len && i < keysLen; i++) { const 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} */ limit(i) { return this.slice(0, i); } /** * Specifies the number of items to skip. * * @param {Number} i * @return {Query} */ skip(i) { return this.slice(i); } /** * Returns documents in a reversed order. * * @return {Query} */ reverse() { return new this.Query(this.toArray().reverse()); } /** * Returns documents in random order. * * @return {Query} */ shuffle() { return new this.Query((0, util_1.shuffle)(this.toArray())); } map(iterator, options) { const result = new Array(this.length); const keys = this.dataKeys; const len = keys.length; for (let i = 0, num = 0; i < len; i++) { const data = this.findById(keys[i], options); if (data) { result[num] = iterator(data, num); num++; } } 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 {*} */ reduce(iterator, initial) { const arr = this.toArray(); const len = this.length; let 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 {*} */ reduceRight(iterator, initial) { const arr = this.toArray(); const len = this.length; let 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; } filter(iterator, options) { const arr = []; this.forEach((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} */ every(iterator) { const keys = this.dataKeys; const len = keys.length; let num = 0; if (!len) return true; for (let i = 0; i < len; i++) { const 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} */ some(iterator) { const keys = this.dataKeys; const len = keys.length; let num = 0; if (!len) return false; for (let i = 0; i < len; i++) { const 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 */ _populateGetter(data, model, options) { let hasCache = false; let cache; return () => { 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 */ _populateGetterArray(data, model, options) { const Query = model.Query; let hasCache = false; let cache; return () => { if (!hasCache) { let arr = []; for (let 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 */ _populate(data, stack) { const models = this._database._models; for (let i = 0, len = stack.length; i < len; i++) { const item = stack[i]; const model = models[item.model]; if (!model) { throw new population_1.default('Model `' + item.model + '` does not exist'); } const path = item.path; const prop = (0, util_1.getProp)(data, path); if (Array.isArray(prop)) { (0, util_1.setGetter)(data, path, this._populateGetterArray(prop, model, item)); } else { (0, util_1.setGetter)(data, path, this._populateGetter(prop, model, item)); } } return data; } /** * Populates document references. * * @param {String|Object} path * @return {Query} */ populate(path) { if (!path) throw new TypeError('path is required'); const stack = this.schema._parsePopulate(path); const arr = new Array(this.length); this.forEach((item, i) => { arr[i] = this._populate(item, stack); }); return new this.Query(arr); } /** * Imports data. * * @param {Array} arr * @private */ _import(arr) { const len = arr.length; const data = this.data; const schema = this.schema; for (let i = 0; i < len; i++) { const item = arr[i]; this.dirty = true; data[item._id] = schema._parseDatabase(item); } this.length = len; } /** * Exports data. * * @return {String} * @private */ _export() { return JSON.stringify(this.toJSON()); } toJSON() { const result = new Array(this.length); const { data, schema } = this; const keys = this.dataKeys; const { length } = keys; for (let i = 0, num = 0; i < length; i++) { const raw = data[keys[i]]; if (raw) { result[num++] = schema._exportDatabase(cloneDeep(raw)); } } return result; } } Model.prototype.get = Model.prototype.findById; function execHooks(schema, type, event, data) { const hooks = schema.hooks[type][event]; if (!hooks.length) return bluebird_1.default.resolve(data); return bluebird_1.default.each(hooks, hook => hook(data)).thenReturn(data); } Model.prototype.size = Model.prototype.count; Model.prototype.each = Model.prototype.forEach; Model.prototype.random = Model.prototype.shuffle; exports.default = Model; //# sourceMappingURL=model.js.map