UNPKG

mongosum

Version:

Maintains summary tables on Mongo collections, on top of Mongolian

499 lines (463 loc) 14.8 kB
// Generated by CoffeeScript 1.3.3 (function() { var Collection, DB, Server, collection_name, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; Server = require('mongolian'); DB = require('mongolian/lib/db.js'); Collection = require('mongolian/lib/collection.js'); collection_name = '_summaries'; Server.prototype.summaryOptions = { ignored_columns: ['_id'], track_column: function(column, options) { return true; }, ignored_collections: ['system.indexes'], track_collection: function(collection, options) { return true; } }; Server.prototype.getSummaryOptions = function() { return this.summaryOptions; }; DB.prototype.getSummaryOptions = function() { return this.server.summaryOptions; }; Collection.prototype.getSummaryOptions = function() { return this.db.server.summaryOptions; }; Server.prototype.db = function(name) { if (!(this._dbs[name] != null)) { this._dbs[name] = new DB(this, name); this._dbs[name].summary = new Collection(this._dbs[name], collection_name); } return this._dbs[name]; }; DB.prototype.collection = function(name) { return this._collections[name] || (this._collections[name] = new Collection(this, name)); }; /* # Retrieve the summary for this collection */ Collection.prototype.getSummary = function(callback) { var criteria, _this = this; if (this.name === collection_name) { throw 'MongoSum cannot get the summary of the summarys collection.'; } criteria = { _collection: this.name }; return this.db.summary.find(criteria).next(function(err, summary) { var _ref, _ref1; if (summary == null) { summary = {}; } if ((_ref = summary._collection) == null) { summary._collection = _this.name; } if ((_ref1 = summary._length) == null) { summary._length = 0; } return callback(err, summary); }); }; /* # Set the summary for this collection (used internally) */ Collection.prototype.setSummary = function(summary, incs, callback) { var criteria, _this = this; if (this.name === collection_name) { throw 'MongoSum cannot set the summary of the summarys collection'; } criteria = { _collection: this.name }; summary._collection = this.name; summary._updated = +(new Date); delete summary._length; return this.db.summary.update(criteria, summary, true, function(err) { return _this.db.summary.update(criteria, { $inc: incs }, true, callback); }); }; /* # Do a full-table update of the summary. This is expensive. */ Collection.prototype.rebuildSummary = function(callback) { var each, options, summary, _this = this; options = this.getSummaryOptions(); summary = { _collection: this.name, _length: 0 }; each = function(object) { summary._length++; return _this._merge_summary(summary, _this._get_summary(object)); }; return this.find().forEach(each, function() { return _this.setSummary(summary, callback); }); }; /* # INTERNAL. Merge summary, save it, and fire the callback. */ Collection.prototype._merge_summarys = function(err, data, callback, options, summary) { var _this = this; return this.getSummary(function(err, full_summary) { var incs; incs = { _length: data.length || 1 }; summary._length = 0; full_summary = _this._walk_objects(full_summary, summary, options, function(key, vals, types, full_key) { if (vals[1] && vals[1].type === 'Number' && vals[1].sum) { incs[full_key.join('.') + '.sum'] = vals[1].sum; } if (!vals[0] && vals[1]) { vals[0] = JSON.parse(JSON.stringify(vals[1])); if (vals[0].sum) { vals[0].sum = vals[1].sum = 0; } } if (vals[0] && vals[1]) { if (vals[0].type === 'Number' && vals[1].type === 'Number') { vals[0].min = Math.min(vals[0].min, vals[1].min); vals[0].max = Math.max(vals[0].max, vals[1].max); } if (vals[0].example && vals[1].example) { vals[0].example = vals[1].example; } } return vals[0]; }); return _this.setSummary(full_summary, incs, function() { return callback && callback(err, data); }); }); }; Collection.prototype._drop = Collection.prototype.drop; Collection.prototype.drop = function() { this.db.summary.remove({ _collection: this.name }); return this._drop.apply(this, arguments); }; Collection.prototype._insert = Collection.prototype.insert; Collection.prototype.insert = function(object, callback) { var complete, obj, options, summary, update_summary, _i, _len, _ref, _results, _this = this; options = this.getSummaryOptions(); if ((this.name === collection_name) || (_ref = this.name, __indexOf.call(options.ignored_collections, _ref) >= 0) || (!options.track_collection(this.name, options))) { return Collection.prototype._insert.apply(this, arguments); } summary = { _length: 0 }; update_summary = function(err, data) { if (!err) { return _this._merge_summary(summary, _this._get_summary(data)); } }; if (Object.prototype.toString.call(object) !== '[object Array]') { object = [object]; } complete = 0; _results = []; for (_i = 0, _len = object.length; _i < _len; _i++) { obj = object[_i]; _results.push(this._insert(obj, function(err, data) { update_summary(err, data); summary._length++; if (++complete === object.length) { return _this._merge_summarys(err, data, callback, {}, summary); } })); } return _results; }; Collection.prototype._update = Collection.prototype.update; Collection.prototype.update = function(criteria, object, upsert, multi, callback) { var merge_opts, options, subtract_summary, summary, update_summary, _ref, _this = this; options = this.getSummaryOptions(); if ((this.name === collection_name) || (_ref = this.name, __indexOf.call(options.ignored_collections, _ref) >= 0) || (!options.track_collection(this.name, options))) { return Collection.prototype._update.apply(this, arguments); } if (!callback && typeof multi === 'function') { callback = multi; multi = false; } if (!callback && typeof upsert === 'function') { callback = upsert; upsert = false; } if (callback && typeof callback !== 'function') { throw 'Callback is not a function!'; } summary = { _length: 0 }; subtract_summary = function(err, data) { if (!err && data) { return _this._merge_summary(summary, _this._get_summary(data), { sum: function(a, b) { return (b === null && -a) || (a - b); }, min: function(a, b) { return a; }, max: function(a, b) { return a; } }); } }; update_summary = function(err, data) { if (!err && data) { return _this._merge_summary(summary, _this._get_summary(data)); } }; if (Object.prototype.toString.call(object) !== '[object Array]') { object = [object]; } if (multi !== true) { object = [object.shift()]; } options = { remove: false, "new": true, upsert: !!upsert }; merge_opts = { min: function(a, b) { if (isNaN(parseInt(a)) || (b === a)) { throw 'FULL UPDATE'; } return Math.min(a, b); }, max: function(a, b) { if (isNaN(parseInt(a)) || (b === a)) { throw 'FULL UPDATE'; } return Math.max(a, b); } }; return this.find(criteria).toArray(function(err, _originals) { var complete, for_merge, o, obj, opts, originals, _i, _j, _len, _len1, _results; if (_originals == null) { _originals = []; } originals = {}; for (_i = 0, _len = _originals.length; _i < _len; _i++) { o = _originals[_i]; originals[o._id.toString()] = o; } for_merge = []; complete = 0; _results = []; for (_j = 0, _len1 = object.length; _j < _len1; _j++) { obj = object[_j]; opts = { query: criteria, update: obj, options: options, remove: false, "new": true, upsert: !!upsert }; _results.push(_this.findAndModify(opts, function(err, data) { var _k, _len2; if (!err && data) { subtract_summary(err, originals[data._id.toString()]); if (!err) { for_merge.push(data); } if (++complete === object.length) { try { for (_k = 0, _len2 = for_merge.length; _k < _len2; _k++) { data = for_merge[_k]; update_summary(null, data); } return _this._merge_summarys(err, data, callback, merge_opts, summary); } catch (e) { if (e === 'FULL UPDATE') { return _this.updateSummary(callback); } else { throw e; } } } } })); } return _results; }); }; Collection.prototype._remove = Collection.prototype.remove; Collection.prototype.remove = function(criteria, callback) { var merge_opts, options, subtract_summary, summary, _ref, _this = this; options = this.getSummaryOptions(); if ((this.name === collection_name) || (_ref = this.name, __indexOf.call(options.ignored_collections, _ref) >= 0) || (!options.track_collection(this.name, options))) { return Collection.prototype._remove.apply(this, arguments); } if (!callback && typeof criteria === 'function') { callback = criteria; criteria = {}; } summary = { _length: 0 }; subtract_summary = function(err, data) { if (!err && data) { return _this._merge_summary(summary, _this._get_summary(data), { sum: function(a, b) { return (b === null && -a) || (a - b); }, min: function(a, b) { return a; }, max: function(a, b) { return a; } }); } }; merge_opts = { min: function(a, b) { if (isNaN(parseInt(a)) || (b === a)) { throw 'FULL UPDATE'; } return Math.min(a, b); }, max: function(a, b) { if (isNaN(parseInt(a)) || (b === a)) { throw 'FULL UPDATE'; } return Math.max(a, b); } }; return this.find(criteria).toArray(function(err, data) { var row, _i, _len; data = data || []; for (_i = 0, _len = data.length; _i < _len; _i++) { row = data[_i]; summary._length--; subtract_summary(err, row); } try { _this._merge_summarys(err, data, (function() { return null; }), merge_opts, summary); return _this._remove(criteria, callback); } catch (e) { if (e === 'FULL UPDATE') { return _this.updateSummary(callback); } else { throw e; } } }); }; Collection.prototype._get_summary = function(object) { return this._walk_objects(object, {}, {}, function(key, vals, types) { var ret; ret = {}; ret.type = types[0]; ret.example = vals[0]; if (types[0] === 'Number' || (vals[0] = parseInt(vals[0], 10))) { ret.min = ret.max = ret.sum = vals[0]; } return ret; }); }; Collection.prototype._merge_summary = function(left, right, options) { var _ref, _ref1, _ref2; if (options == null) { options = {}; } if ((_ref = options.sum) == null) { options.sum = function(a, b) { return (parseInt(a, 10) + parseInt(b, 10)) || a; }; } if ((_ref1 = options.min) == null) { options.min = Math.min; } if ((_ref2 = options.max) == null) { options.max = Math.max; } return this._walk_objects(left, right, {}, function(key, vals, types) { if (!vals[0] && vals[1]) { vals[0] = JSON.parse(JSON.stringify(vals[1])); if (vals[1].sum) { vals[1].sum = null; } } if (vals[0] && vals[0].type && vals[1] && vals[1].type) { if (vals[0].type === 'Number' && vals[1].type === 'Number') { vals[0].min = options.min(vals[0].min, vals[1].min); vals[0].max = options.max(vals[0].max, vals[1].max); vals[0].sum = options.sum(vals[0].sum, vals[1].sum); } vals[0].example = (vals[1] && vals[1].example) || vals[0].example; } return vals[0]; }); }; Collection.prototype._walk_objects = function(first, second, options, fn, full_key) { var k, key, keys, sopts, type, v, v1, v2, val, _i, _len, _ref, _ref1; if (first == null) { first = {}; } if (second == null) { second = {}; } if (full_key == null) { full_key = []; } keys = (function() { var _results; _results = []; for (k in first) { v = first[k]; _results.push(k); } return _results; })(); for (k in second) { v = second[k]; if (__indexOf.call(keys, k) < 0) { keys.push(k); } } sopts = this.getSummaryOptions(); for (_i = 0, _len = keys.length; _i < _len; _i++) { key = keys[_i]; if (!((__indexOf.call(sopts.ignored_columns, key) < 0) || sopts.track_column(key, sopts))) { continue; } v1 = first[key]; v2 = second[key]; type = function(o) { return ((o != null) && o.constructor && o.constructor.name) || 'Null'; }; if ((((_ref = type(v1)) === 'Object' || _ref === 'Array') && !(v1.type != null)) || (((_ref1 = type(v2)) === 'Object' || _ref1 === 'Array') && !(v2.type != null))) { first[key] = this._walk_objects(v1, v2, options, fn, full_key.concat([key])); } else { first[key] = fn(key, [v1, v2], [type(v1), type(v2)], full_key.concat([key])); } } for (key in first) { val = first[key]; if (__indexOf.call(sopts.ignored_columns, key) >= 0 || !sopts.track_column(key, sopts)) { delete first[key]; } } return first; }; module.exports = Server; }).call(this);