UNPKG

camo

Version:

A class-based ES6 ODM for Mongo-like databases.

431 lines (364 loc) 12.6 kB
'use strict'; const _ = require('lodash'); const deprecate = require('depd')('camo'); const DB = require('./clients').getClient; const BaseDocument = require('./base-document'); const isSupportedType = require('./validate').isSupportedType; const isArray = require('./validate').isArray; const isReferenceable = require('./validate').isReferenceable; const isEmbeddedDocument = require('./validate').isEmbeddedDocument; const isString = require('./validate').isString; class Document extends BaseDocument { constructor(name) { super(); if (name !== undefined && name !== null) { deprecate('Document.constructor(name) - override Document.collectionName() instead'); this._meta = { collection: name }; } } // TODO: Is there a way to tell if a class is // a subclass of something? Until I find out // how, we'll be lazy use this. static documentClass() { return 'document'; } documentClass() { return 'document'; } get meta() { return this._meta; } set meta(meta) { this._meta = meta; } /** * Save (upsert) current document * * TODO: The method is too long and complex, it is necessary to divide... * @returns {Promise} */ save() { const that = this; let preValidatePromises = this._getHookPromises('preValidate'); return Promise.all(preValidatePromises).then(function() { // Ensure we at least have defaults set // TODO: We already do this on .create(), so // should it really be done again? _.keys(that._schema).forEach(function(key) { if (!(key in that._schema)) { that[key] = that.getDefault(key); } }); // Validate the assigned type, choices, and min/max that.validate(); // Ensure all data types are saved in the same encodings that.canonicalize(); return Promise.all(that._getHookPromises('postValidate')); }).then(function() { return Promise.all(that._getHookPromises('preSave')); }).then(function() { // TODO: We should instead track what has changed and // only update those values. Maybe make that._changed // object to do this. // Also, this might be really slow for objects with // lots of references. Figure out a better way. let toUpdate = that._toData({_id: false}); // Reference our objects _.keys(that._schema).forEach(function(key) { // Never care about _id if (key === '_id') return; if (isReferenceable(that[key]) || // isReferenceable OR (isArray(that[key]) && // isArray AND contains value AND value isReferenceable that[key].length > 0 && isReferenceable(that[key][0]))) { // Handle array of references (ex: { type: [MyObject] }) if (isArray(that[key])) { toUpdate[key] = []; that[key].forEach(function(v) { if (DB().isNativeId(v)) { toUpdate[key].push(v); } else { toUpdate[key].push(v._id); } }); } else { if (DB().isNativeId(that[key])) { toUpdate[key] = that[key]; } else { toUpdate[key] = that[key]._id; } } } }); // Replace EmbeddedDocument references with just their data _.keys(that._schema).forEach(function(key) { if (isEmbeddedDocument(that[key]) || // isEmbeddedDocument OR (isArray(that[key]) && // isArray AND contains value AND value isEmbeddedDocument that[key].length > 0 && isEmbeddedDocument(that[key][0]))) { // Handle array of references (ex: { type: [MyObject] }) if (isArray(that[key])) { toUpdate[key] = []; that[key].forEach(function(v) { toUpdate[key].push(v._toData()); }); } else { toUpdate[key] = that[key]._toData(); } } }); return DB().save(that.collectionName(), that._id, toUpdate); }).then(function(id) { if (that._id === null) { that._id = id; } }).then(function() { // TODO: hack? let postSavePromises = that._getHookPromises('postSave'); return Promise.all(postSavePromises); }).then(function() { return that; }).catch(function(error) { return Promise.reject(error); }); } /** * Delete current document * * @returns {Promise} */ delete() { const that = this; let preDeletePromises = that._getHookPromises('preDelete'); return Promise.all(preDeletePromises).then(function() { return DB().delete(that.collectionName(), that._id); }).then(function(deleteReturn) { // TODO: hack? let postDeletePromises = [deleteReturn].concat(that._getHookPromises('postDelete')); return Promise.all(postDeletePromises); }).then(function(prevData) { let deleteReturn = prevData[0]; return deleteReturn; }); } /** * Delete one document in current collection * * @param {Object} query Query * @returns {Promise} */ static deleteOne(query) { return DB().deleteOne(this.collectionName(), query); } /** * Delete many documents in current collection * * @param {Object} query Query * @returns {Promise} */ static deleteMany(query) { if (query === undefined || query === null) { query = {}; } return DB().deleteMany(this.collectionName(), query); } /** * @deprecated Use `findOne` */ static loadOne(query, options) { deprecate('loadOne - use findOne instead'); return this.findOne(query, options); } /** * Find one document in current collection * * TODO: Need options to specify whether references should be loaded * * @param {Object} query Query * @returns {Promise} */ static findOne(query, options) { const that = this; let populate = true; if (options && options.hasOwnProperty('populate')) { populate = options.populate; } return DB().findOne(this.collectionName(), query) .then(function(data) { if (!data) { return null; } let doc = that._fromData(data); if (populate === true || (isArray(populate) && populate.length > 0)) { return that.populate(doc, populate); } return doc; }).then(function(docs) { if (docs) { return docs; } return null; }); } /** * @deprecated Use `findOneAndUpdate` */ static loadOneAndUpdate(query, values, options) { deprecate('loadOneAndUpdate - use findOneAndUpdate instead'); return this.findOneAndUpdate(query, values, options); } /** * Find one document and update it in current collection * * @param {Object} query Query * @param {Object} values * @param {Object} options * @returns {Promise} */ static findOneAndUpdate(query, values, options) { const that = this; if (arguments.length < 2) { throw new Error('findOneAndUpdate requires at least 2 arguments. Got ' + arguments.length + '.'); } if (!options) { options = {}; } let populate = true; if (options.hasOwnProperty('populate')) { populate = options.populate; } return DB().findOneAndUpdate(this.collectionName(), query, values, options) .then(function(data) { if (!data) { return null; } let doc = that._fromData(data); if (populate) { return that.populate(doc); } return doc; }).then(function(doc) { if (doc) { return doc; } return null; }); } /** * @deprecated Use `findOneAndDelete` */ static loadOneAndDelete(query, options) { deprecate('loadOneAndDelete - use findOneAndDelete instead'); return this.findOneAndDelete(query, options); } /** * Find one document and delete it in current collection * * @param {Object} query Query * @param {Object} options * @returns {Promise} */ static findOneAndDelete(query, options) { const that = this; if (arguments.length < 1) { throw new Error('findOneAndDelete requires at least 1 argument. Got ' + arguments.length + '.'); } if (!options) { options = {}; } return DB().findOneAndDelete(this.collectionName(), query, options); } /** * @deprecated Use `find` */ static loadMany(query, options) { deprecate('loadMany - use find instead'); return this.find(query, options); } /** * Find documents * * TODO: Need options to specify whether references should be loaded * * @param {Object} query Query * @param {Object} options * @returns {Promise} */ static find(query, options) { const that = this; if (query === undefined || query === null) { query = {}; } if (options === undefined || options === null) { // Populate by default options = {populate: true}; } return DB().find(this.collectionName(), query, options) .then(function(datas) { let docs = that._fromData(datas); if (options.populate === true || (isArray(options.populate) && options.populate.length > 0)) { return that.populate(docs, options.populate); } return docs; }).then(function(docs) { // Ensure we always return an array return [].concat(docs); }); } /** * Get count documents in current collection by query * * @param {Object} query Query * @returns {Promise} */ static count(query) { const that = this; return DB().count(this.collectionName(), query); } /** * Create indexes * * @returns {Promise} */ static createIndexes() { if (this._indexesCreated) { return; } const that = this; let instance = this._instantiate(); _.keys(instance._schema).forEach(function(k) { if (instance._schema[k].unique) { DB().createIndex(that.collectionName(), k, {unique: true}); } }); this._indexesCreated = true; } static _fromData(datas) { let instances = super._fromData(datas); // This way we preserve the original structure of the data. Data // that was passed as an array is returned as an array, and data // passes as a single object is returned as single object let datasArray = [].concat(datas); let instancesArray = [].concat(instances); /*for (let i = 0; i < instancesArray.length; i++) { if (datasArray[i].hasOwnProperty('_id')) { instancesArray[i]._id = datasArray[i]._id; } else { instancesArray[i]._id = null; } }*/ return instances; } /** * Clear current collection * * @returns {Promise} */ static clearCollection() { return DB().clearCollection(this.collectionName()); } } module.exports = Document;