nqm-minimongo
Version:
Client-side mongo database with server sync over http
1,647 lines (1,503 loc) • 101 kB
JavaScript
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({"Focm2+":[function(require,module,exports){
exports.MemoryDb = require('./lib/MemoryDb');
// exports.LocalStorageDb = require('./lib/LocalStorageDb');
// exports.IndexedDb = require('./lib/IndexedDb');
// exports.WebSQLDb = require('./lib/WebSQLDb');
// exports.RemoteDb = require('./lib/RemoteDb');
// exports.HybridDb = require('./lib/HybridDb');
// exports.ReplicatingDb = require('./lib/ReplicatingDb');
exports.utils = require('./lib/utils');
},{"./lib/MemoryDb":7,"./lib/utils":9}],"minimongo":[function(require,module,exports){
module.exports=require('Focm2+');
},{}],"jquery":[function(require,module,exports){
module.exports=require('/nayFu');
},{}],"/nayFu":[function(require,module,exports){
module.exports = window.$;
},{}],5:[function(require,module,exports){
var _ = require('lodash');
EJSON = {}; // Global!
var customTypes = {};
// Add a custom type, using a method of your choice to get to and
// from a basic JSON-able representation. The factory argument
// is a function of JSON-able --> your object
// The type you add must have:
// - A clone() method, so that Meteor can deep-copy it when necessary.
// - A equals() method, so that Meteor can compare it
// - A toJSONValue() method, so that Meteor can serialize it
// - a typeName() method, to show how to look it up in our type table.
// It is okay if these methods are monkey-patched on.
EJSON.addType = function (name, factory) {
if (_.has(customTypes, name))
throw new Error("Type " + name + " already present");
customTypes[name] = factory;
};
var builtinConverters = [
{ // Date
matchJSONValue: function (obj) {
return _.has(obj, '$date') && _.size(obj) === 1;
},
matchObject: function (obj) {
return obj instanceof Date;
},
toJSONValue: function (obj) {
return {$date: obj.getTime()};
},
fromJSONValue: function (obj) {
return new Date(obj.$date);
}
},
{ // Binary
matchJSONValue: function (obj) {
return _.has(obj, '$binary') && _.size(obj) === 1;
},
matchObject: function (obj) {
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array
|| (obj && _.has(obj, '$Uint8ArrayPolyfill'));
},
toJSONValue: function (obj) {
return {$binary: EJSON._base64Encode(obj)};
},
fromJSONValue: function (obj) {
return EJSON._base64Decode(obj.$binary);
}
},
{ // Escaping one level
matchJSONValue: function (obj) {
return _.has(obj, '$escape') && _.size(obj) === 1;
},
matchObject: function (obj) {
if (_.isEmpty(obj) || _.size(obj) > 2) {
return false;
}
return _.any(builtinConverters, function (converter) {
return converter.matchJSONValue(obj);
});
},
toJSONValue: function (obj) {
var newObj = {};
_.each(obj, function (value, key) {
newObj[key] = EJSON.toJSONValue(value);
});
return {$escape: newObj};
},
fromJSONValue: function (obj) {
var newObj = {};
_.each(obj.$escape, function (value, key) {
newObj[key] = EJSON.fromJSONValue(value);
});
return newObj;
}
},
{ // Custom
matchJSONValue: function (obj) {
return _.has(obj, '$type') && _.has(obj, '$value') && _.size(obj) === 2;
},
matchObject: function (obj) {
return EJSON._isCustomType(obj);
},
toJSONValue: function (obj) {
return {$type: obj.typeName(), $value: obj.toJSONValue()};
},
fromJSONValue: function (obj) {
var typeName = obj.$type;
var converter = customTypes[typeName];
return converter(obj.$value);
}
}
];
EJSON._isCustomType = function (obj) {
return obj &&
typeof obj.toJSONValue === 'function' &&
typeof obj.typeName === 'function' &&
_.has(customTypes, obj.typeName());
};
//for both arrays and objects, in-place modification.
var adjustTypesToJSONValue =
EJSON._adjustTypesToJSONValue = function (obj) {
if (obj === null)
return null;
var maybeChanged = toJSONValueHelper(obj);
if (maybeChanged !== undefined)
return maybeChanged;
_.each(obj, function (value, key) {
if (typeof value !== 'object' && value !== undefined)
return; // continue
var changed = toJSONValueHelper(value);
if (changed) {
obj[key] = changed;
return; // on to the next key
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
adjustTypesToJSONValue(value);
});
return obj;
};
// Either return the JSON-compatible version of the argument, or undefined (if
// the item isn't itself replaceable, but maybe some fields in it are)
var toJSONValueHelper = function (item) {
for (var i = 0; i < builtinConverters.length; i++) {
var converter = builtinConverters[i];
if (converter.matchObject(item)) {
return converter.toJSONValue(item);
}
}
return undefined;
};
EJSON.toJSONValue = function (item) {
var changed = toJSONValueHelper(item);
if (changed !== undefined)
return changed;
if (typeof item === 'object') {
item = EJSON.clone(item);
adjustTypesToJSONValue(item);
}
return item;
};
//for both arrays and objects. Tries its best to just
// use the object you hand it, but may return something
// different if the object you hand it itself needs changing.
var adjustTypesFromJSONValue =
EJSON._adjustTypesFromJSONValue = function (obj) {
if (obj === null)
return null;
var maybeChanged = fromJSONValueHelper(obj);
if (maybeChanged !== obj)
return maybeChanged;
_.each(obj, function (value, key) {
if (typeof value === 'object') {
var changed = fromJSONValueHelper(value);
if (value !== changed) {
obj[key] = changed;
return;
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
adjustTypesFromJSONValue(value);
}
});
return obj;
};
// Either return the argument changed to have the non-json
// rep of itself (the Object version) or the argument itself.
// DOES NOT RECURSE. For actually getting the fully-changed value, use
// EJSON.fromJSONValue
var fromJSONValueHelper = function (value) {
if (typeof value === 'object' && value !== null) {
if (_.size(value) <= 2
&& _.all(value, function (v, k) {
return typeof k === 'string' && k.substr(0, 1) === '$';
})) {
for (var i = 0; i < builtinConverters.length; i++) {
var converter = builtinConverters[i];
if (converter.matchJSONValue(value)) {
return converter.fromJSONValue(value);
}
}
}
}
return value;
};
EJSON.fromJSONValue = function (item) {
var changed = fromJSONValueHelper(item);
if (changed === item && typeof item === 'object') {
item = EJSON.clone(item);
adjustTypesFromJSONValue(item);
return item;
} else {
return changed;
}
};
EJSON.stringify = function (item) {
return JSON.stringify(EJSON.toJSONValue(item));
};
EJSON.parse = function (item) {
return EJSON.fromJSONValue(JSON.parse(item));
};
EJSON.isBinary = function (obj) {
return (typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) ||
(obj && obj.$Uint8ArrayPolyfill);
};
EJSON.equals = function (a, b, options) {
var i;
var keyOrderSensitive = !!(options && options.keyOrderSensitive);
if (a === b)
return true;
if (!a || !b) // if either one is falsy, they'd have to be === to be equal
return false;
if (!(typeof a === 'object' && typeof b === 'object'))
return false;
if (a instanceof Date && b instanceof Date)
return a.valueOf() === b.valueOf();
if (EJSON.isBinary(a) && EJSON.isBinary(b)) {
if (a.length !== b.length)
return false;
for (i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
}
if (typeof (a.equals) === 'function')
return a.equals(b, options);
if (a instanceof Array) {
if (!(b instanceof Array))
return false;
if (a.length !== b.length)
return false;
for (i = 0; i < a.length; i++) {
if (!EJSON.equals(a[i], b[i], options))
return false;
}
return true;
}
// fall back to structural equality of objects
var ret;
if (keyOrderSensitive) {
var bKeys = [];
_.each(b, function (val, x) {
bKeys.push(x);
});
i = 0;
ret = _.all(a, function (val, x) {
if (i >= bKeys.length) {
return false;
}
if (x !== bKeys[i]) {
return false;
}
if (!EJSON.equals(val, b[bKeys[i]], options)) {
return false;
}
i++;
return true;
});
return ret && i === bKeys.length;
} else {
i = 0;
ret = _.all(a, function (val, key) {
if (!_.has(b, key)) {
return false;
}
if (!EJSON.equals(val, b[key], options)) {
return false;
}
i++;
return true;
});
return ret && _.size(b) === i;
}
};
EJSON.clone = function (v) {
var ret;
if (typeof v !== "object")
return v;
if (v === null)
return null; // null has typeof "object"
if (v instanceof Date)
return new Date(v.getTime());
if (EJSON.isBinary(v)) {
ret = EJSON.newBinary(v.length);
for (var i = 0; i < v.length; i++) {
ret[i] = v[i];
}
return ret;
}
if (_.isArray(v) || _.isArguments(v)) {
// For some reason, _.map doesn't work in this context on Opera (weird test
// failures).
ret = [];
for (i = 0; i < v.length; i++)
ret[i] = EJSON.clone(v[i]);
return ret;
}
// handle general user-defined typed Objects if they have a clone method
if (typeof v.clone === 'function') {
return v.clone();
}
// handle other objects
ret = {};
_.each(v, function (value, key) {
ret[key] = EJSON.clone(value);
});
return ret;
};
module.exports = EJSON;
},{"lodash":"nJZoxB"}],6:[function(require,module,exports){
/*
Database which caches locally in a localDb but pulls results
ultimately from a RemoteDb
*/
var HybridCollection, HybridDb, processFind, utils, _;
_ = require('lodash');
processFind = require('./utils').processFind;
utils = require('./utils');
module.exports = HybridDb = (function() {
function HybridDb(localDb, remoteDb) {
this.localDb = localDb;
this.remoteDb = remoteDb;
this.collections = {};
}
HybridDb.prototype.addCollection = function(name, options, success, error) {
var collection, _ref;
if (_.isFunction(options)) {
_ref = [{}, options, success], options = _ref[0], success = _ref[1], error = _ref[2];
}
collection = new HybridCollection(name, this.localDb[name], this.remoteDb[name], options);
this[name] = collection;
this.collections[name] = collection;
if (success != null) {
return success();
}
};
HybridDb.prototype.removeCollection = function(name, success, error) {
delete this[name];
delete this.collections[name];
if (success != null) {
return success();
}
};
HybridDb.prototype.upload = function(success, error) {
var cols, uploadCols;
cols = _.values(this.collections);
uploadCols = function(cols, success, error) {
var col;
col = _.first(cols);
if (col) {
return col.upload(function() {
return uploadCols(_.rest(cols), success, error);
}, function(err) {
return error(err);
});
} else {
return success();
}
};
return uploadCols(cols, success, error);
};
return HybridDb;
})();
HybridCollection = (function() {
function HybridCollection(name, localCol, remoteCol, options) {
this.name = name;
this.localCol = localCol;
this.remoteCol = remoteCol;
this.options = options || {};
_.defaults(this.options, {
cacheFind: true,
cacheFindOne: true,
interim: true,
useLocalOnRemoteError: true,
shortcut: false,
timeout: 0,
sortUpserts: null
});
}
HybridCollection.prototype.find = function(selector, options) {
if (options == null) {
options = {};
}
return {
fetch: (function(_this) {
return function(success, error) {
return _this._findFetch(selector, options, success, error);
};
})(this)
};
};
HybridCollection.prototype.findOne = function(selector, options, success, error) {
var step2, _ref;
if (options == null) {
options = {};
}
if (_.isFunction(options)) {
_ref = [{}, options, success], options = _ref[0], success = _ref[1], error = _ref[2];
}
_.defaults(options, this.options);
step2 = (function(_this) {
return function(localDoc) {
var findOptions;
findOptions = _.cloneDeep(options);
findOptions.interim = false;
findOptions.cacheFind = options.cacheFindOne;
if (selector._id) {
findOptions.limit = 1;
} else {
delete findOptions.limit;
}
return _this.find(selector, findOptions).fetch(function(data) {
if (data.length > 0) {
if (!_.isEqual(localDoc, data[0])) {
return success(data[0]);
}
} else {
return success(null);
}
}, error);
};
})(this);
if (options.interim || options.shortcut) {
return this.localCol.findOne(selector, options, function(localDoc) {
if (localDoc) {
success(_.cloneDeep(localDoc));
if (options.shortcut) {
return;
}
}
return step2(localDoc);
}, error);
} else {
return step2();
}
};
HybridCollection.prototype._findFetch = function(selector, options, success, error) {
var localSuccess, step2;
_.defaults(options, this.options);
step2 = (function(_this) {
return function(localData) {
var remoteError, remoteOptions, remoteSuccess, timedOut, timer;
remoteOptions = _.cloneDeep(options);
if (options.cacheFind) {
delete remoteOptions.fields;
}
timer = null;
timedOut = false;
remoteSuccess = function(remoteData) {
var cacheSuccess, data;
if (timer) {
clearTimeout(timer);
}
if (timedOut) {
if (options.cacheFind) {
_this.localCol.cache(remoteData, selector, options, (function() {}), error);
}
return;
}
if (options.cacheFind) {
cacheSuccess = function() {
var localSuccess2;
localSuccess2 = function(localData2) {
if (!_.isEqual(localData, localData2)) {
return success(localData2);
}
};
return _this.localCol.find(selector, options).fetch(localSuccess2, error);
};
return _this.localCol.cache(remoteData, selector, options, cacheSuccess, error);
} else {
data = remoteData;
return _this.localCol.pendingRemoves(function(removes) {
var removesMap;
if (removes.length > 0) {
removesMap = _.object(_.map(removes, function(id) {
return [id, id];
}));
data = _.filter(remoteData, function(doc) {
return !_.has(removesMap, doc._id);
});
}
return _this.localCol.pendingUpserts(function(upserts) {
var upsertsMap;
if (upserts.length > 0) {
upsertsMap = _.object(_.map(upserts, function(u) {
return u.doc._id;
}), _.map(upserts, function(u) {
return u.doc._id;
}));
data = _.filter(data, function(doc) {
return !_.has(upsertsMap, doc._id);
});
data = data.concat(_.pluck(upserts, "doc"));
data = processFind(data, selector, options);
}
if (!_.isEqual(localData, data)) {
return success(data);
}
}, error);
}, error);
}
};
remoteError = function(err) {
if (timer) {
clearTimeout(timer);
}
if (timedOut) {
return;
}
if (!options.interim) {
if (options.useLocalOnRemoteError) {
return _this.localCol.find(selector, options).fetch(success, error);
} else {
if (error) {
return error(err);
}
}
} else {
}
};
if (options.timeout) {
timer = setTimeout(function() {
timer = null;
timedOut = true;
if (!options.interim) {
if (options.useLocalOnRemoteError) {
return _this.localCol.find(selector, options).fetch(success, error);
} else {
if (error) {
return error(new Error("Remote timed out"));
}
}
} else {
}
}, options.timeout);
}
return _this.remoteCol.find(selector, remoteOptions).fetch(remoteSuccess, remoteError);
};
})(this);
if (options.interim) {
localSuccess = function(localData) {
success(localData);
return step2(localData);
};
return this.localCol.find(selector, options).fetch(localSuccess, error);
} else {
return step2();
}
};
HybridCollection.prototype.upsert = function(docs, bases, success, error) {
return this.localCol.upsert(docs, bases, function(result) {
if (_.isFunction(bases)) {
success = bases;
}
return typeof success === "function" ? success(docs) : void 0;
}, error);
};
HybridCollection.prototype.remove = function(id, success, error) {
return this.localCol.remove(id, function() {
if (success != null) {
return success();
}
}, error);
};
HybridCollection.prototype.upload = function(success, error) {
var uploadRemoves, uploadUpserts;
uploadUpserts = (function(_this) {
return function(upserts, success, error) {
var upsert;
upsert = _.first(upserts);
if (upsert) {
return _this.remoteCol.upsert(upsert.doc, upsert.base, function(remoteDoc) {
return _this.localCol.resolveUpserts([upsert], function() {
if (remoteDoc) {
return _this.localCol.cacheOne(remoteDoc, function() {
return uploadUpserts(_.rest(upserts), success, error);
}, error);
} else {
return _this.localCol.remove(upsert.doc._id, function() {
return _this.localCol.resolveRemove(upsert.doc._id, function() {
return uploadUpserts(_.rest(upserts), success, error);
}, error);
}, error);
}
}, error);
}, function(err) {
if (err.status === 410 || err.status === 403) {
return _this.localCol.remove(upsert.doc._id, function() {
return _this.localCol.resolveRemove(upsert.doc._id, function() {
if (err.status === 410) {
return uploadUpserts(_.rest(upserts), success, error);
} else {
return error(err);
}
}, error);
}, error);
} else {
return error(err);
}
});
} else {
return success();
}
};
})(this);
uploadRemoves = (function(_this) {
return function(removes, success, error) {
var remove;
remove = _.first(removes);
if (remove) {
return _this.remoteCol.remove(remove, function() {
return _this.localCol.resolveRemove(remove, function() {
return uploadRemoves(_.rest(removes), success, error);
}, error);
}, function(err) {
if (err.status === 410 || err.status === 403) {
return _this.localCol.resolveRemove(remove, function() {
if (err.status === 410) {
return uploadRemoves(_.rest(removes), success, error);
} else {
return error(err);
}
}, error);
} else {
return error(err);
}
}, error);
} else {
return success();
}
};
})(this);
return this.localCol.pendingUpserts((function(_this) {
return function(upserts) {
if (_this.options.sortUpserts) {
upserts.sort(_this.options.sortUpserts);
}
return uploadUpserts(upserts, function() {
return _this.localCol.pendingRemoves(function(removes) {
return uploadRemoves(removes, success, error);
}, error);
}, error);
};
})(this), error);
};
return HybridCollection;
})();
},{"./utils":9,"lodash":"nJZoxB"}],7:[function(require,module,exports){
var Collection, MemoryDb, async, compileSort, processFind, utils, _;
_ = require('lodash');
async = require('async');
utils = require('./utils');
processFind = require('./utils').processFind;
compileSort = require('./selector').compileSort;
module.exports = MemoryDb = (function() {
function MemoryDb(options, success) {
this.collections = {};
if (success) {
success(this);
}
}
MemoryDb.prototype.addCollection = function(name, success, error) {
var collection;
collection = new Collection(name);
this[name] = collection;
this.collections[name] = collection;
if (success != null) {
return success();
}
};
MemoryDb.prototype.removeCollection = function(name, success, error) {
delete this[name];
delete this.collections[name];
if (success != null) {
return success();
}
};
return MemoryDb;
})();
Collection = (function() {
function Collection(name) {
this.name = name;
this.items = {};
this.upserts = {};
this.removes = {};
}
Collection.prototype.find = function(selector, options) {
return {
fetch: (function(_this) {
return function(success, error) {
return _this._findFetch(selector, options, success, error);
};
})(this)
};
};
Collection.prototype.findOne = function(selector, options, success, error) {
var results, _ref;
if (_.isFunction(options)) {
_ref = [{}, options, success], options = _ref[0], success = _ref[1], error = _ref[2];
}
results = this.find(selector, options).fetch(function(results) {
if (success != null) {
return success(results.length > 0 ? results[0] : null);
}
}, error);
if (results && results.length > 0) {
return results[0];
} else {
return null;
}
};
Collection.prototype._findFetch = function(selector, options, success, error) {
var results;
results = processFind(_.values(this.items), selector, options);
if (success != null) {
success(results);
}
return results;
};
Collection.prototype.upsert = function(docs, bases, success, error) {
var item, items, _i, _len, _ref;
_ref = utils.regularizeUpsert(docs, bases, success, error), items = _ref[0], success = _ref[1], error = _ref[2];
for (_i = 0, _len = items.length; _i < _len; _i++) {
item = items[_i];
if (item.base === void 0) {
if (this.upserts[item.doc._id]) {
item.base = this.upserts[item.doc._id].base;
} else {
item.base = this.items[item.doc._id] || null;
}
}
this.items[item.doc._id] = item.doc;
this.upserts[item.doc._id] = item;
}
if (success) {
return success(docs);
}
};
Collection.prototype.remove = function(id, success, error) {
if (_.isObject(id)) {
this.find(id).fetch((function(_this) {
return function(rows) {
return async.each(rows, function(row, cb) {
return _this.remove(row._id, (function() {
return cb();
}), cb);
}, function() {
return success();
});
};
})(this), error);
return;
}
if (_.has(this.items, id)) {
this.removes[id] = this.items[id];
delete this.items[id];
delete this.upserts[id];
} else {
this.removes[id] = {
_id: id
};
}
if (success != null) {
return success();
}
};
Collection.prototype.cache = function(docs, selector, options, success, error) {
var doc, docsMap, sort, _i, _len;
for (_i = 0, _len = docs.length; _i < _len; _i++) {
doc = docs[_i];
this.cacheOne(doc);
}
docsMap = _.object(_.pluck(docs, "_id"), docs);
if (options.sort) {
sort = compileSort(options.sort);
}
return this.find(selector, options).fetch((function(_this) {
return function(results) {
var result, _j, _len1;
for (_j = 0, _len1 = results.length; _j < _len1; _j++) {
result = results[_j];
if (!docsMap[result._id] && !_.has(_this.upserts, result._id)) {
if (options.sort && options.limit && docs.length === options.limit) {
if (sort(result, _.last(docs)) >= 0) {
continue;
}
}
delete _this.items[result._id];
}
}
if (success != null) {
return success();
}
};
})(this), error);
};
Collection.prototype.pendingUpserts = function(success) {
return success(_.values(this.upserts));
};
Collection.prototype.pendingRemoves = function(success) {
return success(_.pluck(this.removes, "_id"));
};
Collection.prototype.resolveUpserts = function(upserts, success) {
var id, upsert, _i, _len;
for (_i = 0, _len = upserts.length; _i < _len; _i++) {
upsert = upserts[_i];
id = upsert.doc._id;
if (this.upserts[id]) {
if (_.isEqual(upsert.doc, this.upserts[id].doc)) {
delete this.upserts[id];
} else {
this.upserts[id].base = upsert.doc;
}
}
}
if (success != null) {
return success();
}
};
Collection.prototype.resolveRemove = function(id, success) {
delete this.removes[id];
if (success != null) {
return success();
}
};
Collection.prototype.seed = function(docs, success) {
var doc, _i, _len;
if (!_.isArray(docs)) {
docs = [docs];
}
for (_i = 0, _len = docs.length; _i < _len; _i++) {
doc = docs[_i];
if (!_.has(this.items, doc._id) && !_.has(this.removes, doc._id)) {
this.items[doc._id] = doc;
}
}
if (success != null) {
return success();
}
};
Collection.prototype.cacheOne = function(doc, success) {
var existing;
if (!_.has(this.upserts, doc._id) && !_.has(this.removes, doc._id)) {
existing = this.items[doc._id];
if (!existing || !doc._rev || !existing._rev || doc._rev >= existing._rev) {
this.items[doc._id] = doc;
}
}
if (success != null) {
return success();
}
};
Collection.prototype.uncache = function(selector, success, error) {
var compiledSelector, items;
compiledSelector = utils.compileDocumentSelector(selector);
items = _.filter(_.values(this.items), (function(_this) {
return function(item) {
return (_this.upserts[item._id] != null) || !compiledSelector(item);
};
})(this));
this.items = _.object(_.pluck(items, "_id"), items);
if (success != null) {
return success();
}
};
return Collection;
})();
},{"./selector":8,"./utils":9,"async":12,"lodash":"nJZoxB"}],8:[function(require,module,exports){
/*
========================================
Meteor is licensed under the MIT License
========================================
Copyright (C) 2011--2012 Meteor Development Group
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
====================================================================
This license applies to all code in Meteor that is not an externally
maintained library. Externally maintained libraries have their own
licenses, included below:
====================================================================
*/
LocalCollection = {};
EJSON = require("./EJSON");
var _ = require('lodash');
// Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as
// arrays.
var isArray = function (x) {
return _.isArray(x) && !EJSON.isBinary(x);
};
var _anyIfArray = function (x, f) {
if (isArray(x))
return _.any(x, f);
return f(x);
};
var _anyIfArrayPlus = function (x, f) {
if (f(x))
return true;
return isArray(x) && _.any(x, f);
};
var hasOperators = function(valueSelector) {
var theseAreOperators = undefined;
for (var selKey in valueSelector) {
var thisIsOperator = selKey.substr(0, 1) === '$';
if (theseAreOperators === undefined) {
theseAreOperators = thisIsOperator;
} else if (theseAreOperators !== thisIsOperator) {
throw new Error("Inconsistent selector: " + valueSelector);
}
}
return !!theseAreOperators; // {} has no operators
};
var compileValueSelector = function (valueSelector) {
if (valueSelector == null) { // undefined or null
return function (value) {
return _anyIfArray(value, function (x) {
return x == null; // undefined or null
});
};
}
// Selector is a non-null primitive (and not an array or RegExp either).
if (!_.isObject(valueSelector)) {
return function (value) {
return _anyIfArray(value, function (x) {
return x === valueSelector;
});
};
}
if (valueSelector instanceof RegExp) {
return function (value) {
if (value === undefined)
return false;
return _anyIfArray(value, function (x) {
return valueSelector.test(x);
});
};
}
// Arrays match either identical arrays or arrays that contain it as a value.
if (isArray(valueSelector)) {
return function (value) {
if (!isArray(value))
return false;
return _anyIfArrayPlus(value, function (x) {
return LocalCollection._f._equal(valueSelector, x);
});
};
}
// It's an object, but not an array or regexp.
if (hasOperators(valueSelector)) {
var operatorFunctions = [];
_.each(valueSelector, function (operand, operator) {
if (!_.has(VALUE_OPERATORS, operator))
throw new Error("Unrecognized operator: " + operator);
operatorFunctions.push(VALUE_OPERATORS[operator](
operand, valueSelector.$options));
});
return function (value) {
return _.all(operatorFunctions, function (f) {
return f(value);
});
};
}
// It's a literal; compare value (or element of value array) directly to the
// selector.
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._equal(valueSelector, x);
});
};
};
// XXX can factor out common logic below
var LOGICAL_OPERATORS = {
"$and": function(subSelector) {
if (!isArray(subSelector) || _.isEmpty(subSelector))
throw Error("$and/$or/$nor must be nonempty array");
var subSelectorFunctions = _.map(
subSelector, compileDocumentSelector);
return function (doc) {
return _.all(subSelectorFunctions, function (f) {
return f(doc);
});
};
},
"$or": function(subSelector) {
if (!isArray(subSelector) || _.isEmpty(subSelector))
throw Error("$and/$or/$nor must be nonempty array");
var subSelectorFunctions = _.map(
subSelector, compileDocumentSelector);
return function (doc) {
return _.any(subSelectorFunctions, function (f) {
return f(doc);
});
};
},
"$nor": function(subSelector) {
if (!isArray(subSelector) || _.isEmpty(subSelector))
throw Error("$and/$or/$nor must be nonempty array");
var subSelectorFunctions = _.map(
subSelector, compileDocumentSelector);
return function (doc) {
return _.all(subSelectorFunctions, function (f) {
return !f(doc);
});
};
},
"$where": function(selectorValue) {
if (!(selectorValue instanceof Function)) {
selectorValue = Function("return " + selectorValue);
}
return function (doc) {
return selectorValue.call(doc);
};
}
};
var VALUE_OPERATORS = {
"$in": function (operand) {
if (!isArray(operand))
throw new Error("Argument to $in must be array");
// Create index if all strings
var index = null;
if (_.all(operand, _.isString))
index = _.indexBy(operand);
return function (value) {
return _anyIfArrayPlus(value, function (x) {
if (_.isString(x) && index !== null)
return index[x] != undefined;
return _.any(operand, function (operandElt) {
return LocalCollection._f._equal(operandElt, x);
});
});
};
},
"$all": function (operand) {
if (!isArray(operand))
throw new Error("Argument to $all must be array");
return function (value) {
if (!isArray(value))
return false;
return _.all(operand, function (operandElt) {
return _.any(value, function (valueElt) {
return LocalCollection._f._equal(operandElt, valueElt);
});
});
};
},
"$lt": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) < 0;
});
};
},
"$lte": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) <= 0;
});
};
},
"$gt": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) > 0;
});
};
},
"$gte": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) >= 0;
});
};
},
"$ne": function (operand) {
return function (value) {
return ! _anyIfArrayPlus(value, function (x) {
return LocalCollection._f._equal(x, operand);
});
};
},
"$nin": function (operand) {
if (!isArray(operand))
throw new Error("Argument to $nin must be array");
var inFunction = VALUE_OPERATORS.$in(operand);
return function (value) {
// Field doesn't exist, so it's not-in operand
if (value === undefined)
return true;
return !inFunction(value);
};
},
"$exists": function (operand) {
return function (value) {
return operand === (value !== undefined);
};
},
"$mod": function (operand) {
var divisor = operand[0],
remainder = operand[1];
return function (value) {
return _anyIfArray(value, function (x) {
return x % divisor === remainder;
});
};
},
"$size": function (operand) {
return function (value) {
return isArray(value) && operand === value.length;
};
},
"$type": function (operand) {
return function (value) {
// A nonexistent field is of no type.
if (value === undefined)
return false;
// Definitely not _anyIfArrayPlus: $type: 4 only matches arrays that have
// arrays as elements according to the Mongo docs.
return _anyIfArray(value, function (x) {
return LocalCollection._f._type(x) === operand;
});
};
},
"$regex": function (operand, options) {
if (options !== undefined) {
// Options passed in $options (even the empty string) always overrides
// options in the RegExp object itself.
// Be clear that we only support the JS-supported options, not extended
// ones (eg, Mongo supports x and s). Ideally we would implement x and s
// by transforming the regexp, but not today...
if (/[^gim]/.test(options))
throw new Error("Only the i, m, and g regexp options are supported");
var regexSource = operand instanceof RegExp ? operand.source : operand;
operand = new RegExp(regexSource, options);
} else if (!(operand instanceof RegExp)) {
operand = new RegExp(operand);
}
return function (value) {
if (value === undefined)
return false;
return _anyIfArray(value, function (x) {
return operand.test(x);
});
};
},
"$options": function (operand) {
// evaluation happens at the $regex function above
return function (value) { return true; };
},
"$elemMatch": function (operand) {
var matcher = compileDocumentSelector(operand);
return function (value) {
if (!isArray(value))
return false;
return _.any(value, function (x) {
return matcher(x);
});
};
},
"$not": function (operand) {
var matcher = compileValueSelector(operand);
return function (value) {
return !matcher(value);
};
},
"$near": function (operand) {
// Always returns true. Must be handled in post-filter/sort/limit
return function (value) {
return true;
}
},
"$geoIntersects": function (operand) {
// Always returns true. Must be handled in post-filter/sort/limit
return function (value) {
return true;
}
}
};
// helpers used by compiled selector code
LocalCollection._f = {
// XXX for _all and _in, consider building 'inquery' at compile time..
_type: function (v) {
if (typeof v === "number")
return 1;
if (typeof v === "string")
return 2;
if (typeof v === "boolean")
return 8;
if (isArray(v))
return 4;
if (v === null)
return 10;
if (v instanceof RegExp)
return 11;
if (typeof v === "function")
// note that typeof(/x/) === "function"
return 13;
if (v instanceof Date)
return 9;
if (EJSON.isBinary(v))
return 5;
if (v instanceof Meteor.Collection.ObjectID)
return 7;
return 3; // object
// XXX support some/all of these:
// 14, symbol
// 15, javascript code with scope
// 16, 18: 32-bit/64-bit integer
// 17, timestamp
// 255, minkey
// 127, maxkey
},
// deep equality test: use for literal document and array matches
_equal: function (a, b) {
return EJSON.equals(a, b, {keyOrderSensitive: true});
},
// maps a type code to a value that can be used to sort values of
// different types
_typeorder: function (t) {
// http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types
// XXX what is the correct sort position for Javascript code?
// ('100' in the matrix below)
// XXX minkey/maxkey
return [-1, // (not a type)
1, // number
2, // string
3, // object
4, // array
5, // binary
-1, // deprecated
6, // ObjectID
7, // bool
8, // Date
0, // null
9, // RegExp
-1, // deprecated
100, // JS code
2, // deprecated (symbol)
100, // JS code
1, // 32-bit int
8, // Mongo timestamp
1 // 64-bit int
][t];
},
// compare two values of unknown type according to BSON ordering
// semantics. (as an extension, consider 'undefined' to be less than
// any other value.) return negative if a is less, positive if b is
// less, or 0 if equal
_cmp: function (a, b) {
if (a === undefined)
return b === undefined ? 0 : -1;
if (b === undefined)
return 1;
var ta = LocalCollection._f._type(a);
var tb = LocalCollection._f._type(b);
var oa = LocalCollection._f._typeorder(ta);
var ob = LocalCollection._f._typeorder(tb);
if (oa !== ob)
return oa < ob ? -1 : 1;
if (ta !== tb)
// XXX need to implement this if we implement Symbol or integers, or
// Timestamp
throw Error("Missing type coercion logic in _cmp");
if (ta === 7) { // ObjectID
// Convert to string.
ta = tb = 2;
a = a.toHexString();
b = b.toHexString();
}
if (ta === 9) { // Date
// Convert to millis.
ta = tb = 1;
a = a.getTime();
b = b.getTime();
}
if (ta === 1) // double
return a - b;
if (tb === 2) // string
return a < b ? -1 : (a === b ? 0 : 1);
if (ta === 3) { // Object
// this could be much more efficient in the expected case ...
var to_array = function (obj) {
var ret = [];
for (var key in obj) {
ret.push(key);
ret.push(obj[key]);
}
return ret;
};
return LocalCollection._f._cmp(to_array(a), to_array(b));
}
if (ta === 4) { // Array
for (var i = 0; ; i++) {
if (i === a.length)
return (i === b.length) ? 0 : -1;
if (i === b.length)
return 1;
var s = LocalCollection._f._cmp(a[i], b[i]);
if (s !== 0)
return s;
}
}
if (ta === 5) { // binary
// Surprisingly, a small binary blob is always less than a large one in
// Mongo.
if (a.length !== b.length)
return a.length - b.length;
for (i = 0; i < a.length; i++) {
if (a[i] < b[i])
return -1;
if (a[i] > b[i])
return 1;
}
return 0;
}
if (ta === 8) { // boolean
if (a) return b ? 0 : 1;
return b ? -1 : 0;
}
if (ta === 10) // null
return 0;
if (ta === 11) // regexp
throw Error("Sorting not supported on regular expression"); // XXX
// 13: javascript code
// 14: symbol
// 15: javascript code with scope
// 16: 32-bit integer
// 17: timestamp
// 18: 64-bit integer
// 255: minkey
// 127: maxkey
if (ta === 13) // javascript code
throw Error("Sorting not supported on Javascript code"); // XXX
throw Error("Unknown type to sort");
}
};
// For unit tests. True if the given document matches the given
// selector.
LocalCollection._matches = function (selector, doc) {
return (LocalCollection._compileSelector(selector))(doc);
};
// _makeLookupFunction(key) returns a lookup function.
//
// A lookup function takes in a document and returns an array of matching
// values. This array has more than one element if any segment of the key other
// than the last one is an array. ie, any arrays found when doing non-final
// lookups result in this function "branching"; each element in the returned
// array represents the value found at this branch. If any branch doesn't have a
// final value for the full key, its element in the returned list will be
// undefined. It always returns a non-empty array.
//
// _makeLookupFunction('a.x')({a: {x: 1}}) returns [1]
// _makeLookupFunction('a.x')({a: {x: [1]}}) returns [[1]]
// _makeLookupFunction('a.x')({a: 5}) returns [undefined]
// _makeLookupFunction('a.x')({a: [{x: 1},
// {x: [2]},
// {y: 3}]})
// returns [1, [2], undefined]
LocalCollection._makeLookupFunction = function (key) {
var dotLocation = key.indexOf('.');
var first, lookupRest, nextIsNumeric;
if (dotLocation === -1) {
first = key;
} else {
first = key.substr(0, dotLocation);
var rest = key.substr(dotLocation + 1);
lookupRest = LocalCollection._makeLookupFunction(rest);
// Is the next (perhaps final) piece numeric (ie, an array lookup?)
nextIsNumeric = /^\d+(\.|$)/.test(rest);
}
return function (doc) {
if (doc == null) // null or undefined
return [undefined];
var firstLevel = doc[first];
// We don't "branch" at the final level.
if (!lookupRest)
return [firstLevel];
// It's an empty array, and we're not done: we won't find anything.
if (isArray(firstLevel) && firstLevel.length === 0)
return [undefined];
// For each result at this level, finish the lookup on the rest of the key,
// and return everything we find. Also, if the next result is a number,
// don't branch here.
//
// Technically, in MongoDB, we should be able to handle the case where
// objects have numeric keys, but Mongo doesn't actually handle this
// consistently yet itself, see eg
// https://jira.mongodb.org/browse/SERVER-2898
// https://github.com/mongodb/mongo/blob/master/jstests/array_match2.js
if (!isArray(firstLevel) || nextIsNumeric)
firstLevel = [firstLevel];
return Array.prototype.concat.apply([], _.map(firstLevel, lookupRest));
};
};
// The main compilation function for a given selector.
var compileDocumentSelector = function (docSelector) {
var perKeySelectors = [];
_.each(docSelector, function (subSelector, key) {
if (key.substr(0, 1) === '$') {
// Outer operators are either logical operators (they recurse back into
// this function), or $where.
if (!_.has(LOGICAL_OPERATORS, key))
throw new Error("Unrecognized logical operator: " + key);
perKeySelectors.push(LOGICAL_OPERATORS[key](subSelector));
} else {
var lookUpByIndex = LocalCollection._makeLookupFunction(key);
var valueSelectorFunc = compileValueSelector(subSelector);
perKeySelectors.push(function (doc) {
var branchValues = lookUpByIndex(doc);
// We apply the selector to each "branched" value and return true if any
// match. This isn't 100% consistent with MongoDB; eg, see:
// https://jira.mongodb.org/browse/SERVER-8585
return _.any(branchValues, valueSelectorFunc);
});
}
});
return function (doc) {
return _.all(perKeySelectors, function (f) {
return f(doc);
});
};
};
// Given a selector, return a function that takes one argument, a
// document, and returns true if the document matches the selector,
// else false.
LocalCollection._compileSelector = function (selector) {
// you can pass a literal function instead of a selector
if (selector instanceof Function)
return function (doc) {return selector.call(doc);};
// shorthand -- scalars match _id
if (LocalCollection._selectorIsId(selector)) {
return function (doc) {
return EJSON.equals(doc._id, selector);
};
}
// protect against dangerous selectors. falsey and {_id: falsey} are both
// likely programmer error, and not what you want, particularly for
// destructive operations.
if (!selector || (('_id' in selector) && !selector._id))
return function (doc) {return false;};
// Top level can't be an array or true or binary.
if (typeof(selector) === 'boolean' || isArray(selector) ||
EJSON.isBinary(selector))
throw new Error("Invalid selector: " + selector);
return compileDocumentSelector(selector);
};
// Give a sort spec, which can be in any of these forms:
// {"key1": 1, "key2": -1}
// [["key1", "asc"], ["key2", "desc"]]
// ["key1", ["key2", "desc"]]
//
// (.. with the first form being dependent on the key enumeration
// behavior of your javascript VM, which usually does what you mean in
// this case if the key names don't look like integers ..)
//
// return a function that takes two objects, and returns -1 if the
// first object comes first in order, 1 if the second object comes
// first, or 0 if neither object comes before the other.
LocalCollection._compileSort = function (spec) {
var sortSpecParts = [];
if (spec instanceof Array) {
for (var i = 0; i < spec.length; i++) {
if (typeof spec[i] === "string") {
sortSpecParts.push({
lookup: LocalCollection._makeLookupFunction(spec[i]),
ascending: true
});
} else {
sortSpecParts.push({
lookup: LocalCollection._makeLookupFunction(spec[i][0]),
ascending: spec[i][1] !== "desc"
});
}
}
} else if (typeof spec === "object") {
for (var key in spec) {
sortSpecParts.push({
lookup: LocalCollection._makeLookupFunction(key),
ascending: spec[key] >= 0
});
}
} else {
throw Error("Bad sort specification: ", JSON.stringify(spec));
}
if (sortSpecParts.length === 0)
return function () {return 0;};
// reduceValue takes in all the possible values for the sort key al