UNPKG

mobdb

Version:

MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6

369 lines (304 loc) 13.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.compileProjection = compileProjection; exports.checkSupportedProjection = checkSupportedProjection; exports.projectionDetails = projectionDetails; exports.pathsToTree = pathsToTree; exports.combineImportantPathsIntoProjection = combineImportantPathsIntoProjection; exports.combineMatcherWithProjection = combineMatcherWithProjection; exports.combineSorterWithProjection = combineSorterWithProjection; var _checkTypes = require('check-types'); var _checkTypes2 = _interopRequireDefault(_checkTypes); var _forEach = require('fast.js/forEach'); var _forEach2 = _interopRequireDefault(_forEach); var _keys2 = require('fast.js/object/keys'); var _keys3 = _interopRequireDefault(_keys2); var _assign2 = require('fast.js/object/assign'); var _assign3 = _interopRequireDefault(_assign2); var _every2 = require('fast.js/array/every'); var _every3 = _interopRequireDefault(_every2); var _filter2 = require('fast.js/array/filter'); var _filter3 = _interopRequireDefault(_filter2); var _map2 = require('fast.js/map'); var _map3 = _interopRequireDefault(_map2); var _indexOf2 = require('fast.js/array/indexOf'); var _indexOf3 = _interopRequireDefault(_indexOf2); var _EJSON = require('./EJSON'); var _EJSON2 = _interopRequireDefault(_EJSON); var _Document = require('./Document'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // Internals function _has(obj, key) { return _checkTypes2.default.object(obj) && obj.hasOwnProperty(key); } /** * A wrapper around pojection functions. */ var DocumentProjector = function () { function DocumentProjector(fields) { _classCallCheck(this, DocumentProjector); this.fields = fields; this._projector = compileProjection(fields); } _createClass(DocumentProjector, [{ key: 'project', value: function project(docs) { var _this = this; if (_checkTypes2.default.array(docs)) { return (0, _map3.default)(docs, function (doc) { return _this._projector(doc); }); } else { return this._projector(docs); } } }]); return DocumentProjector; }(); // Knows how to compile a fields projection to a predicate function. // @returns - Function: a closure that filters out an object according to the // fields projection rules: // @param obj - Object: MongoDB-styled document // @returns - Object: a document with the fields filtered out // according to projection rules. Doesn't retain subfields // of passed argument. exports.default = DocumentProjector; function compileProjection(fields) { checkSupportedProjection(fields); var _idProjection = fields._id === undefined ? true : fields._id; var details = projectionDetails(fields); // returns transformed doc according to ruleTree var transform = function transform(doc, ruleTree) { // Special case for 'sets' if (_checkTypes2.default.array(doc)) { return (0, _map3.default)(doc, function (subdoc) { return transform(subdoc, ruleTree); }); } var res = details.including ? {} : _EJSON2.default.clone(doc); (0, _forEach2.default)(ruleTree, function (rule, key) { if (!_has(doc, key)) { return; } if (_checkTypes2.default.object(rule)) { // For sub-objects/subsets we branch if (_checkTypes2.default.object(doc[key]) || _checkTypes2.default.array(doc[key])) { res[key] = transform(doc[key], rule); } // Otherwise we don't even touch this subfield } else if (details.including) { res[key] = _EJSON2.default.clone(doc[key]); } else { delete res[key]; } }); return res; }; return function (obj) { var res = transform(obj, details.tree); if (_idProjection && _has(obj, '_id')) { res._id = obj._id; } if (!_idProjection && _has(res, '_id')) { delete res._id; } return res; }; } // Rise an exception if fields object contains // some unsupported fields or values function checkSupportedProjection(fields) { if (!_checkTypes2.default.object(fields) || _checkTypes2.default.array(fields)) { throw Error('fields option must be an object'); } (0, _forEach2.default)(fields, function (val, keyPath) { var valKeys = _checkTypes2.default.object(val) && (0, _keys3.default)(val) || []; if ((0, _indexOf3.default)(keyPath.split('.'), '$') >= 0) { throw Error('Minimongo doesn\'t support $ operator in projections yet.'); } if ((typeof val === 'undefined' ? 'undefined' : _typeof(val)) === 'object' && ((0, _indexOf3.default)(valKeys, '$elemMatch') >= 0 || (0, _indexOf3.default)(valKeys, '$meta') >= 0 || (0, _indexOf3.default)(valKeys, '$slice') >= 0)) { throw Error('Minimongo doesn\'t support operators in projections yet.'); } if ((0, _indexOf3.default)([1, 0, true, false], val) === -1) { throw Error('Projection values should be one of 1, 0, true, or false'); } }); } // Traverses the keys of passed projection and constructs a tree where all // leaves are either all True or all False // @returns Object: // - tree - Object - tree representation of keys involved in projection // (exception for '_id' as it is a special case handled separately) // - including - Boolean - 'take only certain fields' type of projection function projectionDetails(fields) { // Find the non-_id keys (_id is handled specially because it is included unless // explicitly excluded). Sort the keys, so that our code to detect overlaps // like 'foo' and 'foo.bar' can assume that 'foo' comes first. var fieldsKeys = (0, _keys3.default)(fields).sort(); // If _id is the only field in the projection, do not remove it, since it is // required to determine if this is an exclusion or exclusion. Also keep an // inclusive _id, since inclusive _id follows the normal rules about mixing // inclusive and exclusive fields. If _id is not the only field in the // projection and is exclusive, remove it so it can be handled later by a // special case, since exclusive _id is always allowed. if (fieldsKeys.length > 0 && !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && !((0, _indexOf3.default)(fieldsKeys, '_id') >= 0 && fields._id)) { fieldsKeys = (0, _filter3.default)(fieldsKeys, function (key) { return key !== '_id'; }); } var including = null; // Unknown (0, _forEach2.default)(fieldsKeys, function (keyPath) { var rule = !!fields[keyPath]; if (including === null) { including = rule; } if (including !== rule) { // This error message is copied from MongoDB shell throw Error('You cannot currently mix including and excluding fields.'); } }); var projectionRulesTree = pathsToTree(fieldsKeys, function (path) { return including; }, function (node, path, fullPath) { // Check passed projection fields' keys: If you have two rules such as // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If // that happens, there is a probability you are doing something wrong, // framework should notify you about such mistake earlier on cursor // compilation step than later during runtime. Note, that real mongo // doesn't do anything about it and the later rule appears in projection // project, more priority it takes. // // Example, assume following in mongo shell: // > db.coll.insert({ a: { b: 23, c: 44 } }) // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) // { '_id' : ObjectId('520bfe456024608e8ef24af3'), 'a' : { 'b' : 23 } } // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) // { '_id' : ObjectId('520bfe456024608e8ef24af3'), 'a' : { 'b' : 23, 'c' : 44 } } // // Note, how second time the return set of keys is different. var currentPath = fullPath; var anotherPath = path; throw Error('both ' + currentPath + ' and ' + anotherPath + ' found in fields option, using both of them may trigger ' + 'unexpected behavior. Did you mean to use only one of them?'); }); return { tree: projectionRulesTree, including: including }; } // paths - Array: list of mongo style paths // newLeafFn - Function: of form function(path) should return a scalar value to // put into list created for that path // conflictFn - Function: of form function(node, path, fullPath) is called // when building a tree path for 'fullPath' node on // 'path' was already a leaf with a value. Must return a // conflict resolution. // initial tree - Optional Object: starting tree. // @returns - Object: tree represented as a set of nested objects function pathsToTree(paths, newLeafFn, conflictFn, tree) { tree = tree || {}; (0, _forEach2.default)(paths, function (keyPath) { var treePos = tree; var pathArr = keyPath.split('.'); // use _.all just for iteration with break var success = (0, _every3.default)(pathArr.slice(0, -1), function (key, idx) { if (!_has(treePos, key)) { treePos[key] = {}; } else if (!_checkTypes2.default.object(treePos[key])) { treePos[key] = conflictFn(treePos[key], pathArr.slice(0, idx + 1).join('.'), keyPath); // break out of loop if we are failing for this path if (!_checkTypes2.default.object(treePos[key])) { return false; } } treePos = treePos[key]; return true; }); if (success) { var lastKey = pathArr[pathArr.length - 1]; if (!_has(treePos, lastKey)) { treePos[lastKey] = newLeafFn(keyPath); } else { treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); } } }); return tree; } // By given paths array and projection object returns // new projection object combined with paths. function combineImportantPathsIntoProjection(paths, projection) { var prjDetails = projectionDetails(projection); var tree = prjDetails.tree; var mergedProjection = {}; // merge the paths to include tree = pathsToTree(paths, function (path) { return true; }, function (node, path, fullPath) { return true; }, tree); mergedProjection = treeToPaths(tree); if (prjDetails.including) { // both selector and projection are pointing on fields to include // so we can just return the merged tree return mergedProjection; } else { // selector is pointing at fields to include // projection is pointing at fields to exclude // make sure we don't exclude important paths var mergedExclProjection = {}; (0, _forEach2.default)(mergedProjection, function (incl, path) { if (!incl) { mergedExclProjection[path] = false; } }); return mergedExclProjection; } } // Knows how to combine a mongo selector and a fields projection to a new fields // projection taking into account active fields from the passed selector. // @returns Object - projection object (same as fields option of mongo cursor) function combineMatcherWithProjection(matcher, projection) { var selectorPaths = _pathsElidingNumericKeys(matcher._getPaths()); // Special case for $where operator in the selector - projection should depend // on all fields of the document. getSelectorPaths returns a list of paths // selector depends on. If one of the paths is '' (empty string) representing // the root or the whole document, complete projection should be returned. if ((0, _indexOf3.default)(selectorPaths, '') >= 0) { return {}; } return combineImportantPathsIntoProjection(selectorPaths, projection); } // Knows how to combine a mongo selector and a fields projection to a new fields // projection taking into account active fields from the passed selector. // @returns Object - projection object (same as fields option of mongo cursor) function combineSorterWithProjection(sorter, projection) { var specPaths = _pathsElidingNumericKeys(sorter._getPaths()); return combineImportantPathsIntoProjection(specPaths, projection); } // Internal utils var _pathsElidingNumericKeys = function _pathsElidingNumericKeys(paths) { return (0, _map3.default)(paths, function (path) { return (0, _filter3.default)(path.split('.'), function (k) { return !(0, _Document.isNumericKey)(k); }).join('.'); }); }; // Returns a set of key paths similar to // { 'foo.bar': 1, 'a.b.c': 1 } var treeToPaths = function treeToPaths(tree, prefix) { prefix = prefix || ''; var result = {}; (0, _forEach2.default)(tree, function (val, key) { if (_checkTypes2.default.object(val)) { (0, _assign3.default)(result, treeToPaths(val, prefix + key + '.')); } else { result[prefix + key] = val; } }); return result; };