├── .eslintignore ├── test ├── node │ ├── .gitkeep │ └── Performance.js ├── browser │ └── .gitkeep └── both │ ├── debounce.test.js │ ├── Base64.test.js │ ├── Random.test.js │ ├── PromiseQueue.test.js │ ├── StorageManager.test.js │ ├── DocumentProjector.test.js │ ├── DocumentRetriver.test.js │ └── EJSON.test.js ├── babelhook.js ├── .istanbul.yml ├── .babelrc ├── gulp ├── tasks │ ├── default.js │ ├── clean.js │ ├── dev.js │ ├── release.js │ ├── lint.js │ └── build.js └── config.js ├── gulpfile.js ├── browser_tests.js ├── .editorconfig ├── .travis.yml ├── .gitignore ├── .tern-project ├── browser_tests.html ├── lib ├── cursor-processors │ ├── aggregate.js │ ├── sortFunc.js │ ├── map.js │ ├── filter.js │ ├── ifNotEmpty.js │ ├── reduce.js │ ├── joinEach.js │ ├── join.js │ ├── joinAll.js │ └── joinObj.js ├── ShortIdGenerator.js ├── CollectionIndex.js ├── debounce.js ├── StorageManager.js ├── CollectionDelegate.js ├── AsyncEventEmitter.js ├── PromiseQueue.js ├── Base64.js ├── DocumentRetriver.js ├── IndexManager.js ├── Cursor.js ├── Random.js ├── Document.js └── CursorObservable.js ├── polyfills.js ├── .eslintrc ├── dist ├── ShortIdGenerator.js ├── cursor-processors │ ├── sortFunc.js │ ├── aggregate.js │ ├── map.js │ ├── ifNotEmpty.js │ ├── filter.js │ ├── reduce.js │ ├── join.js │ ├── joinEach.js │ ├── joinAll.js │ └── joinObj.js ├── debounce.js ├── CollectionIndex.js ├── StorageManager.js ├── PromiseQueue.js ├── Base64.js ├── AsyncEventEmitter.js ├── CollectionDelegate.js ├── DocumentRetriver.js ├── Document.js ├── IndexManager.js └── Random.js ├── bower.json ├── index.js ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/node/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/browser/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/node/Performance.js: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /babelhook.js: -------------------------------------------------------------------------------- 1 | require("babel-register")(); -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: ./lib -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } -------------------------------------------------------------------------------- /gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | gulp.task('default', ['dev']); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var requireDir = require('require-dir'); 4 | requireDir('./gulp/tasks', { recurse: true }); -------------------------------------------------------------------------------- /browser_tests.js: -------------------------------------------------------------------------------- 1 | require('./polyfills'); 2 | var bulk = require('bulk-require'); 3 | 4 | 5 | mocha.ui('bdd'); 6 | mocha.reporter('html'); 7 | var tests = bulk(__dirname, ['./test/both/*.test.js', './test/browser/*.test.js']); 8 | mocha.run(); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../config'); 4 | var gulp = require('gulp'); 5 | var del = require('del'); 6 | 7 | gulp.task('clean', function(cb) { 8 | del([config.dist, config.build]).then(function (paths) { 9 | cb(); 10 | }); 11 | }); -------------------------------------------------------------------------------- /gulp/tasks/dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var runSequence = require('run-sequence'); 5 | 6 | gulp.task('dev', ['clean'], function(cb) { 7 | cb = cb || function() {}; 8 | global.isProd = true; 9 | process.env.NODE_ENV = 'production'; 10 | runSequence('build', 'lint', 'watch', cb); 11 | }); -------------------------------------------------------------------------------- /gulp/tasks/release.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var runSequence = require('run-sequence'); 5 | 6 | gulp.task('release', ['clean'], function(cb) { 7 | cb = cb || function() {}; 8 | global.isProd = true; 9 | process.env.NODE_ENV = 'production'; 10 | runSequence('build', 'lint', cb); 11 | }); -------------------------------------------------------------------------------- /gulp/tasks/lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../config'); 4 | var gulp = require('gulp'); 5 | var eslint = require('gulp-eslint'); 6 | 7 | gulp.task('lint', function() { 8 | return gulp.src([config.src]) 9 | .pipe(eslint()) 10 | .pipe(eslint.format()) 11 | //.pipe(eslint.failOnError()); 12 | }); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | install: 6 | - npm install -g babel-cli 7 | - npm install 8 | 9 | node_js: 10 | - "5.3" 11 | - "4.2" 12 | - "0.12" 13 | 14 | cache: 15 | directories: 16 | - node_modules 17 | 18 | script: npm test && npm run test_browser 19 | 20 | after_script: npm run coveralls 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | workspace 18 | node_modules 19 | coverage 20 | 21 | browser-version/src 22 | browser-version/node_modules 23 | build/browser_tests.js 24 | 25 | *.sublime-workspace 26 | *.swp 27 | *~ 28 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 6, 3 | "libs": [ 4 | "browser", 5 | "chai" 6 | ], 7 | "loadEagerly": [ 8 | "lib/**/*.js", 9 | "test/**/*.js" 10 | ], 11 | "plugins": { 12 | "complete_strings": {}, 13 | "node": {}, 14 | "modules": {}, 15 | "es_modules": {}, 16 | "doc_comment": { 17 | "fullDocs": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /gulp/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | src: 'lib/**/*', 5 | dist: 'dist', 6 | build: 'build', 7 | 8 | browser: { 9 | bundleName: 'marsdb.js', 10 | bundleMinName: 'marsdb.min.js', 11 | bundlePolyfillsName: 'marsdb.polyfills.js', 12 | entry: 'index.js', 13 | entryTests: 'browser_tests.js', 14 | entryPolyfills: 'polyfills.js', 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /browser_tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/cursor-processors/aggregate.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | 4 | export const aggregate = { 5 | method: function(aggrFn) { 6 | invariant( 7 | typeof aggrFn === 'function', 8 | 'aggregate(...): aggregator must be a function' 9 | ); 10 | 11 | this._addPipeline('aggregate', aggrFn); 12 | return this; 13 | }, 14 | 15 | process: function(docs, pipeObj) { 16 | return pipeObj.value(docs); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/cursor-processors/sortFunc.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | 4 | export const sortFunc = { 5 | method: function(sortFn) { 6 | invariant( 7 | typeof sortFn === 'function', 8 | 'sortFunc(...): argument must be a function' 9 | ); 10 | 11 | this._addPipeline('sortFunc', sortFn); 12 | return this; 13 | }, 14 | 15 | process: function(docs, pipeObj) { 16 | return docs.sort(pipeObj.value); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof window !== 'undefined') { 2 | window.Symbol = require("core-js/es6/symbol"); 3 | window.Promise = require("core-js/es6/promise"); 4 | window.Set = require("core-js/es6/set"); 5 | window.Map = require("core-js/es6/map"); 6 | } else { 7 | global.Symbol = require("core-js/es6/symbol"); 8 | global.Promise = require("core-js/es6/promise"); 9 | global.Set = require("core-js/es6/set"); 10 | global.Map = require("core-js/es6/map"); 11 | } -------------------------------------------------------------------------------- /lib/cursor-processors/map.js: -------------------------------------------------------------------------------- 1 | import _map from 'fast.js/map'; 2 | import invariant from 'invariant'; 3 | 4 | 5 | export const map = { 6 | method: function(mapperFn) { 7 | invariant( 8 | typeof mapperFn === 'function', 9 | 'map(...): mapper must be a function' 10 | ); 11 | 12 | this._addPipeline('map', mapperFn); 13 | return this; 14 | }, 15 | 16 | process: function(docs, pipeObj) { 17 | return _map(docs, pipeObj.value); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/ShortIdGenerator.js: -------------------------------------------------------------------------------- 1 | import Random from './Random'; 2 | 3 | 4 | /** 5 | * Generates a random short obhect id for given collection 6 | * @param {String} modelName 7 | * @return {String} 8 | */ 9 | export default function(modelName) { 10 | const nextSeed = Random.default().hexString(20); 11 | const sequenceSeed = [nextSeed, `/collection/${modelName}`]; 12 | return { 13 | value: Random.createWithSeeds.apply(null, sequenceSeed).id(17), 14 | seed: nextSeed, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /lib/cursor-processors/filter.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import _filter from 'fast.js/array/filter'; 3 | 4 | 5 | export const filter = { 6 | method: function(filterFn) { 7 | invariant( 8 | typeof filterFn === 'function', 9 | 'filter(...): argument must be a function' 10 | ); 11 | 12 | this._addPipeline('filter', filterFn); 13 | return this; 14 | }, 15 | 16 | process: function(docs, pipeObj) { 17 | return _filter(docs, pipeObj.value); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/cursor-processors/ifNotEmpty.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | 3 | 4 | export const ifNotEmpty = { 5 | method: function() { 6 | this._addPipeline('ifNotEmpty'); 7 | return this; 8 | }, 9 | 10 | process: function(docs) { 11 | const isEmptyRes = ( 12 | !_check.assigned(docs) || 13 | (_check.array(docs) && _check.emptyArray(docs)) || 14 | (_check.object(docs) && _check.emptyObject(docs)) 15 | ); 16 | return isEmptyRes ? '___[STOP]___' : docs; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/cursor-processors/reduce.js: -------------------------------------------------------------------------------- 1 | import _reduce from 'fast.js/array/reduce'; 2 | import invariant from 'invariant'; 3 | 4 | 5 | export const reduce = { 6 | method: function(reduceFn, initial) { 7 | invariant( 8 | typeof reduceFn === 'function', 9 | 'reduce(...): reducer argument must be a function' 10 | ); 11 | 12 | this._addPipeline('reduce', reduceFn, initial); 13 | return this; 14 | }, 15 | 16 | process: function(docs, pipeObj) { 17 | return _reduce(docs, pipeObj.value, pipeObj.args[0]); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | 4 | extends: 5 | - ./node_modules/fbjs-scripts/eslint/.eslintrc 6 | 7 | rules: 8 | accessor-pairs: 0 9 | brace-style: [2, 1tbs] 10 | comma-dangle: [2, always-multiline] 11 | consistent-return: 2 12 | dot-location: [2, property] 13 | dot-notation: 2 14 | eol-last: 2 15 | indent: [2, 2, {SwitchCase: 1}] 16 | no-bitwise: 0 17 | no-multi-spaces: 2 18 | no-shadow: 2 19 | no-unused-expressions: 2 20 | no-unused-vars: [2, {args: none}] 21 | quotes: [2, single, avoid-escape] 22 | space-after-keywords: 2 23 | space-before-blocks: 2 24 | strict: [2, never] -------------------------------------------------------------------------------- /dist/ShortIdGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (modelName) { 8 | var nextSeed = _Random2.default.default().hexString(20); 9 | var sequenceSeed = [nextSeed, '/collection/' + modelName]; 10 | return { 11 | value: _Random2.default.createWithSeeds.apply(null, sequenceSeed).id(17), 12 | seed: nextSeed 13 | }; 14 | }; 15 | 16 | var _Random = require('./Random'); 17 | 18 | var _Random2 = _interopRequireDefault(_Random); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /dist/cursor-processors/sortFunc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.sortFunc = undefined; 7 | 8 | var _invariant = require('invariant'); 9 | 10 | var _invariant2 = _interopRequireDefault(_invariant); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var sortFunc = exports.sortFunc = { 15 | method: function method(sortFn) { 16 | (0, _invariant2.default)(typeof sortFn === 'function', 'sortFunc(...): argument must be a function'); 17 | 18 | this._addPipeline('sortFunc', sortFn); 19 | return this; 20 | }, 21 | 22 | process: function process(docs, pipeObj) { 23 | return docs.sort(pipeObj.value); 24 | } 25 | }; -------------------------------------------------------------------------------- /dist/cursor-processors/aggregate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.aggregate = undefined; 7 | 8 | var _invariant = require('invariant'); 9 | 10 | var _invariant2 = _interopRequireDefault(_invariant); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var aggregate = exports.aggregate = { 15 | method: function method(aggrFn) { 16 | (0, _invariant2.default)(typeof aggrFn === 'function', 'aggregate(...): aggregator must be a function'); 17 | 18 | this._addPipeline('aggregate', aggrFn); 19 | return this; 20 | }, 21 | 22 | process: function process(docs, pipeObj) { 23 | return pipeObj.value(docs); 24 | } 25 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marsdb", 3 | "description": "MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6", 4 | "main": "index.js", 5 | "authors": [ 6 | "Artem Artemev" 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "mongo", 11 | "minimongo", 12 | "embedded", 13 | "levelup", 14 | "localstorage", 15 | "db", 16 | "database", 17 | "meteor" 18 | ], 19 | "homepage": "https://github.com/c58/marsdb", 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:c58/marsdb.git" 23 | }, 24 | "moduleType": [ 25 | "amd", 26 | "globals", 27 | "node" 28 | ], 29 | "ignore": [ 30 | "**/.*", 31 | "node_modules", 32 | "bower_components", 33 | "test", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /lib/cursor-processors/joinEach.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | import _map from 'fast.js/map'; 3 | import invariant from 'invariant'; 4 | import { joinAll } from './joinAll'; 5 | 6 | 7 | export const joinEach = { 8 | method: function(joinFn) { 9 | invariant( 10 | typeof joinFn === 'function', 11 | 'joinEach(...): argument must be a function' 12 | ); 13 | 14 | this._addPipeline('joinEach', joinFn); 15 | return this; 16 | }, 17 | 18 | process: function(docs, pipeObj, cursor) { 19 | if (!docs) { 20 | return Promise.resolve(docs); 21 | } else { 22 | docs = _check.array(docs) ? docs : [docs]; 23 | const docsLength = docs.length; 24 | return Promise.all(_map(docs, (x, i) => 25 | joinAll.process(x, pipeObj, cursor, i, docsLength) 26 | )); 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /dist/cursor-processors/map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.map = undefined; 7 | 8 | var _map2 = require('fast.js/map'); 9 | 10 | var _map3 = _interopRequireDefault(_map2); 11 | 12 | var _invariant = require('invariant'); 13 | 14 | var _invariant2 = _interopRequireDefault(_invariant); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | var map = exports.map = { 19 | method: function method(mapperFn) { 20 | (0, _invariant2.default)(typeof mapperFn === 'function', 'map(...): mapper must be a function'); 21 | 22 | this._addPipeline('map', mapperFn); 23 | return this; 24 | }, 25 | 26 | process: function process(docs, pipeObj) { 27 | return (0, _map3.default)(docs, pipeObj.value); 28 | } 29 | }; -------------------------------------------------------------------------------- /dist/cursor-processors/ifNotEmpty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.ifNotEmpty = undefined; 7 | 8 | var _checkTypes = require('check-types'); 9 | 10 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var ifNotEmpty = exports.ifNotEmpty = { 15 | method: function method() { 16 | this._addPipeline('ifNotEmpty'); 17 | return this; 18 | }, 19 | 20 | process: function process(docs) { 21 | var isEmptyRes = !_checkTypes2.default.assigned(docs) || _checkTypes2.default.array(docs) && _checkTypes2.default.emptyArray(docs) || _checkTypes2.default.object(docs) && _checkTypes2.default.emptyObject(docs); 22 | return isEmptyRes ? '___[STOP]___' : docs; 23 | } 24 | }; -------------------------------------------------------------------------------- /dist/cursor-processors/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.filter = undefined; 7 | 8 | var _invariant = require('invariant'); 9 | 10 | var _invariant2 = _interopRequireDefault(_invariant); 11 | 12 | var _filter2 = require('fast.js/array/filter'); 13 | 14 | var _filter3 = _interopRequireDefault(_filter2); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | var filter = exports.filter = { 19 | method: function method(filterFn) { 20 | (0, _invariant2.default)(typeof filterFn === 'function', 'filter(...): argument must be a function'); 21 | 22 | this._addPipeline('filter', filterFn); 23 | return this; 24 | }, 25 | 26 | process: function process(docs, pipeObj) { 27 | return (0, _filter3.default)(docs, pipeObj.value); 28 | } 29 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('./dist/AsyncEventEmitter').default; 2 | var Collection = require('./dist/Collection').default; 3 | var CursorObservable = require('./dist/CursorObservable').default; 4 | var debounce = require('./dist/debounce').default; 5 | var StorageManager = require('./dist/StorageManager').default; 6 | var Random = require('./dist/Random').default; 7 | var EJSON = require('./dist/EJSON').default; 8 | var Base64 = require('./dist/Base64').default; 9 | var PromiseQueue = require('./dist/PromiseQueue').default; 10 | 11 | 12 | module.exports = { 13 | __esModule: true, 14 | default: Collection, 15 | Random: Random, 16 | EJSON: EJSON, 17 | Base64: Base64, 18 | Collection: Collection, 19 | CursorObservable: CursorObservable, 20 | StorageManager: StorageManager, 21 | EventEmitter: EventEmitter, 22 | PromiseQueue: PromiseQueue, 23 | debounce: debounce 24 | }; 25 | -------------------------------------------------------------------------------- /lib/cursor-processors/join.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | import invariant from 'invariant'; 3 | import { joinObj } from './joinObj'; 4 | import { joinEach } from './joinEach'; 5 | import { joinAll } from './joinAll'; 6 | 7 | 8 | export const join = { 9 | method: function(joinFn, options = {}) { 10 | invariant( 11 | typeof joinFn === 'function' || _check.object(joinFn), 12 | 'join(...): argument must be a function' 13 | ); 14 | 15 | this._addPipeline('join', joinFn, options); 16 | return this; 17 | }, 18 | 19 | process: function(docs, pipeObj, cursor) { 20 | if (_check.object(pipeObj.value)) { 21 | return joinObj.process(docs, pipeObj, cursor); 22 | } else if (_check.array(docs)) { 23 | return joinEach.process(docs, pipeObj, cursor); 24 | } else { 25 | return joinAll.process(docs, pipeObj, cursor); 26 | } 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /dist/cursor-processors/reduce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.reduce = undefined; 7 | 8 | var _reduce2 = require('fast.js/array/reduce'); 9 | 10 | var _reduce3 = _interopRequireDefault(_reduce2); 11 | 12 | var _invariant = require('invariant'); 13 | 14 | var _invariant2 = _interopRequireDefault(_invariant); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | var reduce = exports.reduce = { 19 | method: function method(reduceFn, initial) { 20 | (0, _invariant2.default)(typeof reduceFn === 'function', 'reduce(...): reducer argument must be a function'); 21 | 22 | this._addPipeline('reduce', reduceFn, initial); 23 | return this; 24 | }, 25 | 26 | process: function process(docs, pipeObj) { 27 | return (0, _reduce3.default)(docs, pipeObj.value, pipeObj.args[0]); 28 | } 29 | }; -------------------------------------------------------------------------------- /lib/CollectionIndex.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | 4 | export class CollectionIndex { 5 | constructor(options = {}) { 6 | invariant( 7 | options.fieldName, 8 | 'CollectionIndex(...): you must specify a "feildName" option' 9 | ); 10 | invariant( 11 | !Array.isArray(options.fieldName), 12 | 'CollectionIndex(...): compound index is not supported yet' 13 | ); 14 | 15 | this.fieldName = options.fieldName; 16 | this.unique = options.unique || false; 17 | this.sparse = options.sparse || false; 18 | 19 | this.reset(); 20 | } 21 | 22 | reset() { 23 | // TODO 24 | } 25 | 26 | insert(doc) { 27 | // TODO 28 | } 29 | 30 | remove(doc) { 31 | // TODO 32 | } 33 | 34 | update(oldDoc, newDoc) { 35 | // TODO 36 | } 37 | 38 | getMatching(value) { 39 | // TODO 40 | } 41 | 42 | getBetweenBounds(query) { 43 | // TODO 44 | } 45 | 46 | getAll(options) { 47 | // TODO 48 | } 49 | } 50 | 51 | export default CollectionIndex; 52 | -------------------------------------------------------------------------------- /test/both/debounce.test.js: -------------------------------------------------------------------------------- 1 | import Collection from '../../lib/Collection'; 2 | import debounce from '../../lib/debounce'; 3 | import chai, {expect} from 'chai'; 4 | import sinon from 'sinon'; 5 | chai.use(require('chai-as-promised')); 6 | chai.use(require('sinon-chai')); 7 | chai.should(); 8 | 9 | 10 | describe('debounce', function () { 11 | it('should return a promise when calls count out of batch size', function () { 12 | const cb = sinon.spy(); 13 | const debouncedCb = debounce(cb, 100, 2); 14 | let res; 15 | 16 | res = debouncedCb(); 17 | res.should.be.an.instanceof(Promise); 18 | cb.should.have.been.callCount(0); 19 | res = debouncedCb(); 20 | res.should.be.an.instanceof(Promise); 21 | cb.should.have.been.callCount(0); 22 | res = debouncedCb(); 23 | res.should.be.an.instanceof(Promise); 24 | cb.should.have.been.callCount(1); 25 | res = debouncedCb(); 26 | res.should.be.an.instanceof(Promise); 27 | cb.should.have.been.callCount(1); 28 | res = debouncedCb(); 29 | res.should.be.an.instanceof(Promise); 30 | cb.should.have.been.callCount(2); 31 | res = debouncedCb(); 32 | res.should.be.an.instanceof(Promise); 33 | cb.should.have.been.callCount(2); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/cursor-processors/joinAll.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | import _map from 'fast.js/map'; 3 | import _bind from 'fast.js/function/bind'; 4 | import invariant from 'invariant'; 5 | 6 | 7 | export const joinAll = { 8 | method: function(joinFn) { 9 | invariant( 10 | typeof joinFn === 'function', 11 | 'joinAll(...): argument must be a function' 12 | ); 13 | 14 | this._addPipeline('joinAll', joinFn); 15 | return this; 16 | }, 17 | 18 | process: function(docs, pipeObj, cursor, i = 0, len = 1) { 19 | const updatedFn = (cursor._propagateUpdate) 20 | ? _bind(cursor._propagateUpdate, cursor) 21 | : function() {}; 22 | 23 | let res = pipeObj.value(docs, updatedFn, i, len); 24 | res = _check.array(res) ? res : [res]; 25 | res = _map(res, val => { 26 | let cursorPromise; 27 | if (val && val.joinAll) { // instanceof Cursor 28 | cursorPromise = val.exec(); 29 | } else if (_check.object(val) && val.cursor && val.then) { 30 | cursorPromise = val; 31 | } 32 | if (cursorPromise) { 33 | cursor._trackChildCursorPromise(cursorPromise); 34 | } 35 | return cursorPromise || val; 36 | }); 37 | 38 | return Promise.all(res).then(() => docs); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /dist/cursor-processors/join.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.join = undefined; 7 | 8 | var _checkTypes = require('check-types'); 9 | 10 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 11 | 12 | var _invariant = require('invariant'); 13 | 14 | var _invariant2 = _interopRequireDefault(_invariant); 15 | 16 | var _joinObj = require('./joinObj'); 17 | 18 | var _joinEach = require('./joinEach'); 19 | 20 | var _joinAll = require('./joinAll'); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | var join = exports.join = { 25 | method: function method(joinFn) { 26 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 27 | 28 | (0, _invariant2.default)(typeof joinFn === 'function' || _checkTypes2.default.object(joinFn), 'join(...): argument must be a function'); 29 | 30 | this._addPipeline('join', joinFn, options); 31 | return this; 32 | }, 33 | 34 | process: function process(docs, pipeObj, cursor) { 35 | if (_checkTypes2.default.object(pipeObj.value)) { 36 | return _joinObj.joinObj.process(docs, pipeObj, cursor); 37 | } else if (_checkTypes2.default.array(docs)) { 38 | return _joinEach.joinEach.process(docs, pipeObj, cursor); 39 | } else { 40 | return _joinAll.joinAll.process(docs, pipeObj, cursor); 41 | } 42 | } 43 | }; -------------------------------------------------------------------------------- /dist/cursor-processors/joinEach.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.joinEach = undefined; 9 | 10 | var _checkTypes = require('check-types'); 11 | 12 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 13 | 14 | var _map2 = require('fast.js/map'); 15 | 16 | var _map3 = _interopRequireDefault(_map2); 17 | 18 | var _invariant = require('invariant'); 19 | 20 | var _invariant2 = _interopRequireDefault(_invariant); 21 | 22 | var _joinAll = require('./joinAll'); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | var joinEach = exports.joinEach = { 27 | method: function method(joinFn) { 28 | (0, _invariant2.default)(typeof joinFn === 'function', 'joinEach(...): argument must be a function'); 29 | 30 | this._addPipeline('joinEach', joinFn); 31 | return this; 32 | }, 33 | 34 | process: function process(docs, pipeObj, cursor) { 35 | if (!docs) { 36 | return Promise.resolve(docs); 37 | } else { 38 | var _ret = function () { 39 | docs = _checkTypes2.default.array(docs) ? docs : [docs]; 40 | var docsLength = docs.length; 41 | return { 42 | v: Promise.all((0, _map3.default)(docs, function (x, i) { 43 | return _joinAll.joinAll.process(x, pipeObj, cursor, i, docsLength); 44 | })) 45 | }; 46 | }(); 47 | 48 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 49 | } 50 | } 51 | }; -------------------------------------------------------------------------------- /dist/cursor-processors/joinAll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.joinAll = undefined; 7 | 8 | var _checkTypes = require('check-types'); 9 | 10 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 11 | 12 | var _map2 = require('fast.js/map'); 13 | 14 | var _map3 = _interopRequireDefault(_map2); 15 | 16 | var _bind2 = require('fast.js/function/bind'); 17 | 18 | var _bind3 = _interopRequireDefault(_bind2); 19 | 20 | var _invariant = require('invariant'); 21 | 22 | var _invariant2 = _interopRequireDefault(_invariant); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | var joinAll = exports.joinAll = { 27 | method: function method(joinFn) { 28 | (0, _invariant2.default)(typeof joinFn === 'function', 'joinAll(...): argument must be a function'); 29 | 30 | this._addPipeline('joinAll', joinFn); 31 | return this; 32 | }, 33 | 34 | process: function process(docs, pipeObj, cursor) { 35 | var i = arguments.length <= 3 || arguments[3] === undefined ? 0 : arguments[3]; 36 | var len = arguments.length <= 4 || arguments[4] === undefined ? 1 : arguments[4]; 37 | 38 | var updatedFn = cursor._propagateUpdate ? (0, _bind3.default)(cursor._propagateUpdate, cursor) : function () {}; 39 | 40 | var res = pipeObj.value(docs, updatedFn, i, len); 41 | res = _checkTypes2.default.array(res) ? res : [res]; 42 | res = (0, _map3.default)(res, function (val) { 43 | var cursorPromise = undefined; 44 | if (val && val.joinAll) { 45 | // instanceof Cursor 46 | cursorPromise = val.exec(); 47 | } else if (_checkTypes2.default.object(val) && val.cursor && val.then) { 48 | cursorPromise = val; 49 | } 50 | if (cursorPromise) { 51 | cursor._trackChildCursorPromise(cursorPromise); 52 | } 53 | return cursorPromise || val; 54 | }); 55 | 56 | return Promise.all(res).then(function () { 57 | return docs; 58 | }); 59 | } 60 | }; -------------------------------------------------------------------------------- /lib/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Debounce with updetable wait time and force 3 | * execution on some number of calls (batch execution) 4 | * Return promise that resolved with result of execution. 5 | * Promise cerated on each new execution (on idle). 6 | * @param {Function} func 7 | * @param {Number} wait 8 | * @param {Number} batchSize 9 | * @return {Promise} 10 | */ 11 | export default function debounce(func, wait, batchSize) { 12 | var timeout = null; 13 | var callsCount = 0; 14 | var promise = null; 15 | var doNotResolve = true; 16 | var maybeResolve = null; 17 | 18 | const debouncer = function() { 19 | const context = this; 20 | const args = arguments; 21 | 22 | if (!promise) { 23 | promise = new Promise((resolve, reject) => { 24 | maybeResolve = () => { 25 | if (doNotResolve) { 26 | timeout = setTimeout(maybeResolve, wait); 27 | doNotResolve = false; 28 | } else { 29 | resolve(func.apply(context, args)); 30 | promise = null; 31 | callsCount = 0; 32 | timeout = null; 33 | doNotResolve = true; 34 | maybeResolve = null; 35 | } 36 | }; 37 | maybeResolve(); 38 | }); 39 | } else { 40 | const callNow = batchSize && callsCount >= batchSize; 41 | doNotResolve = !callNow; 42 | 43 | if (callNow && maybeResolve) { 44 | const returnPromise = promise; 45 | returnPromise.debouncePassed = true; 46 | clearTimeout(timeout); 47 | maybeResolve(); 48 | callsCount += 1; 49 | return returnPromise; 50 | } 51 | } 52 | 53 | callsCount += 1; 54 | return promise; 55 | }; 56 | 57 | 58 | const updateBatchSize = function(newBatchSize) { 59 | batchSize = newBatchSize; 60 | }; 61 | const updateWait = function(newWait) { 62 | wait = newWait; 63 | }; 64 | const cancel = function() { 65 | clearTimeout(timeout); 66 | }; 67 | 68 | debouncer.updateBatchSize = updateBatchSize; 69 | debouncer.updateWait = updateWait; 70 | debouncer.cancel = cancel; 71 | debouncer.func = func; 72 | return debouncer; 73 | } 74 | -------------------------------------------------------------------------------- /lib/StorageManager.js: -------------------------------------------------------------------------------- 1 | import _keys from 'fast.js/object/keys'; 2 | import EventEmitter from 'eventemitter3'; 3 | import PromiseQueue from './PromiseQueue'; 4 | import EJSON from './EJSON'; 5 | 6 | 7 | /** 8 | * Manager for dealing with backend storage 9 | * of the daatabase. Default implementation uses 10 | * memory. You can implement the same interface 11 | * and use another storage (with levelup, for example) 12 | */ 13 | export class StorageManager { 14 | constructor(db, options = {}) { 15 | this.db = db; 16 | this.options = options; 17 | this._queue = new PromiseQueue(1); 18 | this._storage = {}; 19 | this.reload(); 20 | } 21 | 22 | loaded() { 23 | return this._loadedPromise; 24 | } 25 | 26 | reload() { 27 | if (this._loadedPromise) { 28 | this._loadedPromise = this._loadedPromise.then(() => { 29 | return this._loadStorage(); 30 | }); 31 | } else { 32 | this._loadedPromise = this._loadStorage(); 33 | } 34 | return this.loaded(); 35 | } 36 | 37 | destroy() { 38 | return this.loaded().then(() => { 39 | this._storage = {}; 40 | }); 41 | } 42 | 43 | persist(key, value) { 44 | return this.loaded().then(() => { 45 | this._storage[key] = EJSON.clone(value); 46 | }); 47 | } 48 | 49 | delete(key) { 50 | return this.loaded().then(() => { 51 | delete this._storage[key]; 52 | }); 53 | } 54 | 55 | get(key) { 56 | return this.loaded().then(() => this._storage[key]); 57 | } 58 | 59 | createReadStream(options = {}) { 60 | // Very limited subset of ReadableStream 61 | let paused = false; 62 | const emitter = new EventEmitter(); 63 | emitter.pause = () => paused = true; 64 | 65 | this.loaded().then(() => { 66 | const keys = _keys(this._storage); 67 | for (let i = 0; i < keys.length; i++) { 68 | emitter.emit('data', { value: this._storage[keys[i]] }); 69 | if (paused) { 70 | return; 71 | } 72 | } 73 | emitter.emit('end'); 74 | }); 75 | 76 | return emitter; 77 | } 78 | 79 | _loadStorage() { 80 | this._storage = {}; 81 | return Promise.resolve(); 82 | } 83 | } 84 | 85 | export default StorageManager; 86 | -------------------------------------------------------------------------------- /test/both/Base64.test.js: -------------------------------------------------------------------------------- 1 | import Base64 from '../../lib/Base64'; 2 | import EJSON from '../../lib/EJSON'; 3 | import chai, {except, assert} from 'chai'; 4 | chai.use(require('chai-as-promised')); 5 | chai.should(); 6 | 7 | 8 | var asciiToArray = function (str) { 9 | var arr = Base64.newBinary(str.length); 10 | for (var i = 0; i < str.length; i++) { 11 | var c = str.charCodeAt(i); 12 | if (c > 0xFF) { 13 | throw new Error("Not ascii"); 14 | } 15 | arr[i] = c; 16 | } 17 | return arr; 18 | }; 19 | 20 | var arrayToAscii = function (arr) { 21 | var res = []; 22 | for (var i = 0; i < arr.length; i++) { 23 | res.push(String.fromCharCode(arr[i])); 24 | } 25 | return res.join(""); 26 | }; 27 | 28 | describe('Base64', function () { 29 | it('should encode and decode', function () { 30 | assert.equal(arrayToAscii(asciiToArray("The quick brown fox jumps over the lazy dog")), 31 | "The quick brown fox jumps over the lazy dog"); 32 | }); 33 | 34 | it('should accept empty and produce empty string', function () { 35 | assert.deepEqual(Base64.encode(EJSON.newBinary(0)), ""); 36 | assert.deepEqual(Base64.decode(""), EJSON.newBinary(0)); 37 | }); 38 | 39 | it('should accept wikipedia example', function () { 40 | var tests = [ 41 | {txt: "pleasure.", res: "cGxlYXN1cmUu"}, 42 | {txt: "leasure.", res: "bGVhc3VyZS4="}, 43 | {txt: "easure.", res: "ZWFzdXJlLg=="}, 44 | {txt: "asure.", res: "YXN1cmUu"}, 45 | {txt: "sure.", res: "c3VyZS4="} 46 | ]; 47 | tests.forEach(function(t) { 48 | assert.deepEqual(Base64.encode(asciiToArray(t.txt)), t.res); 49 | assert.deepEqual(arrayToAscii(Base64.decode(t.res)), t.txt); 50 | }); 51 | }); 52 | 53 | it('should accept non-text data', function() { 54 | var tests = [ 55 | {array: [0, 0, 0], b64: "AAAA"}, 56 | {array: [0, 0, 1], b64: "AAAB"} 57 | ]; 58 | tests.forEach(function(t) { 59 | assert.deepEqual(Base64.encode(t.array), t.b64); 60 | var expectedAsBinary = EJSON.newBinary(t.array.length); 61 | t.array.forEach(function (val, i) { 62 | expectedAsBinary[i] = val; 63 | }); 64 | assert.deepEqual(Base64.decode(t.b64), expectedAsBinary); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/both/Random.test.js: -------------------------------------------------------------------------------- 1 | import Random from '../../lib/Random'; 2 | import chai, {except, assert} from 'chai'; 3 | import _ from 'lodash'; 4 | chai.use(require('chai-as-promised')); 5 | chai.should(); 6 | 7 | describe('Random', function () { 8 | it('should generate random with seed', function () { 9 | // Deterministic with a specified seed, which should generate the 10 | // same sequence in all environments. 11 | // 12 | // For repeatable unit test failures using deterministic random 13 | // number sequences it's fine if a new Meteor release changes the 14 | // algorithm being used and it starts generating a different 15 | // sequence for a seed, as long as the sequence is consistent for 16 | // a particular release. 17 | var random = Random.createWithSeeds(0); 18 | assert.equal(random.id(), "cp9hWvhg8GSvuZ9os"); 19 | assert.equal(random.id(), "3f3k6Xo7rrHCifQhR"); 20 | assert.equal(random.id(), "shxDnjWWmnKPEoLhM"); 21 | assert.equal(random.id(), "6QTjB8C5SEqhmz4ni"); 22 | }); 23 | 24 | it('should generate number with specified format without seed', function () { 25 | var idLen = 17; 26 | assert.equal(Random.default().id().length, idLen); 27 | assert.equal(Random.default().id(29).length, 29); 28 | var numDigits = 9; 29 | var hexStr = Random.default().hexString(numDigits); 30 | assert.equal(hexStr.length, numDigits); 31 | parseInt(hexStr, 16); // should not throw 32 | var frac = Random.default().fraction(); 33 | assert.isTrue(frac < 1.0); 34 | assert.isTrue(frac >= 0.0); 35 | 36 | assert.equal(Random.default().secret().length, 43); 37 | assert.equal(Random.default().secret(13).length, 13); 38 | }); 39 | 40 | it('should select Alea only in final resort', function () { 41 | if (typeof window === 'undefined') { 42 | assert.isTrue(Random.default().alea === undefined); 43 | } else { 44 | var useGetRandomValues = !!(typeof window !== "undefined" && 45 | window.crypto && window.crypto.getRandomValues); 46 | assert.equal(Random.default().alea === undefined, useGetRandomValues); 47 | } 48 | }); 49 | 50 | it('should rise an exception if no seed provided', function () { 51 | assert.throws(function () { 52 | Random.createWithSeeds(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /dist/debounce.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = debounce; 7 | /** 8 | * Debounce with updetable wait time and force 9 | * execution on some number of calls (batch execution) 10 | * Return promise that resolved with result of execution. 11 | * Promise cerated on each new execution (on idle). 12 | * @param {Function} func 13 | * @param {Number} wait 14 | * @param {Number} batchSize 15 | * @return {Promise} 16 | */ 17 | function debounce(func, wait, batchSize) { 18 | var timeout = null; 19 | var callsCount = 0; 20 | var promise = null; 21 | var doNotResolve = true; 22 | var _maybeResolve = null; 23 | 24 | var debouncer = function debouncer() { 25 | var context = this; 26 | var args = arguments; 27 | 28 | if (!promise) { 29 | promise = new Promise(function (resolve, reject) { 30 | _maybeResolve = function maybeResolve() { 31 | if (doNotResolve) { 32 | timeout = setTimeout(_maybeResolve, wait); 33 | doNotResolve = false; 34 | } else { 35 | resolve(func.apply(context, args)); 36 | promise = null; 37 | callsCount = 0; 38 | timeout = null; 39 | doNotResolve = true; 40 | _maybeResolve = null; 41 | } 42 | }; 43 | _maybeResolve(); 44 | }); 45 | } else { 46 | var callNow = batchSize && callsCount >= batchSize; 47 | doNotResolve = !callNow; 48 | 49 | if (callNow && _maybeResolve) { 50 | var returnPromise = promise; 51 | returnPromise.debouncePassed = true; 52 | clearTimeout(timeout); 53 | _maybeResolve(); 54 | callsCount += 1; 55 | return returnPromise; 56 | } 57 | } 58 | 59 | callsCount += 1; 60 | return promise; 61 | }; 62 | 63 | var updateBatchSize = function updateBatchSize(newBatchSize) { 64 | batchSize = newBatchSize; 65 | }; 66 | var updateWait = function updateWait(newWait) { 67 | wait = newWait; 68 | }; 69 | var cancel = function cancel() { 70 | clearTimeout(timeout); 71 | }; 72 | 73 | debouncer.updateBatchSize = updateBatchSize; 74 | debouncer.updateWait = updateWait; 75 | debouncer.cancel = cancel; 76 | debouncer.func = func; 77 | return debouncer; 78 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Artem Artemev <art@studytime.me> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | ======================================== 26 | Meteor is licensed under the MIT License 27 | ======================================== 28 | 29 | Copyright (C) 2011--2015 Meteor Development Group 30 | 31 | 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: 32 | 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 35 | 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. 36 | 37 | 38 | -------------------------------------------------------------------------------- /dist/CollectionIndex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.CollectionIndex = undefined; 9 | 10 | var _invariant = require('invariant'); 11 | 12 | var _invariant2 = _interopRequireDefault(_invariant); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 17 | 18 | var CollectionIndex = exports.CollectionIndex = function () { 19 | function CollectionIndex() { 20 | var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 21 | 22 | _classCallCheck(this, CollectionIndex); 23 | 24 | (0, _invariant2.default)(options.fieldName, 'CollectionIndex(...): you must specify a "feildName" option'); 25 | (0, _invariant2.default)(!Array.isArray(options.fieldName), 'CollectionIndex(...): compound index is not supported yet'); 26 | 27 | this.fieldName = options.fieldName; 28 | this.unique = options.unique || false; 29 | this.sparse = options.sparse || false; 30 | 31 | this.reset(); 32 | } 33 | 34 | _createClass(CollectionIndex, [{ 35 | key: 'reset', 36 | value: function reset() { 37 | // TODO 38 | } 39 | }, { 40 | key: 'insert', 41 | value: function insert(doc) { 42 | // TODO 43 | } 44 | }, { 45 | key: 'remove', 46 | value: function remove(doc) { 47 | // TODO 48 | } 49 | }, { 50 | key: 'update', 51 | value: function update(oldDoc, newDoc) { 52 | // TODO 53 | } 54 | }, { 55 | key: 'getMatching', 56 | value: function getMatching(value) { 57 | // TODO 58 | } 59 | }, { 60 | key: 'getBetweenBounds', 61 | value: function getBetweenBounds(query) { 62 | // TODO 63 | } 64 | }, { 65 | key: 'getAll', 66 | value: function getAll(options) { 67 | // TODO 68 | } 69 | }]); 70 | 71 | return CollectionIndex; 72 | }(); 73 | 74 | exports.default = CollectionIndex; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marsdb", 3 | "version": "0.6.11", 4 | "author": { 5 | "name": "Artem Artemev", 6 | "email": "art@studytime.me" 7 | }, 8 | "contributors": [ 9 | "Artem Artemev" 10 | ], 11 | "description": "MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6", 12 | "keywords": [ 13 | "database", 14 | "datastore", 15 | "embedded", 16 | "levelup", 17 | "mongoose", 18 | "linvodb3", 19 | "nedb" 20 | ], 21 | "homepage": "https://github.com/c58/marsdb", 22 | "repository": { 23 | "type": "git", 24 | "url": "git@github.com:c58/marsdb.git" 25 | }, 26 | "dependencies": { 27 | "check-types": "^6.0.0", 28 | "double-ended-queue": "^0.9.7", 29 | "eventemitter3": "1.1.1", 30 | "fast.js": "^0.1.1", 31 | "geojson-utils": "^1.1.0", 32 | "invariant": "^2.2.0" 33 | }, 34 | "devDependencies": { 35 | "core-js": "^2.0.1", 36 | "lodash": "3.10.x", 37 | "babel-cli": "^6.3.17", 38 | "babel-eslint": "^5.0.0-beta6", 39 | "babel-preset-es2015": "^6.3.13", 40 | "babel-preset-stage-0": "^6.3.13", 41 | "babel-register": "^6.3.13", 42 | "babelify": "^7.2.0", 43 | "brfs": "^1.4.1", 44 | "browserify": "^13.0.0", 45 | "bulk-require": "^0.2.1", 46 | "bulkify": "^1.1.1", 47 | "chai": "^3.4.1", 48 | "chai-as-promised": "^5.2.0", 49 | "coveralls": "^2.11.6", 50 | "del": "^2.2.0", 51 | "envify": "^3.4.0", 52 | "fbjs-scripts": "^0.5.0", 53 | "gulp": "^3.9.0", 54 | "gulp-babel": "^6.1.1", 55 | "gulp-eslint": "^1.1.1", 56 | "gulp-if": "^2.0.0", 57 | "gulp-rename": "^1.2.2", 58 | "gulp-uglify": "^1.5.1", 59 | "istanbul": "^1.0.0-alpha.2", 60 | "mocha": "^2.3.4", 61 | "mocha-lcov-reporter": "^1.0.0", 62 | "mocha-phantomjs": "^4.0.2", 63 | "sinon": "^1.17.2", 64 | "sinon-chai": "^2.8.0", 65 | "require-dir": "^0.3.0", 66 | "run-sequence": "^1.1.5", 67 | "vinyl-buffer": "^1.0.0", 68 | "vinyl-source-stream": "^1.1.0" 69 | }, 70 | "scripts": { 71 | "test_some": "mocha --require babelhook --reporter spec --timeout 1000", 72 | "test_browser": "gulp build:browser:tests && mocha-phantomjs --reporter spec browser_tests.html", 73 | "test": "mocha --require babelhook --reporter spec --timeout 1000 test/both test/node", 74 | "coverage": "./node_modules/.bin/babel-node ./node_modules/istanbul/lib/cli cover _mocha test/both test/node -- -u exports -R spec && open coverage/lcov-report/index.html", 75 | "coveralls": "./node_modules/.bin/babel-node ./node_modules/istanbul/lib/cli cover _mocha test/both test/node --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js ./lib && rm -rf ./coverage" 76 | }, 77 | "main": "index.js", 78 | "license": "MIT" 79 | } 80 | -------------------------------------------------------------------------------- /gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var babel = require('gulp-babel'); 5 | var config = require('../config'); 6 | var gulpif = require('gulp-if'); 7 | var source = require('vinyl-source-stream'); 8 | var buffer = require('vinyl-buffer'); 9 | var browserify = require('browserify'); 10 | var babelify = require('babelify'); 11 | var uglify = require('gulp-uglify'); 12 | var rename = require('gulp-rename'); 13 | var path = require('path'); 14 | 15 | 16 | gulp.task('build', [ 17 | 'build:browser', 18 | 'build:browser:min', 19 | 'build:browser:tests', 20 | 'build:browser:polyfills', 21 | 'build:node' 22 | ]); 23 | 24 | gulp.task('build:node', function() { 25 | return gulp.src(config.src) 26 | .pipe(babel()) 27 | .pipe(gulp.dest(config.dist)); 28 | }); 29 | 30 | gulp.task('build:browser:min', ['build:browser'], function() { 31 | return gulp.src(path.join(config.build, config.browser.bundleName)) 32 | .pipe(rename(config.browser.bundleMinName)) 33 | .pipe(uglify()) 34 | .pipe(gulp.dest(config.build)) 35 | }); 36 | 37 | gulp.task('build:browser:polyfills', function() { 38 | var customOpts = { 39 | entries: config.browser.entryPolyfills, 40 | debug: false, 41 | fullPaths: false 42 | }; 43 | 44 | return browserify(customOpts).bundle() 45 | .pipe(source(config.browser.bundlePolyfillsName)) 46 | .pipe(buffer()) 47 | .pipe(uglify()) 48 | .pipe(gulp.dest(config.build)) 49 | }); 50 | 51 | gulp.task('build:browser:tests', ['build:node'], function() { 52 | // Basic options 53 | var customOpts = { 54 | entries: './browser_tests.js', 55 | debug: false, 56 | fullPaths: false, 57 | delay: 50 58 | }; 59 | var b = browserify(customOpts); 60 | 61 | // Transformations 62 | var transforms = [ 63 | babelify.configure(), 64 | 'brfs', 65 | 'bulkify', 66 | 'envify' 67 | ]; 68 | 69 | transforms.forEach(function(transform) { 70 | b.transform(transform); 71 | }); 72 | 73 | // Add handlers 74 | return b.bundle() 75 | .pipe(source('browser_tests.js')) 76 | .pipe(buffer()) 77 | .pipe(gulp.dest(config.build)) 78 | }); 79 | 80 | gulp.task('build:browser', ['build:node'], function() { 81 | var b = browserify({ 82 | entries: config.browser.entry, 83 | debug: false, 84 | fullPaths: false, 85 | delay: 50, 86 | standalone: 'Mars', 87 | }); 88 | 89 | var transforms = [babelify.configure({}), 'bulkify', 'envify']; 90 | transforms.forEach(function(transform) { 91 | b.transform(transform); 92 | }); 93 | 94 | return b 95 | .exclude('crypto') 96 | .bundle() 97 | .pipe(source(config.browser.bundleName)) 98 | .pipe(buffer()) 99 | .pipe(gulp.dest(config.build)) 100 | }); 101 | -------------------------------------------------------------------------------- /lib/CollectionDelegate.js: -------------------------------------------------------------------------------- 1 | import _map from 'fast.js/map'; 2 | import DocumentModifier from './DocumentModifier'; 3 | 4 | 5 | /** 6 | * Default collection delegate for working with a 7 | * normal MarsDB approach – within a browser. 8 | */ 9 | export class CollectionDelegate { 10 | constructor(db) { 11 | this.db = db; 12 | } 13 | 14 | insert(doc, options = {}, randomId) { 15 | return this.db.indexManager.indexDocument(doc).then(() => 16 | this.db.storageManager.persist(doc._id, doc).then(() => 17 | doc._id 18 | ) 19 | ); 20 | } 21 | 22 | remove(query, {sort = {_id: 1}, multi = false}) { 23 | return this.find(query, {noClone: true}) 24 | .sort(sort).then((docs) => { 25 | if (docs.length > 1 && !multi) { 26 | docs = [docs[0]]; 27 | } 28 | const removeStorgePromises = _map(docs, d => 29 | this.db.storageManager.delete(d._id) 30 | ); 31 | const removeIndexPromises = _map(docs, d => 32 | this.db.indexManager.deindexDocument(d) 33 | ); 34 | return Promise.all([ 35 | ...removeStorgePromises, 36 | ...removeIndexPromises, 37 | ]).then(() => docs); 38 | }); 39 | } 40 | 41 | update(query, modifier, {sort = {_id: 1}, multi = false, upsert = false}) { 42 | return this.find(query, {noClone: true}) 43 | .sort(sort).then((docs) => { 44 | if (docs.length > 1 && !multi) { 45 | docs = [docs[0]]; 46 | } 47 | return new DocumentModifier(query) 48 | .modify(docs, modifier, { upsert }); 49 | }).then(({original, updated}) => { 50 | const updateStorgePromises = _map(updated, d => 51 | this.db.storageManager.persist(d._id, d) 52 | ); 53 | const updateIndexPromises = _map(updated, (d, i) => 54 | this.db.indexManager.reindexDocument(original[i], d) 55 | ); 56 | return Promise.all([ 57 | ...updateStorgePromises, 58 | ...updateIndexPromises, 59 | ]).then(() => ({ 60 | modified: updated.length, 61 | original: original, 62 | updated: updated, 63 | })); 64 | }); 65 | } 66 | 67 | find(query, options = {}) { 68 | const cursorClass = this.db.cursorClass; 69 | return new cursorClass(this.db, query, options); 70 | } 71 | 72 | findOne(query, options = {}) { 73 | return this.find(query, options) 74 | .aggregate(docs => docs[0]) 75 | .limit(1); 76 | } 77 | 78 | count(query, options = {}) { 79 | options.noClone = true; 80 | return this.find(query, options) 81 | .aggregate((docs) => docs.length); 82 | } 83 | 84 | ids(query, options = {}) { 85 | options.noClone = true; 86 | return this.find(query, options) 87 | .map((doc) => doc._id); 88 | } 89 | } 90 | 91 | export default CollectionDelegate; 92 | -------------------------------------------------------------------------------- /lib/AsyncEventEmitter.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | 4 | /** 5 | * Extension of a regular EventEmitter that provides a method 6 | * that returns a Promise then resolved when all listeners of the event 7 | * will be resolved. 8 | */ 9 | /* istanbul ignore next */ 10 | export default class AsyncEventEmitter extends EventEmitter { 11 | 12 | /** 13 | * Emit an event and return a Promise that will be resolved 14 | * when all listeren's Promises will be resolved. 15 | * @param {String} event 16 | * @return {Promise} 17 | */ 18 | emitAsync(event, a1, a2, a3, a4, a5) { 19 | const prefix = EventEmitter.prefixed; 20 | const evt = prefix ? prefix + event : event; 21 | 22 | if (!this._events || !this._events[evt]) { 23 | return Promise.resolve(); 24 | } 25 | 26 | let i; 27 | const listeners = this._events[evt]; 28 | const len = arguments.length; 29 | let args; 30 | 31 | if ('function' === typeof listeners.fn) { 32 | if (listeners.once) { 33 | this.removeListener(event, listeners.fn, undefined, true); 34 | } 35 | 36 | switch (len) { 37 | case 1: return Promise.resolve(listeners.fn.call(listeners.context)); 38 | case 2: return Promise.resolve(listeners.fn.call(listeners.context, a1)); 39 | case 3: return Promise.resolve(listeners.fn.call(listeners.context, a1, a2)); 40 | case 4: return Promise.resolve(listeners.fn.call(listeners.context, a1, a2, a3)); 41 | case 5: return Promise.resolve(listeners.fn.call(listeners.context, a1, a2, a3, a4)); 42 | case 6: return Promise.resolve(listeners.fn.call(listeners.context, a1, a2, a3, a4, a5)); 43 | } 44 | 45 | for (i = 1, args = new Array(len -1); i < len; i++) { 46 | args[i - 1] = arguments[i]; 47 | } 48 | 49 | return Promise.resolve(listeners.fn.apply(listeners.context, args)); 50 | } else { 51 | const promises = []; 52 | const length = listeners.length; 53 | let j; 54 | 55 | for (i = 0; i < length; i++) { 56 | if (listeners[i].once) { 57 | this.removeListener(event, listeners[i].fn, undefined, true); 58 | } 59 | 60 | switch (len) { 61 | case 1: promises.push(Promise.resolve(listeners[i].fn.call(listeners[i].context))); break; 62 | case 2: promises.push(Promise.resolve(listeners[i].fn.call(listeners[i].context, a1))); break; 63 | case 3: promises.push(Promise.resolve(listeners[i].fn.call(listeners[i].context, a1, a2))); break; 64 | default: 65 | if (!args) { 66 | for (j = 1, args = new Array(len -1); j < len; j++) { 67 | args[j - 1] = arguments[j]; 68 | } 69 | } 70 | promises.push(Promise.resolve(listeners[i].fn.apply(listeners[i].context, args))); 71 | } 72 | } 73 | 74 | return Promise.all(promises); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/PromiseQueue.js: -------------------------------------------------------------------------------- 1 | import _try from 'fast.js/function/try'; 2 | import Deque from 'double-ended-queue'; 3 | 4 | 5 | /** 6 | * It limits concurrently executed promises 7 | * 8 | * @param {Number} [maxPendingPromises=Infinity] max number of concurrently executed promises 9 | * @param {Number} [maxQueuedPromises=Infinity] max number of queued promises 10 | * @constructor 11 | */ 12 | export default class PromiseQueue { 13 | constructor(maxPendingPromises = Infinity, maxQueuedPromises = Infinity) { 14 | this.pendingPromises = 0; 15 | this.maxPendingPromises = maxPendingPromises; 16 | this.maxQueuedPromises = maxQueuedPromises; 17 | this.queue = new Deque(); 18 | this.length = 0; 19 | } 20 | 21 | /** 22 | * Pause queue processing 23 | */ 24 | pause() { 25 | this._paused = true; 26 | } 27 | 28 | /** 29 | * Resume queue processing 30 | */ 31 | unpause() { 32 | this._paused = false; 33 | this._dequeue(); 34 | } 35 | 36 | /** 37 | * Adds new promise generator in the queue 38 | * @param {Function} promiseGenerator 39 | */ 40 | add(promiseGenerator, unshift = false) { 41 | return new Promise((resolve, reject) => { 42 | if (this.length >= this.maxQueuedPromises) { 43 | reject(new Error('Queue limit reached')); 44 | } else { 45 | const queueItem = { 46 | promiseGenerator: promiseGenerator, 47 | resolve: resolve, 48 | reject: reject, 49 | }; 50 | 51 | if (!unshift) { 52 | this.queue.push(queueItem); 53 | } else { 54 | this.queue.unshift(queueItem); 55 | } 56 | 57 | this.length += 1; 58 | this._dequeue(); 59 | } 60 | }); 61 | } 62 | 63 | /** 64 | * Internal queue processor. Starts processing of 65 | * the next queued function 66 | * @return {Boolean} 67 | */ 68 | _dequeue() { 69 | if (this._paused || this.pendingPromises >= this.maxPendingPromises) { 70 | return false; 71 | } 72 | 73 | const item = this.queue.shift(); 74 | if (!item) { 75 | return false; 76 | } 77 | 78 | const result = _try(() => { 79 | this.pendingPromises++; 80 | return Promise.resolve() 81 | .then(() => item.promiseGenerator()) 82 | .then( 83 | (value) => { 84 | this.length--; 85 | this.pendingPromises--; 86 | item.resolve(value); 87 | this._dequeue(); 88 | }, 89 | (err) => { 90 | this.length--; 91 | this.pendingPromises--; 92 | item.reject(err); 93 | this._dequeue(); 94 | } 95 | ); 96 | }); 97 | 98 | if (result instanceof Error) { 99 | this.length--; 100 | this.pendingPromises--; 101 | item.reject(result); 102 | this._dequeue(); 103 | } 104 | 105 | return true; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/both/PromiseQueue.test.js: -------------------------------------------------------------------------------- 1 | import PromiseQueue from '../../lib/PromiseQueue'; 2 | import chai, {expect} from 'chai'; 3 | import sinon from 'sinon'; 4 | chai.use(require('chai-as-promised')); 5 | chai.use(require('sinon-chai')); 6 | chai.should(); 7 | 8 | 9 | describe('PromiseQueue', () => { 10 | describe('#add', function () { 11 | it('should rise an exception if queue is full', function () { 12 | const queue = new PromiseQueue(1, 1); 13 | return Promise.all([ 14 | queue.add(() => {}), 15 | queue.add(() => {}).should.be.eventually.rejected, 16 | ]); 17 | }); 18 | 19 | it('should start task execution with give concurrency', function () { 20 | const cb1 = sinon.spy(); 21 | const cb2 = sinon.spy(); 22 | const cb3 = sinon.spy(); 23 | const queue = new PromiseQueue(2); 24 | const ops = [ 25 | queue.add(cb1), 26 | queue.add(cb2), 27 | queue.add(cb3), 28 | ]; 29 | queue.length.should.be.equals(3); 30 | cb1.should.have.callCount(0); 31 | cb2.should.have.callCount(0); 32 | cb3.should.have.callCount(0); 33 | return Promise.all(ops).then(() => { 34 | cb1.should.have.callCount(1); 35 | cb2.should.have.callCount(1); 36 | cb3.should.have.callCount(1); 37 | queue.length.should.be.equals(0); 38 | }); 39 | }); 40 | 41 | it('should reject on error and process next item', function () { 42 | const cb1 = sinon.spy(); 43 | const cb2 = sinon.spy(); 44 | const queue = new PromiseQueue(1); 45 | return Promise.all([ 46 | queue.add(() => { 47 | throw new Error(); 48 | }).should.be.eventually.rejected, 49 | queue.add(() => { 50 | return new Promise((resolve, reject) => { 51 | throw new Error(); 52 | }) 53 | }).should.be.eventually.rejected, 54 | queue.add(() => { 55 | return new Promise((resolve, reject) => { 56 | setTimeout(() => { 57 | reject(new Error()); 58 | }, 10); 59 | }) 60 | }).should.be.eventually.rejected, 61 | queue.add(() => {}).should.be.eventually.fulfilled, 62 | ]); 63 | }); 64 | 65 | it('should start execution only after task resolved', function () { 66 | const cb1 = sinon.spy(); 67 | const cb2 = sinon.spy(); 68 | const queue = new PromiseQueue(2); 69 | return Promise.all([ 70 | queue.add(() => { 71 | return new Promise((resolve, reject) => { 72 | setTimeout(() => { 73 | cb1(); 74 | resolve(); 75 | }, 10); 76 | }) 77 | }), 78 | queue.add(() => { 79 | cb1.should.have.callCount(0); 80 | return new Promise((resolve, reject) => { 81 | setTimeout(() => { 82 | cb2(); 83 | resolve(); 84 | }, 20); 85 | }) 86 | }), 87 | queue.add(() => { 88 | cb1.should.have.callCount(1); 89 | cb2.should.have.callCount(0); 90 | }), 91 | ]); 92 | }); 93 | }); 94 | }); -------------------------------------------------------------------------------- /test/both/StorageManager.test.js: -------------------------------------------------------------------------------- 1 | import StorageManager from '../../lib/StorageManager'; 2 | import chai from 'chai'; 3 | import sinon from 'sinon'; 4 | chai.use(require('chai-as-promised')); 5 | chai.use(require('sinon-chai')); 6 | chai.should(); 7 | 8 | 9 | describe('StorageManager', () => { 10 | it('should be created', () => { 11 | const db = new StorageManager(); 12 | }); 13 | 14 | it('should reload storage after running reloading', function () { 15 | const db = new StorageManager(); 16 | const cb = sinon.spy(); 17 | return db.loaded().then(() => { 18 | db.reload().then(cb); 19 | db.reload().then(() => cb.should.have.callCount(1)); 20 | db.reload().then(() => cb.should.have.callCount(2)); 21 | db.reload().then(() => cb.should.have.callCount(3)); 22 | db.reload().then(() => cb.should.have.callCount(4)); 23 | }) 24 | }); 25 | 26 | it('should destroy storage', function () { 27 | const db = new StorageManager(); 28 | return db.persist('a', {_id: 'a', a: 1}).then(() => { 29 | return db.get('a').should.eventually.deep.equal({_id: 'a', a: 1}); 30 | }) 31 | .then(() => db.destroy()) 32 | .then(() => { 33 | return db.get('a').should.eventually.deep.equal(undefined); 34 | }); 35 | }); 36 | 37 | it('should be a able to persist', () => { 38 | const db = new StorageManager(); 39 | return db.persist('a', {_id: 'a', a: 1}).then(() => { 40 | return db.get('a').should.eventually.deep.equal({_id: 'a', a: 1}); 41 | }); 42 | }); 43 | 44 | it('should NOT clone object when getting', () => { 45 | const db = new StorageManager(); 46 | return db.persist('a', {_id: 'a', a: 1}).then(() => { 47 | return db.get('a'); 48 | }).then(doc => { 49 | doc.a = 2; 50 | return db.get('a'); 51 | }).then(doc => { 52 | doc.a.should.be.equal(2); 53 | }); 54 | }); 55 | 56 | it('should NOT clone objects when streaming', (done) => { 57 | const db = new StorageManager(); 58 | db.persist('a', {_id: 'a', a: 1}).then(() => { 59 | db.createReadStream() 60 | .on('data', (doc) => doc.value.a = 2) 61 | .on('end', () => { 62 | db.createReadStream() 63 | .on('data', (doc) => { 64 | doc.value.a.should.be.equal(2); 65 | }) 66 | .on('end', () => { 67 | done(); 68 | }) 69 | }) 70 | }); 71 | }); 72 | 73 | it('should be a able to persist with replace by id', () => { 74 | const db = new StorageManager(); 75 | return db.persist('a', {_id: 'a', a: 1}).then(() => { 76 | db.get('a').should.eventually.deep.equal({_id: 'a', a: 1}); 77 | return db.persist('a', {_id: 'a', b: 1}).then(() => { 78 | return db.get('a').should.eventually.deep.equal({_id: 'a', b: 1}); 79 | }); 80 | }); 81 | }); 82 | 83 | it('should be a able to delete', () => { 84 | const db = new StorageManager(); 85 | return db.persist('a', {_id: 'a', a: 1}).then(() => { 86 | return db.delete('a').then(() => { 87 | db.get('a').should.not.eventually.deep.equal({_id: 'a', a: 1}); 88 | return db.get('a').should.eventually.equal(undefined); 89 | }); 90 | }); 91 | }); 92 | 93 | it('should be a able to create a stream', () => { 94 | const db = new StorageManager(); 95 | return Promise.all([ 96 | db.persist('a', {_id: 'a', a: 1}), 97 | db.persist('b', {_id: 'b', b: 1}), 98 | db.persist('c', {_id: 'c', c: 1}), 99 | db.persist('d', {_id: 'd', d: 1}) 100 | ]).then(() => { 101 | return new Promise((resolve, reject) => { 102 | const givenIds = []; 103 | db.createReadStream() 104 | .on('data', d => givenIds.push(d.value._id)) 105 | .on('end', () => { 106 | givenIds.should.be.deep.equal(['a','b','c','d']); 107 | resolve(); 108 | }) 109 | }) 110 | }) 111 | }); 112 | }); -------------------------------------------------------------------------------- /lib/Base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on Meteor's Base64 package. 3 | * Rewrite with ES6 and better formated for passing 4 | * linter 5 | */ 6 | var BASE_64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 7 | var BASE_64_VALS = {}; 8 | 9 | (function setupBase64Vals() { 10 | for (let j = 0; j < BASE_64_CHARS.length; j++) { 11 | BASE_64_VALS[BASE_64_CHARS.charAt(j)] = j; 12 | } 13 | })(); 14 | 15 | var getChar = function(val) { 16 | return BASE_64_CHARS.charAt(val); 17 | }; 18 | 19 | var getVal = function(ch) { 20 | if (ch === '=') { 21 | return -1; 22 | } 23 | return BASE_64_VALS[ch]; 24 | }; 25 | 26 | // Base 64 encoding 27 | export class Base64 { 28 | encode(array) { 29 | if (typeof array === 'string') { 30 | var str = array; 31 | array = this.newBinary(str.length); 32 | for (let i = 0; i < str.length; i++) { 33 | var ch = str.charCodeAt(i); 34 | if (ch > 0xFF) { 35 | throw new Error( 36 | 'Not ascii. Base64.encode can only take ascii strings.'); 37 | } 38 | array[i] = ch; 39 | } 40 | } 41 | 42 | var answer = []; 43 | var a = null; 44 | var b = null; 45 | var c = null; 46 | var d = null; 47 | for (let i = 0; i < array.length; i++) { 48 | switch (i % 3) { 49 | case 0: 50 | a = (array[i] >> 2) & 0x3F; 51 | b = (array[i] & 0x03) << 4; 52 | break; 53 | case 1: 54 | b |= (array[i] >> 4) & 0xF; 55 | c = (array[i] & 0xF) << 2; 56 | break; 57 | case 2: 58 | c |= (array[i] >> 6) & 0x03; 59 | d = array[i] & 0x3F; 60 | answer.push(getChar(a)); 61 | answer.push(getChar(b)); 62 | answer.push(getChar(c)); 63 | answer.push(getChar(d)); 64 | a = null; 65 | b = null; 66 | c = null; 67 | d = null; 68 | break; 69 | } 70 | } 71 | if (a != null) { 72 | answer.push(getChar(a)); 73 | answer.push(getChar(b)); 74 | if (c == null) { 75 | answer.push('='); 76 | } else { 77 | answer.push(getChar(c)); 78 | } 79 | if (d == null) { 80 | answer.push('='); 81 | } 82 | } 83 | return answer.join(''); 84 | } 85 | 86 | decode(str) { 87 | var len = Math.floor((str.length*3)/4); 88 | if (str.charAt(str.length - 1) == '=') { 89 | len--; 90 | if (str.charAt(str.length - 2) == '=') { 91 | len--; 92 | } 93 | } 94 | var arr = this.newBinary(len); 95 | 96 | var one = null; 97 | var two = null; 98 | var three = null; 99 | 100 | var j = 0; 101 | 102 | for (let i = 0; i < str.length; i++) { 103 | var c = str.charAt(i); 104 | var v = getVal(c); 105 | switch (i % 4) { 106 | case 0: 107 | if (v < 0) { 108 | throw new Error('invalid base64 string'); 109 | } 110 | one = v << 2; 111 | break; 112 | case 1: 113 | if (v < 0) { 114 | throw new Error('invalid base64 string'); 115 | } 116 | one |= (v >> 4); 117 | arr[j++] = one; 118 | two = (v & 0x0F) << 4; 119 | break; 120 | case 2: 121 | if (v >= 0) { 122 | two |= (v >> 2); 123 | arr[j++] = two; 124 | three = (v & 0x03) << 6; 125 | } 126 | break; 127 | case 3: 128 | if (v >= 0) { 129 | arr[j++] = three | v; 130 | } 131 | break; 132 | } 133 | } 134 | return arr; 135 | } 136 | 137 | newBinary(len) { 138 | if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') { 139 | var ret = []; 140 | for (let i = 0; i < len; i++) { 141 | ret.push(0); 142 | } 143 | ret.$Uint8ArrayPolyfill = true; 144 | return ret; 145 | } 146 | return new Uint8Array(new ArrayBuffer(len)); 147 | } 148 | } 149 | 150 | export default new Base64(); 151 | -------------------------------------------------------------------------------- /lib/DocumentRetriver.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | import _map from 'fast.js/map'; 3 | import _filter from 'fast.js/array/filter'; 4 | import {selectorIsId, selectorIsIdPerhapsAsObject} from './Document'; 5 | 6 | 7 | // Internals 8 | const DEFAULT_QUERY_FILTER = () => true; 9 | 10 | /** 11 | * Class for getting data objects by given list of ids. 12 | * Promises based. It makes requests asyncronousle by 13 | * getting request frame from database. 14 | * It's not use caches, because it's a task of store. 15 | * It just retrives content by 'get' method. 16 | */ 17 | export class DocumentRetriver { 18 | constructor(db) { 19 | this.db = db; 20 | } 21 | 22 | /** 23 | * Retrive an optimal superset of documents 24 | * by given query based on _id field of the query 25 | * 26 | * TODO: there is a place for indexes 27 | * 28 | * @param {Object} query 29 | * @return {Promise} 30 | */ 31 | retriveForQeury(query, queryFilter = DEFAULT_QUERY_FILTER, options = {}) { 32 | // Try to get list of ids 33 | let selectorIds; 34 | if (selectorIsId(query)) { 35 | // fast path for scalar query 36 | selectorIds = [query]; 37 | } else if (selectorIsIdPerhapsAsObject(query)) { 38 | // also do the fast path for { _id: idString } 39 | selectorIds = [query._id]; 40 | } else if ( 41 | _check.object(query) && query.hasOwnProperty('_id') && 42 | _check.object(query._id) && query._id.hasOwnProperty('$in') && 43 | _check.array(query._id.$in) 44 | ) { 45 | // and finally fast path for multiple ids 46 | // selected by $in operator 47 | selectorIds = query._id.$in; 48 | } 49 | 50 | // Retrive optimally 51 | if (_check.array(selectorIds) && selectorIds.length > 0) { 52 | return this.retriveIds(queryFilter, selectorIds, options); 53 | } else { 54 | return this.retriveAll(queryFilter, options); 55 | } 56 | } 57 | 58 | /** 59 | * Rterive all ids given in constructor. 60 | * If some id is not retrived (retrived qith error), 61 | * then returned promise will be rejected with that error. 62 | * @return {Promise} 63 | */ 64 | retriveAll(queryFilter = DEFAULT_QUERY_FILTER, options = {}) { 65 | const limit = options.limit || +Infinity; 66 | const result = []; 67 | let stopped = false; 68 | 69 | return new Promise((resolve, reject) => { 70 | const stream = this.db.storage.createReadStream(); 71 | 72 | stream.on('data', (data) => { 73 | // After deleting of an item some storages 74 | // may return an undefined for a few times. 75 | // We need to check it there. 76 | if (!stopped && data.value) { 77 | const doc = this.db.create(data.value); 78 | if (result.length < limit && queryFilter(doc)) { 79 | result.push(doc); 80 | } 81 | // Limit the result if storage supports it 82 | if (result.length === limit && stream.pause) { 83 | stream.pause(); 84 | resolve(result); 85 | stopped = true; 86 | } 87 | } 88 | }) 89 | .on('end', () => !stopped && resolve(result)); 90 | }); 91 | } 92 | 93 | /** 94 | * Rterive all ids given in constructor. 95 | * If some id is not retrived (retrived qith error), 96 | * then returned promise will be rejected with that error. 97 | * @return {Promise} 98 | */ 99 | retriveIds(queryFilter = DEFAULT_QUERY_FILTER, ids = [], options = {}) { 100 | const uniqIds = _filter(ids, (id, i) => ids.indexOf(id) === i); 101 | const retrPromises = _map(uniqIds, id => this.retriveOne(id)); 102 | const limit = options.limit || +Infinity; 103 | 104 | return Promise.all(retrPromises).then((res) => { 105 | const filteredRes = []; 106 | 107 | for (let i = 0; i < res.length; i++) { 108 | const doc = res[i]; 109 | if (doc && queryFilter(doc)) { 110 | filteredRes.push(doc); 111 | if (filteredRes.length === limit) { 112 | break; 113 | } 114 | } 115 | } 116 | 117 | return filteredRes; 118 | }); 119 | } 120 | 121 | /** 122 | * Retrive one document by given id 123 | * @param {String} id 124 | * @return {Promise} 125 | */ 126 | retriveOne(id) { 127 | return this.db.storage.get(id).then((buf) => this.db.create(buf)); 128 | } 129 | } 130 | 131 | export default DocumentRetriver; 132 | -------------------------------------------------------------------------------- /lib/cursor-processors/joinObj.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | import _each from 'fast.js/forEach'; 3 | import _map from 'fast.js/map'; 4 | import _filter from 'fast.js/array/filter'; 5 | import _reduce from 'fast.js/array/reduce'; 6 | import _keys from 'fast.js/object/keys'; 7 | import Collection from '../Collection'; 8 | import invariant from 'invariant'; 9 | import { joinAll } from './joinAll'; 10 | import { findModTarget } from '../DocumentModifier'; 11 | import { makeLookupFunction } from '../DocumentMatcher'; 12 | import { selectorIsId } from '../Document'; 13 | 14 | 15 | /** 16 | * By given list of documents make mapping of joined 17 | * model ids to root document and vise versa. 18 | * @param {Array} docs 19 | * @param {String} key 20 | * @return {Object} 21 | */ 22 | function _decomposeDocuments(docs, key) { 23 | const lookupFn = makeLookupFunction(key); 24 | let allIds = []; 25 | 26 | const docsWrapped = _map(docs, (d) => { 27 | const val = lookupFn(d); 28 | const joinIds = _filter(_reduce(_map(val, x => x.value), (a, b) => { 29 | if (_check.array(b)) { 30 | return [...a, ...b]; 31 | } else { 32 | return [...a, b]; 33 | } 34 | }, []), x => selectorIsId(x)); 35 | 36 | allIds = allIds.concat(joinIds); 37 | return { 38 | doc: d, 39 | lookupResult: val, 40 | }; 41 | }); 42 | 43 | return { allIds, docsWrapped }; 44 | } 45 | 46 | 47 | /** 48 | * By given value of some key in join object return 49 | * an options object. 50 | * @param {Object|Collection} joinValue 51 | * @return {Object} 52 | */ 53 | function _getJoinOptions(key, value) { 54 | if (value instanceof Collection) { 55 | return { model: value, joinPath: key }; 56 | } else if (_check.object(value)) { 57 | return { 58 | model: value.model, 59 | joinPath: value.joinPath || key, 60 | }; 61 | } else { 62 | throw new Error('Invalid join object value'); 63 | } 64 | } 65 | 66 | 67 | /** 68 | * By given result of joining objects restriving and root documents 69 | * decomposition set joining object on each root document 70 | * (if it is exists). 71 | * @param {String} joinPath 72 | * @param {Array} res 73 | * @param {Object} docsById 74 | * @param {Object} childToRootMap 75 | */ 76 | function _joinDocsWithResult(joinPath, res, docsWrapped) { 77 | const resIdMap = {}; 78 | const initKeyparts = joinPath.split('.'); 79 | 80 | _each(res, v => resIdMap[v._id] = v); 81 | _each(docsWrapped, wrap => { 82 | _each(wrap.lookupResult, branch => { 83 | if (branch.value) { 84 | // `findModTarget` will modify `keyparts`. So, it should 85 | // be copied each time. 86 | const keyparts = initKeyparts.slice(); 87 | const target = findModTarget(wrap.doc, keyparts, { 88 | noCreate: false, 89 | forbidArray: false, 90 | arrayIndices: branch.arrayIndices, 91 | }); 92 | const field = keyparts[keyparts.length - 1]; 93 | 94 | if (_check.array(branch.value)) { 95 | target[field] = _map(branch.value, id => resIdMap[id]); 96 | } else { 97 | target[field] = resIdMap[branch.value] || null; 98 | } 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | 105 | export const joinObj = { 106 | method: function(obj, options = {}) { 107 | invariant( 108 | _check.object(obj), 109 | 'joinObj(...): argument must be an object' 110 | ); 111 | 112 | this._addPipeline('joinObj', obj, options); 113 | return this; 114 | }, 115 | 116 | process: function(docs, pipeObj, cursor) { 117 | if (!docs) { 118 | return Promise.resolve(docs); 119 | } else { 120 | const obj = pipeObj.value; 121 | const options = pipeObj.args[0] || {}; 122 | const isObj = !_check.array(docs); 123 | docs = !isObj ? docs : [docs]; 124 | 125 | const joinerFn = (dcs) => _map(_keys(obj), k => { 126 | const { model, joinPath } = _getJoinOptions(k, obj[k]); 127 | const { allIds, docsWrapped } = _decomposeDocuments(docs, k); 128 | 129 | const execFnName = options.observe ? 'observe' : 'then'; 130 | return model.find({_id: {$in: allIds}})[execFnName](res => { 131 | _joinDocsWithResult(joinPath, res, docsWrapped); 132 | }); 133 | }); 134 | 135 | const newPipeObj = { ...pipeObj, value: joinerFn }; 136 | return joinAll 137 | .process(docs, newPipeObj, cursor) 138 | .then(res => isObj ? res[0] : res); 139 | } 140 | }, 141 | }; 142 | -------------------------------------------------------------------------------- /dist/StorageManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.StorageManager = undefined; 9 | 10 | var _keys2 = require('fast.js/object/keys'); 11 | 12 | var _keys3 = _interopRequireDefault(_keys2); 13 | 14 | var _eventemitter = require('eventemitter3'); 15 | 16 | var _eventemitter2 = _interopRequireDefault(_eventemitter); 17 | 18 | var _PromiseQueue = require('./PromiseQueue'); 19 | 20 | var _PromiseQueue2 = _interopRequireDefault(_PromiseQueue); 21 | 22 | var _EJSON = require('./EJSON'); 23 | 24 | var _EJSON2 = _interopRequireDefault(_EJSON); 25 | 26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 27 | 28 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 29 | 30 | /** 31 | * Manager for dealing with backend storage 32 | * of the daatabase. Default implementation uses 33 | * memory. You can implement the same interface 34 | * and use another storage (with levelup, for example) 35 | */ 36 | 37 | var StorageManager = exports.StorageManager = function () { 38 | function StorageManager(db) { 39 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 40 | 41 | _classCallCheck(this, StorageManager); 42 | 43 | this.db = db; 44 | this.options = options; 45 | this._queue = new _PromiseQueue2.default(1); 46 | this._storage = {}; 47 | this.reload(); 48 | } 49 | 50 | _createClass(StorageManager, [{ 51 | key: 'loaded', 52 | value: function loaded() { 53 | return this._loadedPromise; 54 | } 55 | }, { 56 | key: 'reload', 57 | value: function reload() { 58 | var _this = this; 59 | 60 | if (this._loadedPromise) { 61 | this._loadedPromise = this._loadedPromise.then(function () { 62 | return _this._loadStorage(); 63 | }); 64 | } else { 65 | this._loadedPromise = this._loadStorage(); 66 | } 67 | return this.loaded(); 68 | } 69 | }, { 70 | key: 'destroy', 71 | value: function destroy() { 72 | var _this2 = this; 73 | 74 | return this.loaded().then(function () { 75 | _this2._storage = {}; 76 | }); 77 | } 78 | }, { 79 | key: 'persist', 80 | value: function persist(key, value) { 81 | var _this3 = this; 82 | 83 | return this.loaded().then(function () { 84 | _this3._storage[key] = _EJSON2.default.clone(value); 85 | }); 86 | } 87 | }, { 88 | key: 'delete', 89 | value: function _delete(key) { 90 | var _this4 = this; 91 | 92 | return this.loaded().then(function () { 93 | delete _this4._storage[key]; 94 | }); 95 | } 96 | }, { 97 | key: 'get', 98 | value: function get(key) { 99 | var _this5 = this; 100 | 101 | return this.loaded().then(function () { 102 | return _this5._storage[key]; 103 | }); 104 | } 105 | }, { 106 | key: 'createReadStream', 107 | value: function createReadStream() { 108 | var _this6 = this; 109 | 110 | var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 111 | 112 | // Very limited subset of ReadableStream 113 | var paused = false; 114 | var emitter = new _eventemitter2.default(); 115 | emitter.pause = function () { 116 | return paused = true; 117 | }; 118 | 119 | this.loaded().then(function () { 120 | var keys = (0, _keys3.default)(_this6._storage); 121 | for (var i = 0; i < keys.length; i++) { 122 | emitter.emit('data', { value: _this6._storage[keys[i]] }); 123 | if (paused) { 124 | return; 125 | } 126 | } 127 | emitter.emit('end'); 128 | }); 129 | 130 | return emitter; 131 | } 132 | }, { 133 | key: '_loadStorage', 134 | value: function _loadStorage() { 135 | this._storage = {}; 136 | return Promise.resolve(); 137 | } 138 | }]); 139 | 140 | return StorageManager; 141 | }(); 142 | 143 | exports.default = StorageManager; -------------------------------------------------------------------------------- /dist/PromiseQueue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | var _try2 = require('fast.js/function/try'); 10 | 11 | var _try3 = _interopRequireDefault(_try2); 12 | 13 | var _doubleEndedQueue = require('double-ended-queue'); 14 | 15 | var _doubleEndedQueue2 = _interopRequireDefault(_doubleEndedQueue); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | /** 22 | * It limits concurrently executed promises 23 | * 24 | * @param {Number} [maxPendingPromises=Infinity] max number of concurrently executed promises 25 | * @param {Number} [maxQueuedPromises=Infinity] max number of queued promises 26 | * @constructor 27 | */ 28 | 29 | var PromiseQueue = function () { 30 | function PromiseQueue() { 31 | var maxPendingPromises = arguments.length <= 0 || arguments[0] === undefined ? Infinity : arguments[0]; 32 | var maxQueuedPromises = arguments.length <= 1 || arguments[1] === undefined ? Infinity : arguments[1]; 33 | 34 | _classCallCheck(this, PromiseQueue); 35 | 36 | this.pendingPromises = 0; 37 | this.maxPendingPromises = maxPendingPromises; 38 | this.maxQueuedPromises = maxQueuedPromises; 39 | this.queue = new _doubleEndedQueue2.default(); 40 | this.length = 0; 41 | } 42 | 43 | /** 44 | * Pause queue processing 45 | */ 46 | 47 | _createClass(PromiseQueue, [{ 48 | key: 'pause', 49 | value: function pause() { 50 | this._paused = true; 51 | } 52 | 53 | /** 54 | * Resume queue processing 55 | */ 56 | 57 | }, { 58 | key: 'unpause', 59 | value: function unpause() { 60 | this._paused = false; 61 | this._dequeue(); 62 | } 63 | 64 | /** 65 | * Adds new promise generator in the queue 66 | * @param {Function} promiseGenerator 67 | */ 68 | 69 | }, { 70 | key: 'add', 71 | value: function add(promiseGenerator) { 72 | var _this = this; 73 | 74 | var unshift = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; 75 | 76 | return new Promise(function (resolve, reject) { 77 | if (_this.length >= _this.maxQueuedPromises) { 78 | reject(new Error('Queue limit reached')); 79 | } else { 80 | var queueItem = { 81 | promiseGenerator: promiseGenerator, 82 | resolve: resolve, 83 | reject: reject 84 | }; 85 | 86 | if (!unshift) { 87 | _this.queue.push(queueItem); 88 | } else { 89 | _this.queue.unshift(queueItem); 90 | } 91 | 92 | _this.length += 1; 93 | _this._dequeue(); 94 | } 95 | }); 96 | } 97 | 98 | /** 99 | * Internal queue processor. Starts processing of 100 | * the next queued function 101 | * @return {Boolean} 102 | */ 103 | 104 | }, { 105 | key: '_dequeue', 106 | value: function _dequeue() { 107 | var _this2 = this; 108 | 109 | if (this._paused || this.pendingPromises >= this.maxPendingPromises) { 110 | return false; 111 | } 112 | 113 | var item = this.queue.shift(); 114 | if (!item) { 115 | return false; 116 | } 117 | 118 | var result = (0, _try3.default)(function () { 119 | _this2.pendingPromises++; 120 | return Promise.resolve().then(function () { 121 | return item.promiseGenerator(); 122 | }).then(function (value) { 123 | _this2.length--; 124 | _this2.pendingPromises--; 125 | item.resolve(value); 126 | _this2._dequeue(); 127 | }, function (err) { 128 | _this2.length--; 129 | _this2.pendingPromises--; 130 | item.reject(err); 131 | _this2._dequeue(); 132 | }); 133 | }); 134 | 135 | if (result instanceof Error) { 136 | this.length--; 137 | this.pendingPromises--; 138 | item.reject(result); 139 | this._dequeue(); 140 | } 141 | 142 | return true; 143 | } 144 | }]); 145 | 146 | return PromiseQueue; 147 | }(); 148 | 149 | exports.default = PromiseQueue; -------------------------------------------------------------------------------- /dist/Base64.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | /** 12 | * Based on Meteor's Base64 package. 13 | * Rewrite with ES6 and better formated for passing 14 | * linter 15 | */ 16 | var BASE_64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 17 | var BASE_64_VALS = {}; 18 | 19 | (function setupBase64Vals() { 20 | for (var j = 0; j < BASE_64_CHARS.length; j++) { 21 | BASE_64_VALS[BASE_64_CHARS.charAt(j)] = j; 22 | } 23 | })(); 24 | 25 | var getChar = function getChar(val) { 26 | return BASE_64_CHARS.charAt(val); 27 | }; 28 | 29 | var getVal = function getVal(ch) { 30 | if (ch === '=') { 31 | return -1; 32 | } 33 | return BASE_64_VALS[ch]; 34 | }; 35 | 36 | // Base 64 encoding 37 | 38 | var Base64 = exports.Base64 = function () { 39 | function Base64() { 40 | _classCallCheck(this, Base64); 41 | } 42 | 43 | _createClass(Base64, [{ 44 | key: 'encode', 45 | value: function encode(array) { 46 | if (typeof array === 'string') { 47 | var str = array; 48 | array = this.newBinary(str.length); 49 | for (var i = 0; i < str.length; i++) { 50 | var ch = str.charCodeAt(i); 51 | if (ch > 0xFF) { 52 | throw new Error('Not ascii. Base64.encode can only take ascii strings.'); 53 | } 54 | array[i] = ch; 55 | } 56 | } 57 | 58 | var answer = []; 59 | var a = null; 60 | var b = null; 61 | var c = null; 62 | var d = null; 63 | for (var i = 0; i < array.length; i++) { 64 | switch (i % 3) { 65 | case 0: 66 | a = array[i] >> 2 & 0x3F; 67 | b = (array[i] & 0x03) << 4; 68 | break; 69 | case 1: 70 | b |= array[i] >> 4 & 0xF; 71 | c = (array[i] & 0xF) << 2; 72 | break; 73 | case 2: 74 | c |= array[i] >> 6 & 0x03; 75 | d = array[i] & 0x3F; 76 | answer.push(getChar(a)); 77 | answer.push(getChar(b)); 78 | answer.push(getChar(c)); 79 | answer.push(getChar(d)); 80 | a = null; 81 | b = null; 82 | c = null; 83 | d = null; 84 | break; 85 | } 86 | } 87 | if (a != null) { 88 | answer.push(getChar(a)); 89 | answer.push(getChar(b)); 90 | if (c == null) { 91 | answer.push('='); 92 | } else { 93 | answer.push(getChar(c)); 94 | } 95 | if (d == null) { 96 | answer.push('='); 97 | } 98 | } 99 | return answer.join(''); 100 | } 101 | }, { 102 | key: 'decode', 103 | value: function decode(str) { 104 | var len = Math.floor(str.length * 3 / 4); 105 | if (str.charAt(str.length - 1) == '=') { 106 | len--; 107 | if (str.charAt(str.length - 2) == '=') { 108 | len--; 109 | } 110 | } 111 | var arr = this.newBinary(len); 112 | 113 | var one = null; 114 | var two = null; 115 | var three = null; 116 | 117 | var j = 0; 118 | 119 | for (var i = 0; i < str.length; i++) { 120 | var c = str.charAt(i); 121 | var v = getVal(c); 122 | switch (i % 4) { 123 | case 0: 124 | if (v < 0) { 125 | throw new Error('invalid base64 string'); 126 | } 127 | one = v << 2; 128 | break; 129 | case 1: 130 | if (v < 0) { 131 | throw new Error('invalid base64 string'); 132 | } 133 | one |= v >> 4; 134 | arr[j++] = one; 135 | two = (v & 0x0F) << 4; 136 | break; 137 | case 2: 138 | if (v >= 0) { 139 | two |= v >> 2; 140 | arr[j++] = two; 141 | three = (v & 0x03) << 6; 142 | } 143 | break; 144 | case 3: 145 | if (v >= 0) { 146 | arr[j++] = three | v; 147 | } 148 | break; 149 | } 150 | } 151 | return arr; 152 | } 153 | }, { 154 | key: 'newBinary', 155 | value: function newBinary(len) { 156 | if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') { 157 | var ret = []; 158 | for (var i = 0; i < len; i++) { 159 | ret.push(0); 160 | } 161 | ret.$Uint8ArrayPolyfill = true; 162 | return ret; 163 | } 164 | return new Uint8Array(new ArrayBuffer(len)); 165 | } 166 | }]); 167 | 168 | return Base64; 169 | }(); 170 | 171 | exports.default = new Base64(); -------------------------------------------------------------------------------- /dist/AsyncEventEmitter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | var _eventemitter = require('eventemitter3'); 10 | 11 | var _eventemitter2 = _interopRequireDefault(_eventemitter); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 16 | 17 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 18 | 19 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 20 | 21 | /** 22 | * Extension of a regular EventEmitter that provides a method 23 | * that returns a Promise then resolved when all listeners of the event 24 | * will be resolved. 25 | */ 26 | /* istanbul ignore next */ 27 | 28 | var AsyncEventEmitter = function (_EventEmitter) { 29 | _inherits(AsyncEventEmitter, _EventEmitter); 30 | 31 | function AsyncEventEmitter() { 32 | _classCallCheck(this, AsyncEventEmitter); 33 | 34 | return _possibleConstructorReturn(this, Object.getPrototypeOf(AsyncEventEmitter).apply(this, arguments)); 35 | } 36 | 37 | _createClass(AsyncEventEmitter, [{ 38 | key: 'emitAsync', 39 | 40 | /** 41 | * Emit an event and return a Promise that will be resolved 42 | * when all listeren's Promises will be resolved. 43 | * @param {String} event 44 | * @return {Promise} 45 | */ 46 | value: function emitAsync(event, a1, a2, a3, a4, a5) { 47 | var prefix = _eventemitter2.default.prefixed; 48 | var evt = prefix ? prefix + event : event; 49 | 50 | if (!this._events || !this._events[evt]) { 51 | return Promise.resolve(); 52 | } 53 | 54 | var i = undefined; 55 | var listeners = this._events[evt]; 56 | var len = arguments.length; 57 | var args = undefined; 58 | 59 | if ('function' === typeof listeners.fn) { 60 | if (listeners.once) { 61 | this.removeListener(event, listeners.fn, undefined, true); 62 | } 63 | 64 | switch (len) { 65 | case 1: 66 | return Promise.resolve(listeners.fn.call(listeners.context)); 67 | case 2: 68 | return Promise.resolve(listeners.fn.call(listeners.context, a1)); 69 | case 3: 70 | return Promise.resolve(listeners.fn.call(listeners.context, a1, a2)); 71 | case 4: 72 | return Promise.resolve(listeners.fn.call(listeners.context, a1, a2, a3)); 73 | case 5: 74 | return Promise.resolve(listeners.fn.call(listeners.context, a1, a2, a3, a4)); 75 | case 6: 76 | return Promise.resolve(listeners.fn.call(listeners.context, a1, a2, a3, a4, a5)); 77 | } 78 | 79 | for (i = 1, args = new Array(len - 1); i < len; i++) { 80 | args[i - 1] = arguments[i]; 81 | } 82 | 83 | return Promise.resolve(listeners.fn.apply(listeners.context, args)); 84 | } else { 85 | var promises = []; 86 | var length = listeners.length; 87 | var j = undefined; 88 | 89 | for (i = 0; i < length; i++) { 90 | if (listeners[i].once) { 91 | this.removeListener(event, listeners[i].fn, undefined, true); 92 | } 93 | 94 | switch (len) { 95 | case 1: 96 | promises.push(Promise.resolve(listeners[i].fn.call(listeners[i].context)));break; 97 | case 2: 98 | promises.push(Promise.resolve(listeners[i].fn.call(listeners[i].context, a1)));break; 99 | case 3: 100 | promises.push(Promise.resolve(listeners[i].fn.call(listeners[i].context, a1, a2)));break; 101 | default: 102 | if (!args) { 103 | for (j = 1, args = new Array(len - 1); j < len; j++) { 104 | args[j - 1] = arguments[j]; 105 | } 106 | } 107 | promises.push(Promise.resolve(listeners[i].fn.apply(listeners[i].context, args))); 108 | } 109 | } 110 | 111 | return Promise.all(promises); 112 | } 113 | } 114 | }]); 115 | 116 | return AsyncEventEmitter; 117 | }(_eventemitter2.default); 118 | 119 | exports.default = AsyncEventEmitter; -------------------------------------------------------------------------------- /test/both/DocumentProjector.test.js: -------------------------------------------------------------------------------- 1 | import EJSON from '../../lib/EJSON'; 2 | import DocumentProjector, * as utils from '../../lib/DocumentProjector'; 3 | import chai, {except, assert} from 'chai'; 4 | import _ from 'lodash'; 5 | chai.use(require('chai-as-promised')); 6 | chai.should(); 7 | 8 | 9 | describe('DocumentProjector', function () { 10 | describe('projection_compiler', function () { 11 | var testProjection = function (projection, tests) { 12 | var projector = new DocumentProjector(projection); 13 | _.each(tests, function (testCase) { 14 | it(testCase[2], function() { 15 | assert.deepEqual( 16 | projector.project(testCase[0]), 17 | testCase[1], 18 | testCase[2] 19 | ); 20 | }); 21 | }); 22 | }; 23 | 24 | var testCompileProjectionThrows = function (projection, expectedError) { 25 | it('should throw an error', function () { 26 | assert.throws(function () { 27 | new DocumentProjector(projection) 28 | }, expectedError); 29 | }); 30 | }; 31 | 32 | testProjection({ 'foo': 1, 'bar': 1 }, [ 33 | [{ foo: 42, bar: "something", baz: "else" }, 34 | { foo: 42, bar: "something" }, 35 | "simplest - whitelist"], 36 | 37 | [{ foo: { nested: 17 }, baz: {} }, 38 | { foo: { nested: 17 } }, 39 | "nested whitelisted field"], 40 | 41 | [{ _id: "uid", bazbaz: 42 }, 42 | { _id: "uid" }, 43 | "simplest whitelist - preserve _id"] 44 | ]); 45 | 46 | testProjection({ 'foo': 0, 'bar': 0 }, [ 47 | [{ foo: 42, bar: "something", baz: "else" }, 48 | { baz: "else" }, 49 | "simplest - blacklist"], 50 | 51 | [{ foo: { nested: 17 }, baz: { foo: "something" } }, 52 | { baz: { foo: "something" } }, 53 | "nested blacklisted field"], 54 | 55 | [{ _id: "uid", bazbaz: 42 }, 56 | { _id: "uid", bazbaz: 42 }, 57 | "simplest blacklist - preserve _id"] 58 | ]); 59 | 60 | testProjection({ _id: 0, foo: 1 }, [ 61 | [{ foo: 42, bar: 33, _id: "uid" }, 62 | { foo: 42 }, 63 | "whitelist - _id blacklisted"] 64 | ]); 65 | 66 | testProjection({ _id: 0, foo: 0 }, [ 67 | [{ foo: 42, bar: 33, _id: "uid" }, 68 | { bar: 33 }, 69 | "blacklist - _id blacklisted"] 70 | ]); 71 | 72 | testProjection({ 'foo.bar.baz': 1 }, [ 73 | [{ foo: { meh: "fur", bar: { baz: 42 }, tr: 1 }, bar: 33, baz: 'trolololo' }, 74 | { foo: { bar: { baz: 42 } } }, 75 | "whitelist nested"], 76 | 77 | // Behavior of this test is looked up in actual mongo 78 | [{ foo: { meh: "fur", bar: "nope", tr: 1 }, bar: 33, baz: 'trolololo' }, 79 | { foo: {} }, 80 | "whitelist nested - path not found in doc, different type"], 81 | 82 | // Behavior of this test is looked up in actual mongo 83 | [{ foo: { meh: "fur", bar: [], tr: 1 }, bar: 33, baz: 'trolololo' }, 84 | { foo: { bar: [] } }, 85 | "whitelist nested - path not found in doc"] 86 | ]); 87 | 88 | testProjection({ 'hope.humanity': 0, 'hope.people': 0 }, [ 89 | [{ hope: { humanity: "lost", people: 'broken', candies: 'long live!' } }, 90 | { hope: { candies: 'long live!' } }, 91 | "blacklist nested"], 92 | 93 | [{ hope: "new" }, 94 | { hope: "new" }, 95 | "blacklist nested - path not found in doc"] 96 | ]); 97 | 98 | testProjection({ _id: 1 }, [ 99 | [{ _id: 42, x: 1, y: { z: "2" } }, 100 | { _id: 42 }, 101 | "_id whitelisted"], 102 | [{ _id: 33 }, 103 | { _id: 33 }, 104 | "_id whitelisted, _id only"], 105 | [{ x: 1 }, 106 | {}, 107 | "_id whitelisted, no _id"] 108 | ]); 109 | 110 | testProjection({ _id: 0 }, [ 111 | [{ _id: 42, x: 1, y: { z: "2" } }, 112 | { x: 1, y: { z: "2" } }, 113 | "_id blacklisted"], 114 | [{ _id: 33 }, 115 | {}, 116 | "_id blacklisted, _id only"], 117 | [{ x: 1 }, 118 | { x: 1 }, 119 | "_id blacklisted, no _id"] 120 | ]); 121 | 122 | testProjection({}, [ 123 | [{ a: 1, b: 2, c: "3" }, 124 | { a: 1, b: 2, c: "3" }, 125 | "empty projection"] 126 | ]); 127 | 128 | testCompileProjectionThrows( 129 | { 'inc': 1, 'excl': 0 }, 130 | "You cannot currently mix including and excluding fields"); 131 | testCompileProjectionThrows( 132 | { _id: 1, a: 0 }, 133 | "You cannot currently mix including and excluding fields"); 134 | 135 | testCompileProjectionThrows( 136 | { 'a': 1, 'a.b': 1 }, 137 | "using both of them may trigger unexpected behavior"); 138 | testCompileProjectionThrows( 139 | { 'a.b.c': 1, 'a.b': 1, 'a': 1 }, 140 | "using both of them may trigger unexpected behavior"); 141 | 142 | testCompileProjectionThrows("some string", "fields option must be an object"); 143 | }); 144 | 145 | 146 | it('shoutl copy document', function () { 147 | // Compiled fields projection defines the contract: returned document doesn't 148 | // retain anything from the passed argument. 149 | var doc = { 150 | a: { x: 42 }, 151 | b: { 152 | y: { z: 33 } 153 | }, 154 | c: "asdf" 155 | }; 156 | 157 | var fields = { 158 | 'a': 1, 159 | 'b.y': 1 160 | }; 161 | 162 | var projectionFn = new DocumentProjector(fields); 163 | var filteredDoc = projectionFn.project(doc); 164 | doc.a.x++; 165 | doc.b.y.z--; 166 | assert.equal(filteredDoc.a.x, 42, "projection returning deep copy - including"); 167 | assert.equal(filteredDoc.b.y.z, 33, "projection returning deep copy - including"); 168 | 169 | fields = { c: 0 }; 170 | projectionFn = new DocumentProjector(fields); 171 | filteredDoc = projectionFn.project(doc); 172 | 173 | doc.a.x = 5; 174 | assert.equal(filteredDoc.a.x, 43, "projection returning deep copy - excluding"); 175 | }); 176 | 177 | }); 178 | -------------------------------------------------------------------------------- /lib/IndexManager.js: -------------------------------------------------------------------------------- 1 | import _bind from 'fast.js/function/bind'; 2 | import _keys from 'fast.js/object/keys'; 3 | import _each from 'fast.js/forEach'; 4 | import _map from 'fast.js/map'; 5 | import invariant from 'invariant'; 6 | import PromiseQueue from './PromiseQueue'; 7 | import CollectionIndex from './CollectionIndex'; 8 | import DocumentRetriver from './DocumentRetriver'; 9 | 10 | 11 | /** 12 | * Manager for controlling a list of indexes 13 | * for some model. Building indexes is promise 14 | * based. 15 | * By default it creates an index for `_id` field. 16 | */ 17 | /* istanbul ignore next */ 18 | export class IndexManager { 19 | constructor(db, options = {}) { 20 | this.db = db; 21 | this.indexes = {}; 22 | this._queue = new PromiseQueue(options.concurrency || 2); 23 | 24 | // By default ensure index by _id field 25 | this.ensureIndex({ 26 | fieldName: '_id', 27 | unique: true, 28 | }); 29 | } 30 | 31 | /** 32 | * Check index existance for given `options.fieldName` and 33 | * if index not exists it creates new one. 34 | * Always return a promise that resolved only when 35 | * index succesfully created, built and ready for working with. 36 | * If `options.forceRebuild` provided and equals to true then 37 | * existing index will be rebuilt, otherwise existing index 38 | * don't touched. 39 | * 40 | * @param {Object} options.fieldName name of the field for indexing 41 | * @param {Object} options.forceRebuild rebuild index if it exists 42 | * @return {Promise} 43 | */ 44 | ensureIndex(options) { 45 | invariant( 46 | options && options.fieldName, 47 | 'You must specify a fieldName in options object' 48 | ); 49 | 50 | const key = options.fieldName; 51 | if (!this.indexes[key]) { 52 | this.indexes[key] = new CollectionIndex(options); 53 | return this.buildIndex(key); 54 | } else if (this.indexes[key].buildPromise) { 55 | return this.indexes[key].buildPromise; 56 | } else if (options && options.forceRebuild) { 57 | return this.buildIndex(key); 58 | } else { 59 | return Promise.resolve(); 60 | } 61 | } 62 | 63 | /** 64 | * Buld an existing index (ensured) and return a 65 | * promise that will be resolved only when index successfully 66 | * built for all documents in the storage. 67 | * @param {String} key 68 | * @return {Promise} 69 | */ 70 | buildIndex(key) { 71 | invariant( 72 | this.indexes[key], 73 | 'Index with key `%s` does not ensured yet', 74 | key 75 | ); 76 | 77 | const cleanup = () => this.indexes[key].buildPromise = null; 78 | const buildPromise = this._queue.add( 79 | _bind(this._doBuildIndex, this, key) 80 | ).then(cleanup, cleanup); 81 | 82 | this.indexes[key].buildPromise = buildPromise; 83 | return buildPromise; 84 | } 85 | 86 | /** 87 | * Schedule a task for each index in the 88 | * manager. Return promise that will be resolved 89 | * when all indexes is successfully built. 90 | * @return {Promise} 91 | */ 92 | buildAllIndexes() { 93 | return Promise.all( 94 | _map(this.indexes, (v, k) => { 95 | return this.ensureIndex({ 96 | fieldName: k, 97 | forceRebuild: true, 98 | }); 99 | }) 100 | ); 101 | } 102 | 103 | /** 104 | * Remove an index 105 | * @param {String} key 106 | * @return {Promise} 107 | */ 108 | removeIndex(key) { 109 | return this._queue.add(() => { 110 | delete this.indexes[key]; 111 | }); 112 | } 113 | 114 | /** 115 | * Add a document to all indexes 116 | * @param {Object} doc 117 | * @return {Promise} 118 | */ 119 | indexDocument(doc) { 120 | return this._queue.add(() => { 121 | const keys = _keys(this.indexes); 122 | let failingIndex = null; 123 | try { 124 | _each(keys, (k, i) => { 125 | failingIndex = i; 126 | this.indexes[k].insert(doc); 127 | }); 128 | } catch (e) { 129 | _each(keys.slice(0, failingIndex), (k) => { 130 | this.indexes[k].remove(doc); 131 | }); 132 | throw e; 133 | } 134 | }); 135 | } 136 | 137 | /** 138 | * Update all indexes with new version of 139 | * the document 140 | * @param {Object} oldDoc 141 | * @param {Object} newDoc 142 | * @return {Promise} 143 | */ 144 | reindexDocument(oldDoc, newDoc) { 145 | return this._queue.add(() => { 146 | const keys = _keys(this.indexes); 147 | let failingIndex = null; 148 | try { 149 | _each(keys, (k, i) => { 150 | failingIndex = i; 151 | this.indexes[k].update(oldDoc, newDoc); 152 | }); 153 | } catch (e) { 154 | _each(keys.slice(0, failingIndex), (k) => { 155 | this.indexes[k].revertUpdate(oldDoc, newDoc); 156 | }); 157 | throw e; 158 | } 159 | }); 160 | } 161 | 162 | /** 163 | * Remove document from all indexes 164 | * @param {Object} doc 165 | * @return {Promise} 166 | */ 167 | deindexDocument(doc) { 168 | return this._queue.add(() => { 169 | const keys = _keys(this.indexes); 170 | _each(keys, (k) => { 171 | this.indexes[k].remove(doc); 172 | }); 173 | }); 174 | } 175 | 176 | /** 177 | * Build an existing index with reseting first 178 | * @param {String} key 179 | * @return {Promise} 180 | */ 181 | _doBuildIndex(key) { 182 | // Get and reset index 183 | const index = this.indexes[key]; 184 | index.reset(); 185 | 186 | // Loop through all doucments in the storage 187 | const errors = []; 188 | return new DocumentRetriver(this.db) 189 | .retriveAll().then((docs) => { 190 | _each(docs, doc => { 191 | try { 192 | index.insert(doc); 193 | } catch (e) { 194 | errors.push([e, doc]); 195 | } 196 | }); 197 | 198 | if (errors.length > 0) { 199 | throw new Error('Index build failed with errors: ', errors); 200 | } 201 | }); 202 | } 203 | } 204 | 205 | export default IndexManager; 206 | -------------------------------------------------------------------------------- /test/both/DocumentRetriver.test.js: -------------------------------------------------------------------------------- 1 | import Collection from '../../lib/Collection'; 2 | import DocumentRetriver from '../../lib/DocumentRetriver'; 3 | import chai from 'chai'; 4 | import sinon from 'sinon'; 5 | chai.use(require('chai-as-promised')); 6 | chai.use(require('sinon-chai')); 7 | chai.should(); 8 | 9 | 10 | 11 | describe('DocumentRetriver', () => { 12 | let db, retr; 13 | beforeEach(function () { 14 | db = new Collection('test') 15 | retr = new DocumentRetriver(db); 16 | return Promise.all([ 17 | db.insert({a: 1, _id: '1'}), 18 | db.insert({a: 2, _id: '2'}), 19 | db.insert({a: 3, _id: '3'}), 20 | ]); 21 | }); 22 | 23 | describe('#retriveForQeury', function () { 24 | it('should retrive one by id if given an id', function () { 25 | return Promise.all([ 26 | retr.retriveForQeury('1').should.eventually.be.deep.equal([{a: 1, _id: '1'}]), 27 | ]); 28 | }); 29 | 30 | it('should retrive one by id if given only object with id', function () { 31 | return Promise.all([ 32 | retr.retriveForQeury({_id: '1'}).should.eventually.be.deep.equal([{a: 1, _id: '1'}]), 33 | ]); 34 | }); 35 | 36 | it('should retrive multiple by given list of ids', function () { 37 | return Promise.all([ 38 | retr.retriveForQeury({_id: {$in: ['1', '2']}}).should.eventually 39 | .be.deep.equal([{a: 1, _id: '1'}, {a: 2, _id: '2'}]), 40 | ]); 41 | }); 42 | 43 | it('should retrive all if no ids provided', function () { 44 | return Promise.all([ 45 | retr.retriveForQeury({}).should.eventually.have.length(3), 46 | retr.retriveForQeury().should.eventually.have.length(3), 47 | retr.retriveForQeury(null).should.eventually.have.length(3), 48 | retr.retriveForQeury(undefined).should.eventually.have.length(3), 49 | retr.retriveForQeury({a: {$gte: 1}}).should.eventually.have.length(3), 50 | retr.retriveForQeury({a: {$gte: 2}}).should.eventually.have.length(3), 51 | retr.retriveForQeury({a: {$gte: 3}}).should.eventually.have.length(3), 52 | retr.retriveForQeury({_id: null}).should.eventually.have.length(3), 53 | retr.retriveForQeury({_id: undefined}).should.eventually.have.length(3), 54 | retr.retriveForQeury({_id: {$in: []}}).should.eventually.have.length(3), 55 | ]); 56 | }); 57 | 58 | it('should use queryFilter for filtering documents', function () { 59 | retr.retriveForQeury({}, (d) => d._id === '1').should.eventually.have.length(1); 60 | }); 61 | }); 62 | 63 | describe('#retriveIds', function () { 64 | it('should retrive only documents by id', function () { 65 | return Promise.all([ 66 | retr.retriveIds(undefined, []).should.eventually.be.deep.equal([]), 67 | retr.retriveIds(undefined, ['1']).should.eventually.be.deep.equal([{a: 1, _id: '1'}]), 68 | retr.retriveIds(undefined, ['2', '1']).should.eventually 69 | .be.deep.equal([{a: 2, _id: '2'}, {a: 1, _id: '1'}]), 70 | ]); 71 | }); 72 | }); 73 | 74 | describe('#retriveAll', function () { 75 | it('should retrive all', () => { 76 | const db = new Collection('test'); 77 | return Promise.all([ 78 | db.storage.persist('1', {_id: '1', a: 1}), 79 | db.storage.persist('2', {_id: '2', a: 2}), 80 | db.storage.persist('3', {_id: '3', a: 3}), 81 | db.storage.persist('4', {_id: '4', a: 4}), 82 | db.storage.persist('5', {_id: '5', a: 5}), 83 | db.storage.persist('6', {_id: '6', a: 6}) 84 | ]).then(() => { 85 | const retr = new DocumentRetriver(db); 86 | return retr.retriveAll().should.eventually.deep.equal([ 87 | {_id: '1', a: 1}, 88 | {_id: '2', a: 2}, 89 | {_id: '3', a: 3}, 90 | {_id: '4', a: 4}, 91 | {_id: '5', a: 5}, 92 | {_id: '6', a: 6}, 93 | ]); 94 | }); 95 | }); 96 | 97 | it('should filter documents by queryFilter', function () { 98 | const db = new Collection('test'); 99 | return Promise.all([ 100 | db.storage.persist('1', {_id: '1', a: 1}), 101 | db.storage.persist('2', {_id: '2', a: 2}), 102 | db.storage.persist('3', {_id: '3', a: 3}), 103 | db.storage.persist('4', {_id: '4', a: 4}), 104 | db.storage.persist('5', {_id: '5', a: 5}), 105 | db.storage.persist('6', {_id: '6', a: 6}) 106 | ]).then(() => { 107 | const retr = new DocumentRetriver(db); 108 | const qf = (d) => d._id === '1' || d._id === '2'; 109 | return retr.retriveAll(qf).should.eventually.deep.equal([ 110 | {_id: '1', a: 1}, 111 | {_id: '2', a: 2}, 112 | ]); 113 | }); 114 | }); 115 | 116 | it('should limit the result of passed docs', function () { 117 | const db = new Collection('test'); 118 | return Promise.all([ 119 | db.storage.persist('1', {_id: '1', a: 1}), 120 | db.storage.persist('2', {_id: '2', a: 2}), 121 | db.storage.persist('3', {_id: '3', a: 3}), 122 | db.storage.persist('4', {_id: '4', a: 4}), 123 | db.storage.persist('5', {_id: '5', a: 5}), 124 | db.storage.persist('6', {_id: '6', a: 6}) 125 | ]).then(() => { 126 | const retr = new DocumentRetriver(db); 127 | const qf = (d) => d._id === '1' || d._id === '2'; 128 | return retr.retriveAll(qf, {limit: 1}).should.eventually.deep.equal([ 129 | {_id: '1', a: 1}, 130 | ]); 131 | }); 132 | }); 133 | }); 134 | 135 | describe('#retriveOne', function () { 136 | it('should retrive one', () => { 137 | const db = new Collection('test') 138 | return Promise.all([ 139 | db.storage.persist('1', {_id: '1', a: 1}), 140 | db.storage.persist('2', {_id: '2', a: 2}), 141 | db.storage.persist('3', {_id: '3', a: 3}), 142 | db.storage.persist('4', {_id: '4', a: 4}), 143 | db.storage.persist('5', {_id: '5', a: 5}), 144 | db.storage.persist('6', {_id: '6', a: 6}) 145 | ]).then(() => { 146 | const retr = new DocumentRetriver(db); 147 | return retr.retriveOne('1').should.eventually.deep.equal({_id: '1', a: 1}); 148 | }); 149 | }); 150 | }); 151 | 152 | }); 153 | -------------------------------------------------------------------------------- /dist/CollectionDelegate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.CollectionDelegate = undefined; 9 | 10 | var _map2 = require('fast.js/map'); 11 | 12 | var _map3 = _interopRequireDefault(_map2); 13 | 14 | var _DocumentModifier = require('./DocumentModifier'); 15 | 16 | var _DocumentModifier2 = _interopRequireDefault(_DocumentModifier); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 21 | 22 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 23 | 24 | /** 25 | * Default collection delegate for working with a 26 | * normal MarsDB approach – within a browser. 27 | */ 28 | 29 | var CollectionDelegate = exports.CollectionDelegate = function () { 30 | function CollectionDelegate(db) { 31 | _classCallCheck(this, CollectionDelegate); 32 | 33 | this.db = db; 34 | } 35 | 36 | _createClass(CollectionDelegate, [{ 37 | key: 'insert', 38 | value: function insert(doc) { 39 | var _this = this; 40 | 41 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 42 | var randomId = arguments[2]; 43 | 44 | return this.db.indexManager.indexDocument(doc).then(function () { 45 | return _this.db.storageManager.persist(doc._id, doc).then(function () { 46 | return doc._id; 47 | }); 48 | }); 49 | } 50 | }, { 51 | key: 'remove', 52 | value: function remove(query, _ref) { 53 | var _this2 = this; 54 | 55 | var _ref$sort = _ref.sort; 56 | var sort = _ref$sort === undefined ? { _id: 1 } : _ref$sort; 57 | var _ref$multi = _ref.multi; 58 | var multi = _ref$multi === undefined ? false : _ref$multi; 59 | 60 | return this.find(query, { noClone: true }).sort(sort).then(function (docs) { 61 | if (docs.length > 1 && !multi) { 62 | docs = [docs[0]]; 63 | } 64 | var removeStorgePromises = (0, _map3.default)(docs, function (d) { 65 | return _this2.db.storageManager.delete(d._id); 66 | }); 67 | var removeIndexPromises = (0, _map3.default)(docs, function (d) { 68 | return _this2.db.indexManager.deindexDocument(d); 69 | }); 70 | return Promise.all([].concat(_toConsumableArray(removeStorgePromises), _toConsumableArray(removeIndexPromises))).then(function () { 71 | return docs; 72 | }); 73 | }); 74 | } 75 | }, { 76 | key: 'update', 77 | value: function update(query, modifier, _ref2) { 78 | var _this3 = this; 79 | 80 | var _ref2$sort = _ref2.sort; 81 | var sort = _ref2$sort === undefined ? { _id: 1 } : _ref2$sort; 82 | var _ref2$multi = _ref2.multi; 83 | var multi = _ref2$multi === undefined ? false : _ref2$multi; 84 | var _ref2$upsert = _ref2.upsert; 85 | var upsert = _ref2$upsert === undefined ? false : _ref2$upsert; 86 | 87 | return this.find(query, { noClone: true }).sort(sort).then(function (docs) { 88 | if (docs.length > 1 && !multi) { 89 | docs = [docs[0]]; 90 | } 91 | return new _DocumentModifier2.default(query).modify(docs, modifier, { upsert: upsert }); 92 | }).then(function (_ref3) { 93 | var original = _ref3.original; 94 | var updated = _ref3.updated; 95 | 96 | var updateStorgePromises = (0, _map3.default)(updated, function (d) { 97 | return _this3.db.storageManager.persist(d._id, d); 98 | }); 99 | var updateIndexPromises = (0, _map3.default)(updated, function (d, i) { 100 | return _this3.db.indexManager.reindexDocument(original[i], d); 101 | }); 102 | return Promise.all([].concat(_toConsumableArray(updateStorgePromises), _toConsumableArray(updateIndexPromises))).then(function () { 103 | return { 104 | modified: updated.length, 105 | original: original, 106 | updated: updated 107 | }; 108 | }); 109 | }); 110 | } 111 | }, { 112 | key: 'find', 113 | value: function find(query) { 114 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 115 | 116 | var cursorClass = this.db.cursorClass; 117 | return new cursorClass(this.db, query, options); 118 | } 119 | }, { 120 | key: 'findOne', 121 | value: function findOne(query) { 122 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 123 | 124 | return this.find(query, options).aggregate(function (docs) { 125 | return docs[0]; 126 | }).limit(1); 127 | } 128 | }, { 129 | key: 'count', 130 | value: function count(query) { 131 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 132 | 133 | options.noClone = true; 134 | return this.find(query, options).aggregate(function (docs) { 135 | return docs.length; 136 | }); 137 | } 138 | }, { 139 | key: 'ids', 140 | value: function ids(query) { 141 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 142 | 143 | options.noClone = true; 144 | return this.find(query, options).map(function (doc) { 145 | return doc._id; 146 | }); 147 | } 148 | }]); 149 | 150 | return CollectionDelegate; 151 | }(); 152 | 153 | exports.default = CollectionDelegate; -------------------------------------------------------------------------------- /lib/Cursor.js: -------------------------------------------------------------------------------- 1 | import _each from 'fast.js/forEach'; 2 | import _assign from 'fast.js/object/assign'; 3 | import _keys from 'fast.js/object/keys'; 4 | import _map from 'fast.js/map'; 5 | import AsyncEventEmitter from './AsyncEventEmitter'; 6 | import invariant from 'invariant'; 7 | import DocumentRetriver from './DocumentRetriver'; 8 | import DocumentMatcher from './DocumentMatcher'; 9 | import DocumentSorter from './DocumentSorter'; 10 | import DocumentProjector from './DocumentProjector'; 11 | import EJSON from './EJSON'; 12 | 13 | 14 | // UUID counter for all cursors 15 | let _currentCursorId = 0; 16 | 17 | // Pipeline processors map 18 | export const PIPELINE_PROCESSORS = { 19 | ...require('./cursor-processors/filter'), 20 | ...require('./cursor-processors/sortFunc'), 21 | ...require('./cursor-processors/map'), 22 | ...require('./cursor-processors/aggregate'), 23 | ...require('./cursor-processors/reduce'), 24 | ...require('./cursor-processors/join'), 25 | ...require('./cursor-processors/joinEach'), 26 | ...require('./cursor-processors/joinAll'), 27 | ...require('./cursor-processors/joinObj'), 28 | ...require('./cursor-processors/ifNotEmpty'), 29 | }; 30 | 31 | // Create basic cursor with pipeline methods 32 | class BasicCursor extends AsyncEventEmitter {} 33 | _each(PIPELINE_PROCESSORS, (v, procName) => { 34 | BasicCursor.prototype[procName] = v.method; 35 | }); 36 | 37 | 38 | /** 39 | * Class for storing information about query 40 | * and executing it. It also have a sugar like 41 | * map/reduce, aggregation and others for making 42 | * fully customizable response 43 | */ 44 | export class Cursor extends BasicCursor { 45 | constructor(db, query = {}, options = {}) { 46 | super(); 47 | this.db = db; 48 | this.options = options; 49 | this._id = _currentCursorId++; 50 | this._query = query; 51 | this._pipeline = []; 52 | this._latestResult = null; 53 | this._childrenCursors = {}; 54 | this._parentCursors = {}; 55 | this._ensureMatcherSorter(); 56 | } 57 | 58 | skip(skip) { 59 | invariant( 60 | skip >= 0 || typeof skip === 'undefined', 61 | 'skip(...): skip must be a positive number' 62 | ); 63 | 64 | this._skip = skip; 65 | return this; 66 | } 67 | 68 | limit(limit) { 69 | invariant( 70 | limit >= 0 || typeof limit === 'undefined', 71 | 'limit(...): limit must be a positive number' 72 | ); 73 | 74 | this._limit = limit; 75 | return this; 76 | } 77 | 78 | find(query) { 79 | this._query = query; 80 | this._ensureMatcherSorter(); 81 | return this; 82 | } 83 | 84 | project(projection) { 85 | if (projection) { 86 | this._projector = new DocumentProjector(projection); 87 | } else { 88 | this._projector = null; 89 | } 90 | return this; 91 | } 92 | 93 | sort(sortObj) { 94 | invariant( 95 | typeof sortObj === 'object' || typeof sortObj === 'undefined' || Array.isArray(sortObj), 96 | 'sort(...): argument must be an object' 97 | ); 98 | 99 | this._sort = sortObj; 100 | this._ensureMatcherSorter(); 101 | return this; 102 | } 103 | 104 | exec() { 105 | this.emit('beforeExecute'); 106 | return this._createCursorPromise( 107 | this._doExecute().then((result) => { 108 | this._latestResult = result; 109 | return result; 110 | }) 111 | ); 112 | } 113 | 114 | then(resolve, reject) { 115 | return this.exec().then(resolve, reject); 116 | } 117 | 118 | _addPipeline(type, val, ...args) { 119 | invariant( 120 | type && PIPELINE_PROCESSORS[type], 121 | 'Unknown pipeline processor type %s', 122 | type 123 | ); 124 | 125 | this._pipeline.push({ 126 | type: type, 127 | value: val, 128 | args: args || [], 129 | }); 130 | return this; 131 | } 132 | 133 | _processPipeline(docs, i = 0) { 134 | const pipeObj = this._pipeline[i]; 135 | if (!pipeObj) { 136 | return Promise.resolve(docs); 137 | } else { 138 | return Promise.resolve( 139 | PIPELINE_PROCESSORS[pipeObj.type].process( 140 | docs, pipeObj, this 141 | ) 142 | ).then((result) => { 143 | if (result === '___[STOP]___') { 144 | return result; 145 | } else { 146 | return this._processPipeline(result, i + 1); 147 | } 148 | }); 149 | } 150 | } 151 | 152 | _doExecute() { 153 | return this._matchObjects() 154 | .then(docs => { 155 | let clonned; 156 | if (this.options.noClone) { 157 | clonned = docs; 158 | } else { 159 | if (!this._projector) { 160 | clonned = _map(docs, doc => EJSON.clone(doc)); 161 | } else { 162 | clonned = this._projector.project(docs); 163 | } 164 | } 165 | return this._processPipeline(clonned); 166 | }); 167 | } 168 | 169 | _matchObjects() { 170 | const withFastLimit = this._limit && !this._skip && !this._sorter; 171 | const retrOpts = withFastLimit ? { limit: this._limit } : {}; 172 | const queryFilter = (doc) => { 173 | return doc && this._matcher.documentMatches(doc).result; 174 | }; 175 | 176 | return new DocumentRetriver(this.db) 177 | .retriveForQeury(this._query, queryFilter, retrOpts) 178 | .then((results) => { 179 | if (withFastLimit) { 180 | return results; 181 | } 182 | 183 | if (this._sorter) { 184 | const comparator = this._sorter.getComparator(); 185 | results.sort(comparator); 186 | } 187 | 188 | const skip = this._skip || 0; 189 | const limit = this._limit || results.length; 190 | return results.slice(skip, limit + skip); 191 | } 192 | ); 193 | } 194 | 195 | _ensureMatcherSorter() { 196 | this._sorter = undefined; 197 | this._matcher = new DocumentMatcher(this._query || {}); 198 | 199 | if (this._matcher.hasGeoQuery || this._sort) { 200 | this._sorter = new DocumentSorter( 201 | this._sort || [], { matcher: this._matcher }); 202 | } 203 | } 204 | 205 | _trackChildCursorPromise(childCursorPromise) { 206 | const childCursor = childCursorPromise.cursor; 207 | this._childrenCursors[childCursor._id] = childCursor; 208 | childCursor._parentCursors[this._id] = this; 209 | 210 | this.once('beforeExecute', () => { 211 | delete this._childrenCursors[childCursor._id]; 212 | delete childCursor._parentCursors[this._id]; 213 | if (_keys(childCursor._parentCursors).length === 0) { 214 | childCursor.emit('beforeExecute'); 215 | } 216 | }); 217 | } 218 | 219 | _createCursorPromise(promise, mixin = {}) { 220 | return _assign({ 221 | cursor: this, 222 | then: (successFn, failFn) => { 223 | return this._createCursorPromise( 224 | promise.then(successFn, failFn), 225 | mixin 226 | ); 227 | }, 228 | }, mixin); 229 | } 230 | } 231 | 232 | export default Cursor; 233 | -------------------------------------------------------------------------------- /dist/DocumentRetriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.DocumentRetriver = undefined; 9 | 10 | var _checkTypes = require('check-types'); 11 | 12 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 13 | 14 | var _map2 = require('fast.js/map'); 15 | 16 | var _map3 = _interopRequireDefault(_map2); 17 | 18 | var _filter2 = require('fast.js/array/filter'); 19 | 20 | var _filter3 = _interopRequireDefault(_filter2); 21 | 22 | var _Document = require('./Document'); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 27 | 28 | // Internals 29 | var DEFAULT_QUERY_FILTER = function DEFAULT_QUERY_FILTER() { 30 | return true; 31 | }; 32 | 33 | /** 34 | * Class for getting data objects by given list of ids. 35 | * Promises based. It makes requests asyncronousle by 36 | * getting request frame from database. 37 | * It's not use caches, because it's a task of store. 38 | * It just retrives content by 'get' method. 39 | */ 40 | 41 | var DocumentRetriver = exports.DocumentRetriver = function () { 42 | function DocumentRetriver(db) { 43 | _classCallCheck(this, DocumentRetriver); 44 | 45 | this.db = db; 46 | } 47 | 48 | /** 49 | * Retrive an optimal superset of documents 50 | * by given query based on _id field of the query 51 | * 52 | * TODO: there is a place for indexes 53 | * 54 | * @param {Object} query 55 | * @return {Promise} 56 | */ 57 | 58 | _createClass(DocumentRetriver, [{ 59 | key: 'retriveForQeury', 60 | value: function retriveForQeury(query) { 61 | var queryFilter = arguments.length <= 1 || arguments[1] === undefined ? DEFAULT_QUERY_FILTER : arguments[1]; 62 | var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; 63 | 64 | // Try to get list of ids 65 | var selectorIds = undefined; 66 | if ((0, _Document.selectorIsId)(query)) { 67 | // fast path for scalar query 68 | selectorIds = [query]; 69 | } else if ((0, _Document.selectorIsIdPerhapsAsObject)(query)) { 70 | // also do the fast path for { _id: idString } 71 | selectorIds = [query._id]; 72 | } else if (_checkTypes2.default.object(query) && query.hasOwnProperty('_id') && _checkTypes2.default.object(query._id) && query._id.hasOwnProperty('$in') && _checkTypes2.default.array(query._id.$in)) { 73 | // and finally fast path for multiple ids 74 | // selected by $in operator 75 | selectorIds = query._id.$in; 76 | } 77 | 78 | // Retrive optimally 79 | if (_checkTypes2.default.array(selectorIds) && selectorIds.length > 0) { 80 | return this.retriveIds(queryFilter, selectorIds, options); 81 | } else { 82 | return this.retriveAll(queryFilter, options); 83 | } 84 | } 85 | 86 | /** 87 | * Rterive all ids given in constructor. 88 | * If some id is not retrived (retrived qith error), 89 | * then returned promise will be rejected with that error. 90 | * @return {Promise} 91 | */ 92 | 93 | }, { 94 | key: 'retriveAll', 95 | value: function retriveAll() { 96 | var _this = this; 97 | 98 | var queryFilter = arguments.length <= 0 || arguments[0] === undefined ? DEFAULT_QUERY_FILTER : arguments[0]; 99 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 100 | 101 | var limit = options.limit || +Infinity; 102 | var result = []; 103 | var stopped = false; 104 | 105 | return new Promise(function (resolve, reject) { 106 | var stream = _this.db.storage.createReadStream(); 107 | 108 | stream.on('data', function (data) { 109 | // After deleting of an item some storages 110 | // may return an undefined for a few times. 111 | // We need to check it there. 112 | if (!stopped && data.value) { 113 | var doc = _this.db.create(data.value); 114 | if (result.length < limit && queryFilter(doc)) { 115 | result.push(doc); 116 | } 117 | // Limit the result if storage supports it 118 | if (result.length === limit && stream.pause) { 119 | stream.pause(); 120 | resolve(result); 121 | stopped = true; 122 | } 123 | } 124 | }).on('end', function () { 125 | return !stopped && resolve(result); 126 | }); 127 | }); 128 | } 129 | 130 | /** 131 | * Rterive all ids given in constructor. 132 | * If some id is not retrived (retrived qith error), 133 | * then returned promise will be rejected with that error. 134 | * @return {Promise} 135 | */ 136 | 137 | }, { 138 | key: 'retriveIds', 139 | value: function retriveIds() { 140 | var queryFilter = arguments.length <= 0 || arguments[0] === undefined ? DEFAULT_QUERY_FILTER : arguments[0]; 141 | 142 | var _this2 = this; 143 | 144 | var ids = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; 145 | var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; 146 | 147 | var uniqIds = (0, _filter3.default)(ids, function (id, i) { 148 | return ids.indexOf(id) === i; 149 | }); 150 | var retrPromises = (0, _map3.default)(uniqIds, function (id) { 151 | return _this2.retriveOne(id); 152 | }); 153 | var limit = options.limit || +Infinity; 154 | 155 | return Promise.all(retrPromises).then(function (res) { 156 | var filteredRes = []; 157 | 158 | for (var i = 0; i < res.length; i++) { 159 | var doc = res[i]; 160 | if (doc && queryFilter(doc)) { 161 | filteredRes.push(doc); 162 | if (filteredRes.length === limit) { 163 | break; 164 | } 165 | } 166 | } 167 | 168 | return filteredRes; 169 | }); 170 | } 171 | 172 | /** 173 | * Retrive one document by given id 174 | * @param {String} id 175 | * @return {Promise} 176 | */ 177 | 178 | }, { 179 | key: 'retriveOne', 180 | value: function retriveOne(id) { 181 | var _this3 = this; 182 | 183 | return this.db.storage.get(id).then(function (buf) { 184 | return _this3.db.create(buf); 185 | }); 186 | } 187 | }]); 188 | 189 | return DocumentRetriver; 190 | }(); 191 | 192 | exports.default = DocumentRetriver; -------------------------------------------------------------------------------- /dist/cursor-processors/joinObj.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 4 | 5 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 6 | 7 | Object.defineProperty(exports, "__esModule", { 8 | value: true 9 | }); 10 | exports.joinObj = undefined; 11 | 12 | var _checkTypes = require('check-types'); 13 | 14 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 15 | 16 | var _forEach = require('fast.js/forEach'); 17 | 18 | var _forEach2 = _interopRequireDefault(_forEach); 19 | 20 | var _map2 = require('fast.js/map'); 21 | 22 | var _map3 = _interopRequireDefault(_map2); 23 | 24 | var _filter2 = require('fast.js/array/filter'); 25 | 26 | var _filter3 = _interopRequireDefault(_filter2); 27 | 28 | var _reduce2 = require('fast.js/array/reduce'); 29 | 30 | var _reduce3 = _interopRequireDefault(_reduce2); 31 | 32 | var _keys2 = require('fast.js/object/keys'); 33 | 34 | var _keys3 = _interopRequireDefault(_keys2); 35 | 36 | var _Collection = require('../Collection'); 37 | 38 | var _Collection2 = _interopRequireDefault(_Collection); 39 | 40 | var _invariant = require('invariant'); 41 | 42 | var _invariant2 = _interopRequireDefault(_invariant); 43 | 44 | var _joinAll = require('./joinAll'); 45 | 46 | var _DocumentModifier = require('../DocumentModifier'); 47 | 48 | var _DocumentMatcher = require('../DocumentMatcher'); 49 | 50 | var _Document = require('../Document'); 51 | 52 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 53 | 54 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 55 | 56 | /** 57 | * By given list of documents make mapping of joined 58 | * model ids to root document and vise versa. 59 | * @param {Array} docs 60 | * @param {String} key 61 | * @return {Object} 62 | */ 63 | function _decomposeDocuments(docs, key) { 64 | var lookupFn = (0, _DocumentMatcher.makeLookupFunction)(key); 65 | var allIds = []; 66 | 67 | var docsWrapped = (0, _map3.default)(docs, function (d) { 68 | var val = lookupFn(d); 69 | var joinIds = (0, _filter3.default)((0, _reduce3.default)((0, _map3.default)(val, function (x) { 70 | return x.value; 71 | }), function (a, b) { 72 | if (_checkTypes2.default.array(b)) { 73 | return [].concat(_toConsumableArray(a), _toConsumableArray(b)); 74 | } else { 75 | return [].concat(_toConsumableArray(a), [b]); 76 | } 77 | }, []), function (x) { 78 | return (0, _Document.selectorIsId)(x); 79 | }); 80 | 81 | allIds = allIds.concat(joinIds); 82 | return { 83 | doc: d, 84 | lookupResult: val 85 | }; 86 | }); 87 | 88 | return { allIds: allIds, docsWrapped: docsWrapped }; 89 | } 90 | 91 | /** 92 | * By given value of some key in join object return 93 | * an options object. 94 | * @param {Object|Collection} joinValue 95 | * @return {Object} 96 | */ 97 | function _getJoinOptions(key, value) { 98 | if (value instanceof _Collection2.default) { 99 | return { model: value, joinPath: key }; 100 | } else if (_checkTypes2.default.object(value)) { 101 | return { 102 | model: value.model, 103 | joinPath: value.joinPath || key 104 | }; 105 | } else { 106 | throw new Error('Invalid join object value'); 107 | } 108 | } 109 | 110 | /** 111 | * By given result of joining objects restriving and root documents 112 | * decomposition set joining object on each root document 113 | * (if it is exists). 114 | * @param {String} joinPath 115 | * @param {Array} res 116 | * @param {Object} docsById 117 | * @param {Object} childToRootMap 118 | */ 119 | function _joinDocsWithResult(joinPath, res, docsWrapped) { 120 | var resIdMap = {}; 121 | var initKeyparts = joinPath.split('.'); 122 | 123 | (0, _forEach2.default)(res, function (v) { 124 | return resIdMap[v._id] = v; 125 | }); 126 | (0, _forEach2.default)(docsWrapped, function (wrap) { 127 | (0, _forEach2.default)(wrap.lookupResult, function (branch) { 128 | if (branch.value) { 129 | // `findModTarget` will modify `keyparts`. So, it should 130 | // be copied each time. 131 | var keyparts = initKeyparts.slice(); 132 | var target = (0, _DocumentModifier.findModTarget)(wrap.doc, keyparts, { 133 | noCreate: false, 134 | forbidArray: false, 135 | arrayIndices: branch.arrayIndices 136 | }); 137 | var field = keyparts[keyparts.length - 1]; 138 | 139 | if (_checkTypes2.default.array(branch.value)) { 140 | target[field] = (0, _map3.default)(branch.value, function (id) { 141 | return resIdMap[id]; 142 | }); 143 | } else { 144 | target[field] = resIdMap[branch.value] || null; 145 | } 146 | } 147 | }); 148 | }); 149 | } 150 | 151 | var joinObj = exports.joinObj = { 152 | method: function method(obj) { 153 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 154 | 155 | (0, _invariant2.default)(_checkTypes2.default.object(obj), 'joinObj(...): argument must be an object'); 156 | 157 | this._addPipeline('joinObj', obj, options); 158 | return this; 159 | }, 160 | 161 | process: function process(docs, pipeObj, cursor) { 162 | if (!docs) { 163 | return Promise.resolve(docs); 164 | } else { 165 | var _ret = function () { 166 | var obj = pipeObj.value; 167 | var options = pipeObj.args[0] || {}; 168 | var isObj = !_checkTypes2.default.array(docs); 169 | docs = !isObj ? docs : [docs]; 170 | 171 | var joinerFn = function joinerFn(dcs) { 172 | return (0, _map3.default)((0, _keys3.default)(obj), function (k) { 173 | var _getJoinOptions2 = _getJoinOptions(k, obj[k]); 174 | 175 | var model = _getJoinOptions2.model; 176 | var joinPath = _getJoinOptions2.joinPath; 177 | 178 | var _decomposeDocuments2 = _decomposeDocuments(docs, k); 179 | 180 | var allIds = _decomposeDocuments2.allIds; 181 | var docsWrapped = _decomposeDocuments2.docsWrapped; 182 | 183 | var execFnName = options.observe ? 'observe' : 'then'; 184 | return model.find({ _id: { $in: allIds } })[execFnName](function (res) { 185 | _joinDocsWithResult(joinPath, res, docsWrapped); 186 | }); 187 | }); 188 | }; 189 | 190 | var newPipeObj = _extends({}, pipeObj, { value: joinerFn }); 191 | return { 192 | v: _joinAll.joinAll.process(docs, newPipeObj, cursor).then(function (res) { 193 | return isObj ? res[0] : res; 194 | }) 195 | }; 196 | }(); 197 | 198 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 199 | } 200 | } 201 | }; -------------------------------------------------------------------------------- /lib/Random.js: -------------------------------------------------------------------------------- 1 | import _try from 'fast.js/function/try'; 2 | import invariant from 'invariant'; 3 | 4 | 5 | // Intarnals 6 | let _defaultRandomGenerator; 7 | const RANDOM_GENERATOR_TYPE = { 8 | NODE_CRYPTO: 'NODE_CRYPTO', 9 | BROWSER_CRYPTO: 'BROWSER_CRYPTO', 10 | ALEA: 'ALEA', 11 | }; 12 | const UNMISTAKABLE_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'; 13 | const BASE64_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + 14 | '0123456789-_'; 15 | 16 | // see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript 17 | // for a full discussion and Alea implementation. 18 | const Alea = function() { 19 | function Mash() { 20 | var n = 0xefc8249d; 21 | 22 | var mash = function(data) { 23 | data = data.toString(); 24 | for (var i = 0; i < data.length; i++) { 25 | n += data.charCodeAt(i); 26 | var h = 0.02519603282416938 * n; 27 | n = h >>> 0; 28 | h -= n; 29 | h *= n; 30 | n = h >>> 0; 31 | h -= n; 32 | n += h * 0x100000000; // 2^32 33 | } 34 | return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 35 | }; 36 | 37 | mash.version = 'Mash 0.9'; 38 | return mash; 39 | } 40 | 41 | return (function(args) { 42 | var s0 = 0; 43 | var s1 = 0; 44 | var s2 = 0; 45 | var c = 1; 46 | 47 | if (args.length == 0) { 48 | args = [+new Date()]; 49 | } 50 | var mash = Mash(); 51 | s0 = mash(' '); 52 | s1 = mash(' '); 53 | s2 = mash(' '); 54 | 55 | for (var i = 0; i < args.length; i++) { 56 | s0 -= mash(args[i]); 57 | if (s0 < 0) { 58 | s0 += 1; 59 | } 60 | s1 -= mash(args[i]); 61 | if (s1 < 0) { 62 | s1 += 1; 63 | } 64 | s2 -= mash(args[i]); 65 | if (s2 < 0) { 66 | s2 += 1; 67 | } 68 | } 69 | mash = null; 70 | 71 | var random = function() { 72 | var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 73 | s0 = s1; 74 | s1 = s2; 75 | return s2 = t - (c = t | 0); 76 | }; 77 | random.uint32 = function() { 78 | return random() * 0x100000000; // 2^32 79 | }; 80 | random.fract53 = function() { 81 | return random() + 82 | (random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53 83 | }; 84 | random.version = 'Alea 0.9'; 85 | random.args = args; 86 | return random; 87 | 88 | }(Array.prototype.slice.call(arguments))); 89 | }; 90 | 91 | /** 92 | * Create seeds array for a browser based on window sizes, 93 | * Date and some random number. 94 | * @return {Arrat} 95 | */ 96 | export function _getBrowserSeeds() { 97 | var height = (typeof window !== 'undefined' && window.innerHeight) || 98 | (typeof document !== 'undefined' 99 | && document.documentElement 100 | && document.documentElement.clientHeight) || 101 | (typeof document !== 'undefined' 102 | && document.body 103 | && document.body.clientHeight) || 104 | 1; 105 | 106 | var width = (typeof window !== 'undefined' && window.innerWidth) || 107 | (typeof document !== 'undefined' 108 | && document.documentElement 109 | && document.documentElement.clientWidth) || 110 | (typeof document !== 'undefined' 111 | && document.body 112 | && document.body.clientWidth) || 113 | 1; 114 | 115 | var agent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''; 116 | return [new Date(), height, width, agent, Math.random()]; 117 | } 118 | 119 | /** 120 | * Random string generator copied from Meteor 121 | * with minor modifications and refactoring. 122 | */ 123 | export default class Random { 124 | constructor(type, options = {}) { 125 | this.type = type; 126 | 127 | invariant( 128 | RANDOM_GENERATOR_TYPE[type], 129 | 'Random(...): no generator type %s', 130 | type 131 | ); 132 | 133 | if (type === RANDOM_GENERATOR_TYPE.ALEA) { 134 | invariant( 135 | options.seeds, 136 | 'Random(...): seed is not provided for ALEA seeded generator' 137 | ); 138 | this.alea = Alea.apply(null, options.seeds); 139 | } 140 | } 141 | 142 | fraction() { 143 | if (this.type === RANDOM_GENERATOR_TYPE.ALEA) { 144 | return this.alea(); 145 | } else if (this.type === RANDOM_GENERATOR_TYPE.NODE_CRYPTO) { 146 | const numerator = parseInt(this.hexString(8), 16); 147 | return numerator * 2.3283064365386963e-10; // 2^-32 148 | } else if (this.type === RANDOM_GENERATOR_TYPE.BROWSER_CRYPTO) { 149 | const array = new Uint32Array(1); 150 | window.crypto.getRandomValues(array); 151 | return array[0] * 2.3283064365386963e-10; // 2^-32 152 | } else { 153 | throw new Error('Unknown random generator type: ' + this.type); 154 | } 155 | } 156 | 157 | hexString(digits) { 158 | if (this.type === RANDOM_GENERATOR_TYPE.NODE_CRYPTO) { 159 | const nodeCrypto = require('crypto'); 160 | const numBytes = Math.ceil(digits / 2); 161 | 162 | // Try to get cryptographically strong randomness. Fall back to 163 | // non-cryptographically strong if not available. 164 | let bytes = _try(() => nodeCrypto.randomBytes(numBytes)); 165 | if (bytes instanceof Error) { 166 | bytes = nodeCrypto.pseudoRandomBytes(numBytes); 167 | } 168 | 169 | const result = bytes.toString('hex'); 170 | // If the number of digits is odd, we'll have generated an extra 4 bits 171 | // of randomness, so we need to trim the last digit. 172 | return result.substring(0, digits); 173 | } else { 174 | return this._randomString(digits, '0123456789abcdef'); 175 | } 176 | } 177 | 178 | _randomString(charsCount, alphabet) { 179 | const digits = []; 180 | for (let i = 0; i < charsCount; i++) { 181 | digits[i] = this.choice(alphabet); 182 | } 183 | return digits.join(''); 184 | } 185 | 186 | id(charsCount) { 187 | // 17 characters is around 96 bits of entropy, which is the amount of 188 | // state in the Alea PRNG. 189 | if (charsCount === undefined) { 190 | charsCount = 17; 191 | } 192 | return this._randomString(charsCount, UNMISTAKABLE_CHARS); 193 | } 194 | 195 | secret(charsCount) { 196 | // Default to 256 bits of entropy, or 43 characters at 6 bits per 197 | // character. 198 | if (charsCount === undefined) { 199 | charsCount = 43; 200 | } 201 | return this._randomString(charsCount, BASE64_CHARS); 202 | } 203 | 204 | choice(arrayOrString) { 205 | const index = Math.floor(this.fraction() * arrayOrString.length); 206 | if (typeof arrayOrString === 'string') { 207 | return arrayOrString.substr(index, 1); 208 | } else { 209 | return arrayOrString[index]; 210 | } 211 | } 212 | 213 | static default() { 214 | if (!_defaultRandomGenerator) { 215 | if (typeof window !== 'undefined') { 216 | if (window.crypto && window.crypto.getRandomValues) { 217 | return new Random(RANDOM_GENERATOR_TYPE.BROWSER_CRYPTO); 218 | } else { 219 | return new Random( 220 | RANDOM_GENERATOR_TYPE.ALEA, 221 | { seeds: _getBrowserSeeds() } 222 | ); 223 | } 224 | } else { 225 | return new Random(RANDOM_GENERATOR_TYPE.NODE_CRYPTO); 226 | } 227 | } 228 | return _defaultRandomGenerator; 229 | } 230 | 231 | static createWithSeeds() { 232 | invariant( 233 | arguments.length, 234 | 'Random.createWithSeeds(...): no seeds were provided' 235 | ); 236 | 237 | return new Random( 238 | RANDOM_GENERATOR_TYPE.ALEA, 239 | { seeds: arguments } 240 | ); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /lib/Document.js: -------------------------------------------------------------------------------- 1 | import _check from 'check-types'; 2 | import _each from 'fast.js/forEach'; 3 | import _keys from 'fast.js/object/keys'; 4 | import EJSON from './EJSON'; 5 | 6 | 7 | /** 8 | * Return true if given selector is an 9 | * object id type (string or number) 10 | * @param {Mixed} selector 11 | * @return {Boolean} 12 | */ 13 | export function selectorIsId(selector) { 14 | return ( 15 | _check.string(selector) || 16 | _check.number(selector) 17 | ); 18 | } 19 | 20 | export function selectorIsIdPerhapsAsObject(selector) { 21 | return ( 22 | selectorIsId(selector) || 23 | (selector && _check.object(selector) && 24 | selector._id && selectorIsId(selector._id) && 25 | _keys(selector).length === 1) 26 | ); 27 | } 28 | 29 | export function isArray(x) { 30 | return _check.array(x) && !EJSON.isBinary(x); 31 | } 32 | 33 | export function isPlainObject(x) { 34 | return x && MongoTypeComp._type(x) === 3; 35 | } 36 | 37 | export function isIndexable(x) { 38 | return isArray(x) || isPlainObject(x); 39 | } 40 | 41 | // Returns true if this is an object with at least one key and all keys begin 42 | // with $. Unless inconsistentOK is set, throws if some keys begin with $ and 43 | // others don't. 44 | export function isOperatorObject(valueSelector, inconsistentOK) { 45 | if (!isPlainObject(valueSelector)) { 46 | return false; 47 | } 48 | 49 | var theseAreOperators = undefined; 50 | _each(valueSelector, function(value, selKey) { 51 | var thisIsOperator = selKey.substr(0, 1) === '$'; 52 | if (theseAreOperators === undefined) { 53 | theseAreOperators = thisIsOperator; 54 | } else if (theseAreOperators !== thisIsOperator) { 55 | if (!inconsistentOK) { 56 | throw new Error('Inconsistent operator: ' + 57 | JSON.stringify(valueSelector)); 58 | } 59 | theseAreOperators = false; 60 | } 61 | }); 62 | return !!theseAreOperators; // {} has no operators 63 | } 64 | 65 | 66 | // string can be converted to integer 67 | export function isNumericKey(s) { 68 | return /^[0-9]+$/.test(s); 69 | } 70 | 71 | // helpers used by compiled selector code 72 | export const MongoTypeComp = { 73 | // XXX for _all and _in, consider building 'inquery' at compile time.. 74 | 75 | _type: function(v) { 76 | if (typeof v === 'number') { 77 | return 1; 78 | } else if (typeof v === 'string') { 79 | return 2; 80 | } else if (typeof v === 'boolean') { 81 | return 8; 82 | } else if (isArray(v)) { 83 | return 4; 84 | } else if (v === null) { 85 | return 10; 86 | } else if (v instanceof RegExp) { 87 | // note that typeof(/x/) === 'object' 88 | return 11; 89 | } else if (typeof v === 'function') { 90 | return 13; 91 | } else if (v instanceof Date) { 92 | return 9; 93 | } else if (EJSON.isBinary(v)) { 94 | return 5; 95 | } 96 | return 3; // object 97 | 98 | // XXX support some/all of these: 99 | // 14, symbol 100 | // 15, javascript code with scope 101 | // 16, 18: 32-bit/64-bit integer 102 | // 17, timestamp 103 | // 255, minkey 104 | // 127, maxkey 105 | }, 106 | 107 | // deep equality test: use for literal document and array matches 108 | _equal: function(a, b) { 109 | return EJSON.equals(a, b, {keyOrderSensitive: true}); 110 | }, 111 | 112 | // maps a type code to a value that can be used to sort values of 113 | // different types 114 | _typeorder: function(t) { 115 | // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types 116 | // XXX what is the correct sort position for Javascript code? 117 | // ('100' in the matrix below) 118 | // XXX minkey/maxkey 119 | return [-1, // (not a type) 120 | 1, // number 121 | 2, // string 122 | 3, // object 123 | 4, // array 124 | 5, // binary 125 | -1, // deprecated 126 | 6, // ObjectID 127 | 7, // bool 128 | 8, // Date 129 | 0, // null 130 | 9, // RegExp 131 | -1, // deprecated 132 | 100, // JS code 133 | 2, // deprecated (symbol) 134 | 100, // JS code 135 | 1, // 32-bit int 136 | 8, // Mongo timestamp 137 | 1, // 64-bit int 138 | ][t]; 139 | }, 140 | 141 | // compare two values of unknown type according to BSON ordering 142 | // semantics. (as an extension, consider 'undefined' to be less than 143 | // any other value.) return negative if a is less, positive if b is 144 | // less, or 0 if equal 145 | _cmp: function(a, b) { 146 | if (a === undefined) { 147 | return b === undefined ? 0 : -1; 148 | } 149 | if (b === undefined) { 150 | return 1; 151 | } 152 | var ta = MongoTypeComp._type(a); 153 | var tb = MongoTypeComp._type(b); 154 | var oa = MongoTypeComp._typeorder(ta); 155 | var ob = MongoTypeComp._typeorder(tb); 156 | if (oa !== ob) { 157 | return oa < ob ? -1 : 1; 158 | } 159 | if (ta !== tb) { 160 | // XXX need to implement this if we implement Symbol or integers, or 161 | // Timestamp 162 | throw Error('Missing type coercion logic in _cmp'); 163 | } 164 | if (ta === 7) { // ObjectID 165 | // Convert to string. 166 | ta = tb = 2; 167 | a = a.toHexString(); 168 | b = b.toHexString(); 169 | } 170 | if (ta === 9) { // Date 171 | // Convert to millis. 172 | ta = tb = 1; 173 | a = a.getTime(); 174 | b = b.getTime(); 175 | } 176 | 177 | if (ta === 1) { // double 178 | return a - b; 179 | } 180 | if (tb === 2) { // string 181 | return a < b ? -1 : (a === b ? 0 : 1); 182 | } 183 | if (ta === 3) { // Object 184 | // this could be much more efficient in the expected case ... 185 | var to_array = function(obj) { 186 | var ret = []; 187 | for (var key in obj) { 188 | ret.push(key); 189 | ret.push(obj[key]); 190 | } 191 | return ret; 192 | }; 193 | return MongoTypeComp._cmp(to_array(a), to_array(b)); 194 | } 195 | if (ta === 4) { // Array 196 | for (var i = 0; ; i++) { 197 | if (i === a.length) { 198 | return (i === b.length) ? 0 : -1; 199 | } 200 | if (i === b.length) { 201 | return 1; 202 | } 203 | var s = MongoTypeComp._cmp(a[i], b[i]); 204 | if (s !== 0) { 205 | return s; 206 | } 207 | } 208 | } 209 | if (ta === 5) { // binary 210 | // Surprisingly, a small binary blob is always less than a large one in 211 | // Mongo. 212 | if (a.length !== b.length) { 213 | return a.length - b.length; 214 | } 215 | for (i = 0; i < a.length; i++) { 216 | if (a[i] < b[i]) { 217 | return -1; 218 | } 219 | if (a[i] > b[i]) { 220 | return 1; 221 | } 222 | } 223 | return 0; 224 | } 225 | if (ta === 8) { // boolean 226 | if (a) { 227 | return b ? 0 : 1; 228 | } 229 | return b ? -1 : 0; 230 | } 231 | if (ta === 10) { // null 232 | return 0; 233 | } 234 | if (ta === 11) { // regexp 235 | throw Error('Sorting not supported on regular expression'); // XXX 236 | } 237 | // 13: javascript code 238 | // 14: symbol 239 | // 15: javascript code with scope 240 | // 16: 32-bit integer 241 | // 17: timestamp 242 | // 18: 64-bit integer 243 | // 255: minkey 244 | // 127: maxkey 245 | if (ta === 13) { // javascript code 246 | throw Error('Sorting not supported on Javascript code'); // XXX 247 | } 248 | throw Error('Unknown type to sort'); 249 | }, 250 | }; 251 | -------------------------------------------------------------------------------- /test/both/EJSON.test.js: -------------------------------------------------------------------------------- 1 | import EJSON from '../../lib/EJSON'; 2 | import chai, {except, assert} from 'chai'; 3 | import _ from 'lodash'; 4 | chai.use(require('chai-as-promised')); 5 | chai.should(); 6 | 7 | describe('EJSON', function () { 8 | it('should be key order sensetive', function() { 9 | EJSON.equals({ 10 | a: {b: 1, c: 2}, 11 | d: {e: 3, f: 4} 12 | }, { 13 | d: {f: 4, e: 3}, 14 | a: {c: 2, b: 1} 15 | }).should.be.true; 16 | 17 | EJSON.equals({ 18 | a: {b: 1, c: 2}, 19 | d: {e: 3, f: 4} 20 | }, { 21 | d: {f: 4, e: 3}, 22 | a: {c: 2, b: 1} 23 | }, {keyOrderSensitive: true}).should.be.false; 24 | 25 | EJSON.equals({ 26 | a: {b: 1, c: 2}, 27 | d: {e: 3, f: 4} 28 | }, { 29 | a: {c: 2, b: 1}, 30 | d: {f: 4, e: 3} 31 | }, {keyOrderSensitive: true}).should.be.false; 32 | EJSON.equals({a: {}}, {a: {b:2}}, 33 | {keyOrderSensitive: true}).should.be.false; 34 | EJSON.equals({a: {b:2}}, {a: {}}, 35 | {keyOrderSensitive: true}).should.be.false; 36 | }); 37 | 38 | it('should support nested and literal', function() { 39 | var d = new Date; 40 | var obj = {$date: d}; 41 | var eObj = EJSON.toJSONValue(obj); 42 | var roundTrip = EJSON.fromJSONValue(eObj); 43 | obj.should.be.deep.equals(roundTrip); 44 | }); 45 | 46 | it('should accept more complex equality', function() { 47 | EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, c: 3, b: 2}).should.be.true; 48 | EJSON.equals({a: 1, b: 2}, {a: 1, c: 3, b: 2}).should.be.false; 49 | EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, b: 2}).should.be.false; 50 | EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, c: 3, b: 4}).should.be.false; 51 | EJSON.equals({a: {}}, {a: {b:2}}).should.be.false; 52 | EJSON.equals({a: {b:2}}, {a: {}}).should.be.false; 53 | }); 54 | 55 | it('should accept equality of null and undefined', function() { 56 | EJSON.equals(null, null).should.be.true; 57 | EJSON.equals(undefined, undefined).should.be.true; 58 | EJSON.equals({foo: "foo"}, null).should.be.false; 59 | EJSON.equals(null, {foo: "foo"}).should.be.false; 60 | EJSON.equals(undefined, {foo: "foo"}).should.be.false; 61 | EJSON.equals({foo: "foo"}, undefined).should.be.false; 62 | }); 63 | 64 | it('should accept equality of Nan and Inf', function() { 65 | EJSON.parse("{\"$InfNaN\": 1}").should.be.equals(Infinity); 66 | EJSON.parse("{\"$InfNaN\": -1}").should.be.equals(-Infinity); 67 | _.isNaN(EJSON.parse("{\"$InfNaN\": 0}")).should.be.true; 68 | EJSON.parse(EJSON.stringify(Infinity)).should.be.equals(Infinity); 69 | EJSON.parse(EJSON.stringify(-Infinity)).should.be.equals(-Infinity); 70 | _.isNaN(EJSON.parse(EJSON.stringify(NaN))).should.be.true; 71 | EJSON.equals(NaN, NaN).should.be.true; 72 | EJSON.equals(Infinity, Infinity).should.be.true; 73 | EJSON.equals(-Infinity, -Infinity).should.be.true; 74 | EJSON.equals(Infinity, -Infinity).should.be.false; 75 | EJSON.equals(Infinity, NaN).should.be.false; 76 | EJSON.equals(Infinity, 0).should.be.false; 77 | EJSON.equals(NaN, 0).should.be.false; 78 | 79 | EJSON.equals( 80 | EJSON.parse("{\"a\": {\"$InfNaN\": 1}}"), 81 | {a: Infinity} 82 | ).should.be.true; 83 | EJSON.equals( 84 | EJSON.parse("{\"a\": {\"$InfNaN\": 0}}"), 85 | {a: NaN} 86 | ).should.be.true; 87 | }); 88 | 89 | it('should clone', function() { 90 | var cloneTest = function (x, identical) { 91 | var y = EJSON.clone(x); 92 | EJSON.equals(x, y).should.be.true; 93 | (x === y).should.be.equals(!!identical); 94 | }; 95 | cloneTest(null, true); 96 | cloneTest(undefined, true); 97 | cloneTest(42, true); 98 | cloneTest("asdf", true); 99 | cloneTest([1, 2, 3]); 100 | cloneTest([1, "fasdf", {foo: 42}]); 101 | cloneTest({x: 42, y: "asdf"}); 102 | 103 | var testCloneArgs = function (/*arguments*/) { 104 | EJSON.clone(arguments).should.be.deep.equals([1, 2, "foo", [4]]); 105 | }; 106 | testCloneArgs(1, 2, "foo", [4]); 107 | }); 108 | 109 | it('should stringify object', function() { 110 | assert.equal(EJSON.stringify(null), "null"); 111 | assert.equal(EJSON.stringify(true), "true"); 112 | assert.equal(EJSON.stringify(false), "false"); 113 | assert.equal(EJSON.stringify(123), "123"); 114 | assert.equal(EJSON.stringify("abc"), "\"abc\""); 115 | 116 | assert.equal(EJSON.stringify([1, 2, 3]), 117 | "[1,2,3]" 118 | ); 119 | 120 | assert.deepEqual( 121 | EJSON.parse(EJSON.stringify({b: [2, {d: 4, c: 3}], a: 1})), 122 | {b: [2, {d: 4, c: 3}], a: 1} 123 | ); 124 | }); 125 | 126 | it('should parse', function() { 127 | assert.deepEqual(EJSON.parse("[1,2,3]"), [1,2,3]); 128 | assert.throws( 129 | function () { EJSON.parse(null) }, 130 | /argument should be a string/ 131 | ); 132 | }); 133 | 134 | it('should support custom types', function() { 135 | // Address type 136 | class Address { 137 | constructor(city, state) { 138 | this.city = city; 139 | this.state = state; 140 | } 141 | typeName() { 142 | return "Address"; 143 | } 144 | toJSONValue() { 145 | return { 146 | city: this.city, 147 | state: this.state 148 | }; 149 | } 150 | } 151 | EJSON.addType("Address", function fromJSONValue(value) { 152 | return new Address(value.city, value.state); 153 | }); 154 | 155 | // Person type 156 | class Person { 157 | constructor(name, dob, address) { 158 | this.name = name; 159 | this.dob = dob; 160 | this.address = address; 161 | } 162 | typeName() { 163 | return "Person"; 164 | } 165 | toJSONValue() { 166 | return { 167 | name: this.name, 168 | dob: EJSON.toJSONValue(this.dob), 169 | address: EJSON.toJSONValue(this.address) 170 | }; 171 | } 172 | static fromJSONValue(value) { 173 | return new Person( 174 | value.name, 175 | EJSON.fromJSONValue(value.dob), 176 | EJSON.fromJSONValue(value.address) 177 | ); 178 | } 179 | } 180 | EJSON.addType("Person", Person.fromJSONValue); 181 | 182 | // Holder type 183 | class Holder { 184 | constructor(content) { 185 | this.content = content; 186 | } 187 | typeName() { 188 | return "Holder"; 189 | } 190 | toJSONValue() { 191 | return this.content; 192 | } 193 | static fromJSONValue(value) { 194 | return new Holder(value); 195 | } 196 | } 197 | EJSON.addType("Holder", Holder.fromJSONValue); 198 | 199 | var testSameConstructors = function (obj, compareWith) { 200 | assert.equal(obj.constructor, compareWith.constructor); 201 | if (typeof obj === 'object') { 202 | _.keys(obj).forEach(k => { 203 | testSameConstructors(obj[k], compareWith[k]); 204 | }); 205 | } 206 | } 207 | var testReallyEqual = function (obj, compareWith) { 208 | assert.deepEqual(obj, compareWith); 209 | testSameConstructors(obj, compareWith); 210 | } 211 | var testRoundTrip = function (obj) { 212 | var str = EJSON.stringify(obj); 213 | var roundTrip = EJSON.parse(str); 214 | testReallyEqual(obj, roundTrip); 215 | } 216 | var testCustomObject = function (obj) { 217 | testRoundTrip(obj); 218 | testReallyEqual(obj, EJSON.clone(obj)); 219 | } 220 | 221 | var a = new Address('Montreal', 'Quebec'); 222 | testCustomObject( {address: a} ); 223 | // Test that difference is detected even if they 224 | // have similar toJSONValue results: 225 | var nakedA = {city: 'Montreal', state: 'Quebec'}; 226 | assert.notEqual(nakedA, a); 227 | assert.notEqual(a, nakedA); 228 | var holder = new Holder(nakedA); 229 | assert.deepEqual(holder.toJSONValue(), a.toJSONValue()); // sanity check 230 | assert.notEqual(holder, a); 231 | assert.notEqual(a, holder); 232 | 233 | 234 | var d = new Date; 235 | var obj = new Person("John Doe", d, a); 236 | testCustomObject( obj ); 237 | 238 | // Test clone is deep: 239 | var clone = EJSON.clone(obj); 240 | clone.address.city = 'Sherbrooke'; 241 | assert.notEqual( obj, clone ); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /dist/Document.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.MongoTypeComp = undefined; 7 | exports.selectorIsId = selectorIsId; 8 | exports.selectorIsIdPerhapsAsObject = selectorIsIdPerhapsAsObject; 9 | exports.isArray = isArray; 10 | exports.isPlainObject = isPlainObject; 11 | exports.isIndexable = isIndexable; 12 | exports.isOperatorObject = isOperatorObject; 13 | exports.isNumericKey = isNumericKey; 14 | 15 | var _checkTypes = require('check-types'); 16 | 17 | var _checkTypes2 = _interopRequireDefault(_checkTypes); 18 | 19 | var _forEach = require('fast.js/forEach'); 20 | 21 | var _forEach2 = _interopRequireDefault(_forEach); 22 | 23 | var _keys2 = require('fast.js/object/keys'); 24 | 25 | var _keys3 = _interopRequireDefault(_keys2); 26 | 27 | var _EJSON = require('./EJSON'); 28 | 29 | var _EJSON2 = _interopRequireDefault(_EJSON); 30 | 31 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 32 | 33 | /** 34 | * Return true if given selector is an 35 | * object id type (string or number) 36 | * @param {Mixed} selector 37 | * @return {Boolean} 38 | */ 39 | function selectorIsId(selector) { 40 | return _checkTypes2.default.string(selector) || _checkTypes2.default.number(selector); 41 | } 42 | 43 | function selectorIsIdPerhapsAsObject(selector) { 44 | return selectorIsId(selector) || selector && _checkTypes2.default.object(selector) && selector._id && selectorIsId(selector._id) && (0, _keys3.default)(selector).length === 1; 45 | } 46 | 47 | function isArray(x) { 48 | return _checkTypes2.default.array(x) && !_EJSON2.default.isBinary(x); 49 | } 50 | 51 | function isPlainObject(x) { 52 | return x && MongoTypeComp._type(x) === 3; 53 | } 54 | 55 | function isIndexable(x) { 56 | return isArray(x) || isPlainObject(x); 57 | } 58 | 59 | // Returns true if this is an object with at least one key and all keys begin 60 | // with $. Unless inconsistentOK is set, throws if some keys begin with $ and 61 | // others don't. 62 | function isOperatorObject(valueSelector, inconsistentOK) { 63 | if (!isPlainObject(valueSelector)) { 64 | return false; 65 | } 66 | 67 | var theseAreOperators = undefined; 68 | (0, _forEach2.default)(valueSelector, function (value, selKey) { 69 | var thisIsOperator = selKey.substr(0, 1) === '$'; 70 | if (theseAreOperators === undefined) { 71 | theseAreOperators = thisIsOperator; 72 | } else if (theseAreOperators !== thisIsOperator) { 73 | if (!inconsistentOK) { 74 | throw new Error('Inconsistent operator: ' + JSON.stringify(valueSelector)); 75 | } 76 | theseAreOperators = false; 77 | } 78 | }); 79 | return !!theseAreOperators; // {} has no operators 80 | } 81 | 82 | // string can be converted to integer 83 | function isNumericKey(s) { 84 | return (/^[0-9]+$/.test(s) 85 | ); 86 | } 87 | 88 | // helpers used by compiled selector code 89 | var MongoTypeComp = exports.MongoTypeComp = { 90 | // XXX for _all and _in, consider building 'inquery' at compile time.. 91 | 92 | _type: function _type(v) { 93 | if (typeof v === 'number') { 94 | return 1; 95 | } else if (typeof v === 'string') { 96 | return 2; 97 | } else if (typeof v === 'boolean') { 98 | return 8; 99 | } else if (isArray(v)) { 100 | return 4; 101 | } else if (v === null) { 102 | return 10; 103 | } else if (v instanceof RegExp) { 104 | // note that typeof(/x/) === 'object' 105 | return 11; 106 | } else if (typeof v === 'function') { 107 | return 13; 108 | } else if (v instanceof Date) { 109 | return 9; 110 | } else if (_EJSON2.default.isBinary(v)) { 111 | return 5; 112 | } 113 | return 3; // object 114 | 115 | // XXX support some/all of these: 116 | // 14, symbol 117 | // 15, javascript code with scope 118 | // 16, 18: 32-bit/64-bit integer 119 | // 17, timestamp 120 | // 255, minkey 121 | // 127, maxkey 122 | }, 123 | 124 | // deep equality test: use for literal document and array matches 125 | _equal: function _equal(a, b) { 126 | return _EJSON2.default.equals(a, b, { keyOrderSensitive: true }); 127 | }, 128 | 129 | // maps a type code to a value that can be used to sort values of 130 | // different types 131 | _typeorder: function _typeorder(t) { 132 | // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types 133 | // XXX what is the correct sort position for Javascript code? 134 | // ('100' in the matrix below) 135 | // XXX minkey/maxkey 136 | return [-1, // (not a type) 137 | 1, // number 138 | 2, // string 139 | 3, // object 140 | 4, // array 141 | 5, // binary 142 | -1, // deprecated 143 | 6, // ObjectID 144 | 7, // bool 145 | 8, // Date 146 | 0, // null 147 | 9, // RegExp 148 | -1, // deprecated 149 | 100, // JS code 150 | 2, // deprecated (symbol) 151 | 100, // JS code 152 | 1, // 32-bit int 153 | 8, // Mongo timestamp 154 | 1][// 64-bit int 155 | t]; 156 | }, 157 | 158 | // compare two values of unknown type according to BSON ordering 159 | // semantics. (as an extension, consider 'undefined' to be less than 160 | // any other value.) return negative if a is less, positive if b is 161 | // less, or 0 if equal 162 | _cmp: function _cmp(a, b) { 163 | if (a === undefined) { 164 | return b === undefined ? 0 : -1; 165 | } 166 | if (b === undefined) { 167 | return 1; 168 | } 169 | var ta = MongoTypeComp._type(a); 170 | var tb = MongoTypeComp._type(b); 171 | var oa = MongoTypeComp._typeorder(ta); 172 | var ob = MongoTypeComp._typeorder(tb); 173 | if (oa !== ob) { 174 | return oa < ob ? -1 : 1; 175 | } 176 | if (ta !== tb) { 177 | // XXX need to implement this if we implement Symbol or integers, or 178 | // Timestamp 179 | throw Error('Missing type coercion logic in _cmp'); 180 | } 181 | if (ta === 7) { 182 | // ObjectID 183 | // Convert to string. 184 | ta = tb = 2; 185 | a = a.toHexString(); 186 | b = b.toHexString(); 187 | } 188 | if (ta === 9) { 189 | // Date 190 | // Convert to millis. 191 | ta = tb = 1; 192 | a = a.getTime(); 193 | b = b.getTime(); 194 | } 195 | 196 | if (ta === 1) { 197 | // double 198 | return a - b; 199 | } 200 | if (tb === 2) { 201 | // string 202 | return a < b ? -1 : a === b ? 0 : 1; 203 | } 204 | if (ta === 3) { 205 | // Object 206 | // this could be much more efficient in the expected case ... 207 | var to_array = function to_array(obj) { 208 | var ret = []; 209 | for (var key in obj) { 210 | ret.push(key); 211 | ret.push(obj[key]); 212 | } 213 | return ret; 214 | }; 215 | return MongoTypeComp._cmp(to_array(a), to_array(b)); 216 | } 217 | if (ta === 4) { 218 | // Array 219 | for (var i = 0;; i++) { 220 | if (i === a.length) { 221 | return i === b.length ? 0 : -1; 222 | } 223 | if (i === b.length) { 224 | return 1; 225 | } 226 | var s = MongoTypeComp._cmp(a[i], b[i]); 227 | if (s !== 0) { 228 | return s; 229 | } 230 | } 231 | } 232 | if (ta === 5) { 233 | // binary 234 | // Surprisingly, a small binary blob is always less than a large one in 235 | // Mongo. 236 | if (a.length !== b.length) { 237 | return a.length - b.length; 238 | } 239 | for (i = 0; i < a.length; i++) { 240 | if (a[i] < b[i]) { 241 | return -1; 242 | } 243 | if (a[i] > b[i]) { 244 | return 1; 245 | } 246 | } 247 | return 0; 248 | } 249 | if (ta === 8) { 250 | // boolean 251 | if (a) { 252 | return b ? 0 : 1; 253 | } 254 | return b ? -1 : 0; 255 | } 256 | if (ta === 10) { 257 | // null 258 | return 0; 259 | } 260 | if (ta === 11) { 261 | // regexp 262 | throw Error('Sorting not supported on regular expression'); // XXX 263 | } 264 | // 13: javascript code 265 | // 14: symbol 266 | // 15: javascript code with scope 267 | // 16: 32-bit integer 268 | // 17: timestamp 269 | // 18: 64-bit integer 270 | // 255: minkey 271 | // 127: maxkey 272 | if (ta === 13) { 273 | // javascript code 274 | throw Error('Sorting not supported on Javascript code'); // XXX 275 | } 276 | throw Error('Unknown type to sort'); 277 | } 278 | }; -------------------------------------------------------------------------------- /dist/IndexManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 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; }; }(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.IndexManager = undefined; 9 | 10 | var _bind2 = require('fast.js/function/bind'); 11 | 12 | var _bind3 = _interopRequireDefault(_bind2); 13 | 14 | var _keys2 = require('fast.js/object/keys'); 15 | 16 | var _keys3 = _interopRequireDefault(_keys2); 17 | 18 | var _forEach = require('fast.js/forEach'); 19 | 20 | var _forEach2 = _interopRequireDefault(_forEach); 21 | 22 | var _map2 = require('fast.js/map'); 23 | 24 | var _map3 = _interopRequireDefault(_map2); 25 | 26 | var _invariant = require('invariant'); 27 | 28 | var _invariant2 = _interopRequireDefault(_invariant); 29 | 30 | var _PromiseQueue = require('./PromiseQueue'); 31 | 32 | var _PromiseQueue2 = _interopRequireDefault(_PromiseQueue); 33 | 34 | var _CollectionIndex = require('./CollectionIndex'); 35 | 36 | var _CollectionIndex2 = _interopRequireDefault(_CollectionIndex); 37 | 38 | var _DocumentRetriver = require('./DocumentRetriver'); 39 | 40 | var _DocumentRetriver2 = _interopRequireDefault(_DocumentRetriver); 41 | 42 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 43 | 44 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 45 | 46 | /** 47 | * Manager for controlling a list of indexes 48 | * for some model. Building indexes is promise 49 | * based. 50 | * By default it creates an index for `_id` field. 51 | */ 52 | /* istanbul ignore next */ 53 | 54 | var IndexManager = exports.IndexManager = function () { 55 | function IndexManager(db) { 56 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 57 | 58 | _classCallCheck(this, IndexManager); 59 | 60 | this.db = db; 61 | this.indexes = {}; 62 | this._queue = new _PromiseQueue2.default(options.concurrency || 2); 63 | 64 | // By default ensure index by _id field 65 | this.ensureIndex({ 66 | fieldName: '_id', 67 | unique: true 68 | }); 69 | } 70 | 71 | /** 72 | * Check index existance for given `options.fieldName` and 73 | * if index not exists it creates new one. 74 | * Always return a promise that resolved only when 75 | * index succesfully created, built and ready for working with. 76 | * If `options.forceRebuild` provided and equals to true then 77 | * existing index will be rebuilt, otherwise existing index 78 | * don't touched. 79 | * 80 | * @param {Object} options.fieldName name of the field for indexing 81 | * @param {Object} options.forceRebuild rebuild index if it exists 82 | * @return {Promise} 83 | */ 84 | 85 | _createClass(IndexManager, [{ 86 | key: 'ensureIndex', 87 | value: function ensureIndex(options) { 88 | (0, _invariant2.default)(options && options.fieldName, 'You must specify a fieldName in options object'); 89 | 90 | var key = options.fieldName; 91 | if (!this.indexes[key]) { 92 | this.indexes[key] = new _CollectionIndex2.default(options); 93 | return this.buildIndex(key); 94 | } else if (this.indexes[key].buildPromise) { 95 | return this.indexes[key].buildPromise; 96 | } else if (options && options.forceRebuild) { 97 | return this.buildIndex(key); 98 | } else { 99 | return Promise.resolve(); 100 | } 101 | } 102 | 103 | /** 104 | * Buld an existing index (ensured) and return a 105 | * promise that will be resolved only when index successfully 106 | * built for all documents in the storage. 107 | * @param {String} key 108 | * @return {Promise} 109 | */ 110 | 111 | }, { 112 | key: 'buildIndex', 113 | value: function buildIndex(key) { 114 | var _this = this; 115 | 116 | (0, _invariant2.default)(this.indexes[key], 'Index with key `%s` does not ensured yet', key); 117 | 118 | var cleanup = function cleanup() { 119 | return _this.indexes[key].buildPromise = null; 120 | }; 121 | var buildPromise = this._queue.add((0, _bind3.default)(this._doBuildIndex, this, key)).then(cleanup, cleanup); 122 | 123 | this.indexes[key].buildPromise = buildPromise; 124 | return buildPromise; 125 | } 126 | 127 | /** 128 | * Schedule a task for each index in the 129 | * manager. Return promise that will be resolved 130 | * when all indexes is successfully built. 131 | * @return {Promise} 132 | */ 133 | 134 | }, { 135 | key: 'buildAllIndexes', 136 | value: function buildAllIndexes() { 137 | var _this2 = this; 138 | 139 | return Promise.all((0, _map3.default)(this.indexes, function (v, k) { 140 | return _this2.ensureIndex({ 141 | fieldName: k, 142 | forceRebuild: true 143 | }); 144 | })); 145 | } 146 | 147 | /** 148 | * Remove an index 149 | * @param {String} key 150 | * @return {Promise} 151 | */ 152 | 153 | }, { 154 | key: 'removeIndex', 155 | value: function removeIndex(key) { 156 | var _this3 = this; 157 | 158 | return this._queue.add(function () { 159 | delete _this3.indexes[key]; 160 | }); 161 | } 162 | 163 | /** 164 | * Add a document to all indexes 165 | * @param {Object} doc 166 | * @return {Promise} 167 | */ 168 | 169 | }, { 170 | key: 'indexDocument', 171 | value: function indexDocument(doc) { 172 | var _this4 = this; 173 | 174 | return this._queue.add(function () { 175 | var keys = (0, _keys3.default)(_this4.indexes); 176 | var failingIndex = null; 177 | try { 178 | (0, _forEach2.default)(keys, function (k, i) { 179 | failingIndex = i; 180 | _this4.indexes[k].insert(doc); 181 | }); 182 | } catch (e) { 183 | (0, _forEach2.default)(keys.slice(0, failingIndex), function (k) { 184 | _this4.indexes[k].remove(doc); 185 | }); 186 | throw e; 187 | } 188 | }); 189 | } 190 | 191 | /** 192 | * Update all indexes with new version of 193 | * the document 194 | * @param {Object} oldDoc 195 | * @param {Object} newDoc 196 | * @return {Promise} 197 | */ 198 | 199 | }, { 200 | key: 'reindexDocument', 201 | value: function reindexDocument(oldDoc, newDoc) { 202 | var _this5 = this; 203 | 204 | return this._queue.add(function () { 205 | var keys = (0, _keys3.default)(_this5.indexes); 206 | var failingIndex = null; 207 | try { 208 | (0, _forEach2.default)(keys, function (k, i) { 209 | failingIndex = i; 210 | _this5.indexes[k].update(oldDoc, newDoc); 211 | }); 212 | } catch (e) { 213 | (0, _forEach2.default)(keys.slice(0, failingIndex), function (k) { 214 | _this5.indexes[k].revertUpdate(oldDoc, newDoc); 215 | }); 216 | throw e; 217 | } 218 | }); 219 | } 220 | 221 | /** 222 | * Remove document from all indexes 223 | * @param {Object} doc 224 | * @return {Promise} 225 | */ 226 | 227 | }, { 228 | key: 'deindexDocument', 229 | value: function deindexDocument(doc) { 230 | var _this6 = this; 231 | 232 | return this._queue.add(function () { 233 | var keys = (0, _keys3.default)(_this6.indexes); 234 | (0, _forEach2.default)(keys, function (k) { 235 | _this6.indexes[k].remove(doc); 236 | }); 237 | }); 238 | } 239 | 240 | /** 241 | * Build an existing index with reseting first 242 | * @param {String} key 243 | * @return {Promise} 244 | */ 245 | 246 | }, { 247 | key: '_doBuildIndex', 248 | value: function _doBuildIndex(key) { 249 | // Get and reset index 250 | var index = this.indexes[key]; 251 | index.reset(); 252 | 253 | // Loop through all doucments in the storage 254 | var errors = []; 255 | return new _DocumentRetriver2.default(this.db).retriveAll().then(function (docs) { 256 | (0, _forEach2.default)(docs, function (doc) { 257 | try { 258 | index.insert(doc); 259 | } catch (e) { 260 | errors.push([e, doc]); 261 | } 262 | }); 263 | 264 | if (errors.length > 0) { 265 | throw new Error('Index build failed with errors: ', errors); 266 | } 267 | }); 268 | } 269 | }]); 270 | 271 | return IndexManager; 272 | }(); 273 | 274 | exports.default = IndexManager; -------------------------------------------------------------------------------- /dist/Random.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 4 | 5 | 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; }; }(); 6 | 7 | Object.defineProperty(exports, "__esModule", { 8 | value: true 9 | }); 10 | exports._getBrowserSeeds = _getBrowserSeeds; 11 | 12 | var _try2 = require('fast.js/function/try'); 13 | 14 | var _try3 = _interopRequireDefault(_try2); 15 | 16 | var _invariant = require('invariant'); 17 | 18 | var _invariant2 = _interopRequireDefault(_invariant); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 23 | 24 | // Intarnals 25 | var _defaultRandomGenerator = undefined; 26 | var RANDOM_GENERATOR_TYPE = { 27 | NODE_CRYPTO: 'NODE_CRYPTO', 28 | BROWSER_CRYPTO: 'BROWSER_CRYPTO', 29 | ALEA: 'ALEA' 30 | }; 31 | var UNMISTAKABLE_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'; 32 | var BASE64_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789-_'; 33 | 34 | // see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript 35 | // for a full discussion and Alea implementation. 36 | var Alea = function Alea() { 37 | function Mash() { 38 | var n = 0xefc8249d; 39 | 40 | var mash = function mash(data) { 41 | data = data.toString(); 42 | for (var i = 0; i < data.length; i++) { 43 | n += data.charCodeAt(i); 44 | var h = 0.02519603282416938 * n; 45 | n = h >>> 0; 46 | h -= n; 47 | h *= n; 48 | n = h >>> 0; 49 | h -= n; 50 | n += h * 0x100000000; // 2^32 51 | } 52 | return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 53 | }; 54 | 55 | mash.version = 'Mash 0.9'; 56 | return mash; 57 | } 58 | 59 | return function (args) { 60 | var s0 = 0; 61 | var s1 = 0; 62 | var s2 = 0; 63 | var c = 1; 64 | 65 | if (args.length == 0) { 66 | args = [+new Date()]; 67 | } 68 | var mash = Mash(); 69 | s0 = mash(' '); 70 | s1 = mash(' '); 71 | s2 = mash(' '); 72 | 73 | for (var i = 0; i < args.length; i++) { 74 | s0 -= mash(args[i]); 75 | if (s0 < 0) { 76 | s0 += 1; 77 | } 78 | s1 -= mash(args[i]); 79 | if (s1 < 0) { 80 | s1 += 1; 81 | } 82 | s2 -= mash(args[i]); 83 | if (s2 < 0) { 84 | s2 += 1; 85 | } 86 | } 87 | mash = null; 88 | 89 | var random = function random() { 90 | var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 91 | s0 = s1; 92 | s1 = s2; 93 | return s2 = t - (c = t | 0); 94 | }; 95 | random.uint32 = function () { 96 | return random() * 0x100000000; // 2^32 97 | }; 98 | random.fract53 = function () { 99 | return random() + (random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53 100 | }; 101 | random.version = 'Alea 0.9'; 102 | random.args = args; 103 | return random; 104 | }(Array.prototype.slice.call(arguments)); 105 | }; 106 | 107 | /** 108 | * Create seeds array for a browser based on window sizes, 109 | * Date and some random number. 110 | * @return {Arrat} 111 | */ 112 | function _getBrowserSeeds() { 113 | var height = typeof window !== 'undefined' && window.innerHeight || typeof document !== 'undefined' && document.documentElement && document.documentElement.clientHeight || typeof document !== 'undefined' && document.body && document.body.clientHeight || 1; 114 | 115 | var width = typeof window !== 'undefined' && window.innerWidth || typeof document !== 'undefined' && document.documentElement && document.documentElement.clientWidth || typeof document !== 'undefined' && document.body && document.body.clientWidth || 1; 116 | 117 | var agent = typeof navigator !== 'undefined' && navigator.userAgent || ''; 118 | return [new Date(), height, width, agent, Math.random()]; 119 | } 120 | 121 | /** 122 | * Random string generator copied from Meteor 123 | * with minor modifications and refactoring. 124 | */ 125 | 126 | var Random = function () { 127 | function Random(type) { 128 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 129 | 130 | _classCallCheck(this, Random); 131 | 132 | this.type = type; 133 | 134 | (0, _invariant2.default)(RANDOM_GENERATOR_TYPE[type], 'Random(...): no generator type %s', type); 135 | 136 | if (type === RANDOM_GENERATOR_TYPE.ALEA) { 137 | (0, _invariant2.default)(options.seeds, 'Random(...): seed is not provided for ALEA seeded generator'); 138 | this.alea = Alea.apply(null, options.seeds); 139 | } 140 | } 141 | 142 | _createClass(Random, [{ 143 | key: 'fraction', 144 | value: function fraction() { 145 | if (this.type === RANDOM_GENERATOR_TYPE.ALEA) { 146 | return this.alea(); 147 | } else if (this.type === RANDOM_GENERATOR_TYPE.NODE_CRYPTO) { 148 | var numerator = parseInt(this.hexString(8), 16); 149 | return numerator * 2.3283064365386963e-10; // 2^-32 150 | } else if (this.type === RANDOM_GENERATOR_TYPE.BROWSER_CRYPTO) { 151 | var array = new Uint32Array(1); 152 | window.crypto.getRandomValues(array); 153 | return array[0] * 2.3283064365386963e-10; // 2^-32 154 | } else { 155 | throw new Error('Unknown random generator type: ' + this.type); 156 | } 157 | } 158 | }, { 159 | key: 'hexString', 160 | value: function hexString(digits) { 161 | if (this.type === RANDOM_GENERATOR_TYPE.NODE_CRYPTO) { 162 | var _ret = function () { 163 | var nodeCrypto = require('crypto'); 164 | var numBytes = Math.ceil(digits / 2); 165 | 166 | // Try to get cryptographically strong randomness. Fall back to 167 | // non-cryptographically strong if not available. 168 | var bytes = (0, _try3.default)(function () { 169 | return nodeCrypto.randomBytes(numBytes); 170 | }); 171 | if (bytes instanceof Error) { 172 | bytes = nodeCrypto.pseudoRandomBytes(numBytes); 173 | } 174 | 175 | var result = bytes.toString('hex'); 176 | // If the number of digits is odd, we'll have generated an extra 4 bits 177 | // of randomness, so we need to trim the last digit. 178 | return { 179 | v: result.substring(0, digits) 180 | }; 181 | }(); 182 | 183 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 184 | } else { 185 | return this._randomString(digits, '0123456789abcdef'); 186 | } 187 | } 188 | }, { 189 | key: '_randomString', 190 | value: function _randomString(charsCount, alphabet) { 191 | var digits = []; 192 | for (var i = 0; i < charsCount; i++) { 193 | digits[i] = this.choice(alphabet); 194 | } 195 | return digits.join(''); 196 | } 197 | }, { 198 | key: 'id', 199 | value: function id(charsCount) { 200 | // 17 characters is around 96 bits of entropy, which is the amount of 201 | // state in the Alea PRNG. 202 | if (charsCount === undefined) { 203 | charsCount = 17; 204 | } 205 | return this._randomString(charsCount, UNMISTAKABLE_CHARS); 206 | } 207 | }, { 208 | key: 'secret', 209 | value: function secret(charsCount) { 210 | // Default to 256 bits of entropy, or 43 characters at 6 bits per 211 | // character. 212 | if (charsCount === undefined) { 213 | charsCount = 43; 214 | } 215 | return this._randomString(charsCount, BASE64_CHARS); 216 | } 217 | }, { 218 | key: 'choice', 219 | value: function choice(arrayOrString) { 220 | var index = Math.floor(this.fraction() * arrayOrString.length); 221 | if (typeof arrayOrString === 'string') { 222 | return arrayOrString.substr(index, 1); 223 | } else { 224 | return arrayOrString[index]; 225 | } 226 | } 227 | }], [{ 228 | key: 'default', 229 | value: function _default() { 230 | if (!_defaultRandomGenerator) { 231 | if (typeof window !== 'undefined') { 232 | if (window.crypto && window.crypto.getRandomValues) { 233 | return new Random(RANDOM_GENERATOR_TYPE.BROWSER_CRYPTO); 234 | } else { 235 | return new Random(RANDOM_GENERATOR_TYPE.ALEA, { seeds: _getBrowserSeeds() }); 236 | } 237 | } else { 238 | return new Random(RANDOM_GENERATOR_TYPE.NODE_CRYPTO); 239 | } 240 | } 241 | return _defaultRandomGenerator; 242 | } 243 | }, { 244 | key: 'createWithSeeds', 245 | value: function createWithSeeds() { 246 | (0, _invariant2.default)(arguments.length, 'Random.createWithSeeds(...): no seeds were provided'); 247 | 248 | return new Random(RANDOM_GENERATOR_TYPE.ALEA, { seeds: arguments }); 249 | } 250 | }]); 251 | 252 | return Random; 253 | }(); 254 | 255 | exports.default = Random; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [MarsDB](https://github.com/c58/marsdb) 4 | ========= 5 | 6 | [![Build Status](https://travis-ci.org/c58/marsdb.svg?branch=master)](https://travis-ci.org/c58/marsdb) 7 | [![npm version](https://badge.fury.io/js/marsdb.svg)](https://www.npmjs.com/package/marsdb) 8 | [![Coverage Status](https://coveralls.io/repos/c58/marsdb/badge.svg?branch=master&service=github)](https://coveralls.io/github/c58/marsdb?branch=master) 9 | [![Dependency Status](https://david-dm.org/c58/marsdb.svg)](https://david-dm.org/c58/marsdb) 10 | [![bitHound Overall Score](https://www.bithound.io/github/c58/marsdb/badges/score.svg)](https://www.bithound.io/github/c58/marsdb) 11 | [![Join the chat at https://gitter.im/c58/marsdb](https://badges.gitter.im/c58/marsdb.svg)](https://gitter.im/c58/marsdb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 12 | [![GitHub stars](https://img.shields.io/github/stars/c58/marsdb.svg?style=social)](https://github.com/c58/marsdb) 13 | 14 | MarsDB is a lightweight client-side database. 15 | It's based on a Meteor's **minimongo** matching/modifying implementation. It's carefully written on **ES6**, have a **Promise based** interface and may be backed with any storage implementation ([see plugins](https://github.com/c58/marsdb#plugins)). It's also supports **observable** cursors. 16 | 17 | MarsDB supports any kind of find/update/remove operations that Meteor's minimongo does. So, go to the Meteor docs for supported query/modifier operations. 18 | 19 | You can use it in any JS environment (Browser, Electron, NW.js, Node.js). 20 | 21 | ## Features 22 | 23 | * **Promise based API** 24 | * **Carefully written on ES6** 25 | * **Very very flexible** – just take a look to the [plugins section](https://github.com/c58/marsdb#plugins) 26 | * **Supports many of MongoDB query/modify operations** – thanks to a Meteor's minimongo 27 | * **Flexible pipeline** – map, reduce, custom sorting function, filtering. All with a sexy JS interface (no ugly mongo's aggregation language) 28 | * **Persistence API** – all collections can be stored (and restored) with any kind of storage (in-memory, LocalStorage, LevelUP, etc) 29 | * **Observable queries** - live queries just like in Meteor, but with simplier interface 30 | * **Reactive joins** – out of the box 31 | 32 | ## Bindings 33 | 34 | * [React](https://github.com/c58/marsdb-react) 35 | * [AngularJS 1.x](https://github.com/c58/marsdb-angular) 36 | 37 | ## Plugins 38 | 39 | * In-memory storage (built-in default) 40 | * [LocalForage storage](https://github.com/c58/marsdb-localforage) – fastest in-browser storage (InexedDB, WebSQL and fallback to localStorage) 41 | * [LocalStorage storage](https://github.com/c58/marsdb-localstorage) – not recommended, better prefer LocalForage 42 | * [LevelUP storage](https://github.com/c58/marsdb-levelup) – lightweight server-less Node.js storage 43 | * [MongoDB wrapper](https://github.com/c58/marsdb-mongo) – use MarsDB for comfortable work with MongoDB 44 | * [Validation via Mongoose](https://github.com/c58/marsdb-validation) – validate objects with Mongoose 45 | 46 | ## Meteor compatible client/server 47 | Sometimes you can't use Meteor infrastructure. Maybe you need to build a custom client. Maybe you need to build a custom server with express and other modules. In meteor it can be done with a ton of hack. But the only reason why it's so ugly to do a simple things is because Meteor forces you to use their infrastructure. I'm trying to solve this issue with DDP client/server modules, based on MarsDB. 48 | 49 | * [DDP client](https://github.com/c58/marsdb-sync-client) 50 | * [DDP server](https://github.com/c58/marsdb-sync-server) 51 | 52 | ## Examples 53 | 54 | ### Using within non-ES6 environment 55 | The `./dist` folder contains already compiled to a ES5 code, but some polyfills needed. For using in a browser you must to include `marsdb.polyfills.js` before `marsdb.min.js`. In node.js you need to `require('marsdb/polyfills')`. 56 | It sets in a window/global: Promise, Set and Symbol. 57 | 58 | ### Create a collection 59 | ```javascript 60 | import Collection from 'marsdb'; 61 | import LocalForageManager from 'marsdb-localforage'; 62 | 63 | // Default storage is in-memory 64 | // Setup different storage managers 65 | // (all documents will be save in a browser cache) 66 | Collection.defaultStorageManager(LocalForageManager); 67 | 68 | // Create collection wit new default storage 69 | const users = new Collection('users'); 70 | ``` 71 | 72 | ### Create an in-memory collection 73 | ```javascript 74 | import Collection from 'marsdb'; 75 | import LocalStorageManager from 'marsdb-localstorage'; 76 | 77 | // Set some defaults and create collection 78 | Collection.defaultStorageManager(LocalStorageManager); 79 | const users = new Collection('users'); 80 | 81 | // But it may be useful to create in-memory 82 | // collection without defined defaults 83 | // (for example to save some session state) 84 | const session = new Collection('session', {inMemory: true}); 85 | ``` 86 | 87 | ### Find documents 88 | ```javascript 89 | const posts = new Collection('posts'); 90 | posts.find({author: 'Bob'}) 91 | .project({author: 1}) 92 | .sort(['createdAt']) 93 | .then(docs => { 94 | // do something with docs 95 | }); 96 | ``` 97 | 98 | ### Find with pipeline (map, reduce, filter) 99 | An order of pipeline methods invokation is important. Next pipeline operation gives as argument a result of a previous operation. 100 | ```javascript 101 | const posts = new Collection('posts'); 102 | 103 | // Get number of all comments in the DB 104 | posts.find() 105 | .limit(10) 106 | .sortFunc((a, b) => a - b + 10) 107 | .filter(doc => Matsh.sqrt(doc.comment.length) > 1.5) 108 | .map(doc => doc.comments.length) 109 | .reduce((acum, val) => acum + val) 110 | .then(result => { 111 | // result is a number of all comments 112 | // in all found posts 113 | }); 114 | 115 | // Result is `undefined` because posts 116 | // is not exists and additional processing 117 | // is not ran (thanks to `.ifNotEmpty()`) 118 | posts.find({author: 'not_existing_name'}) 119 | .aggregate(docs => docs[0]) 120 | .ifNotEmpty() 121 | .aggregate(user => user.name) 122 | ``` 123 | 124 | ### Find with observing changes 125 | Observable cursor returned by a `find` and `findOne` methods of a collection. Updates of the cursor is batched and debounced (default batch size is `20` and debounce time is `1000 / 15` ms). You can change the paramters by `batchSize` and `debounce` methods of an observable cursor (methods is chained). 126 | 127 | ```javascript 128 | const posts = new Collection('posts'); 129 | const stopper = posts.find({tags: {$in: ['marsdb', 'is', 'awesome']}}) 130 | .observe(docs => { 131 | // invoked on every result change 132 | // (on initial result too) 133 | stopper.stop(); // stops observing 134 | }).then(docs => { 135 | // invoked once on initial result 136 | // (after `observer` callback) 137 | }); 138 | ``` 139 | 140 | ### Find with joins 141 | ```javascript 142 | const users = new Collection('users'); 143 | const posts = new Collection('posts'); 144 | posts.find() 145 | .join(doc => { 146 | // Return a Promise for waiting of the result. 147 | return users.findOne(doc.authorId).then(user => { 148 | doc.authorObj = user; 149 | // any return is ignored 150 | }); 151 | }) 152 | .join(doc => { 153 | // For reactive join you must invoke `observe` instead `then` 154 | // That's it! 155 | return users.findOne(doc.authorId).observe(user => { 156 | doc.authorObj = user; 157 | }); 158 | }) 159 | .join((doc, updated) => { 160 | // Also any other “join” mutations supported 161 | doc.another = _cached_data_by_post[doc._id]; 162 | 163 | // Manually update a joined parameter and propagate 164 | // update event from current cursor to a root 165 | // (`observe` callback invoked) 166 | setTimeout(() => { 167 | doc.another = 'some another user'; 168 | updated(); 169 | }, 10); 170 | }) 171 | // Or just pass join spec object for fast joining 172 | // (only one `find` will be produced for all posts) 173 | .join({ authorId: users }) // posts[i].authorId will be user object 174 | .observe((posts) => { 175 | // do something with posts with authors 176 | // invoked any time when posts changed 177 | // (and when observed joins changed too) 178 | }) 179 | ``` 180 | 181 | ### Inserting 182 | ```javascript 183 | const posts = new Collection('posts'); 184 | posts.insert({text: 'MarsDB is awesome'}).then(docId => { 185 | // Invoked after persisting document 186 | }) 187 | posts.insertAll( 188 | {text: 'MarsDB'}, 189 | {text: 'is'}, 190 | {text: 'awesome'} 191 | ).then(docsIds => { 192 | // invoked when all documents inserted 193 | }); 194 | ``` 195 | 196 | ### Updating 197 | ```javascript 198 | const posts = new Collection('posts'); 199 | posts.update( 200 | {authorId: {$in: [1, 2, 3]}}, 201 | {$set: {text: 'noop'}} 202 | ).then(result => { 203 | console.log(result.modified) // count of modified docs 204 | console.log(result.updated) // array of updated docs 205 | console.log(result.original) // array of original docs 206 | }); 207 | 208 | // Upsert (insert when nothing found) 209 | posts.update( 210 | {authorId: "123"}, 211 | {$set: {text: 'noop'}}, 212 | {upsert: true} 213 | ).then(result => { 214 | // { authorId: "123", text: 'noop', _id: '...' } 215 | }); 216 | ``` 217 | 218 | ### Removing 219 | ```javascript 220 | const posts = new Collection('posts'); 221 | posts.remove({authorId: {$in: [1,2,3]}}) 222 | .then(removedDocs => { 223 | // do something with removed documents array 224 | }); 225 | ``` 226 | 227 | ## Roadmap 228 | * Indexes support for some kind of simple requests {a: '^b'}, {a: {$lt: 9}} 229 | * Documentation 230 | 231 | ## Contributing 232 | I'm waiting for your pull requests and issues. 233 | Don't forget to execute `gulp lint` before requesting. Accepted only requests without errors. 234 | 235 | ## License 236 | See [License](LICENSE) 237 | -------------------------------------------------------------------------------- /lib/CursorObservable.js: -------------------------------------------------------------------------------- 1 | import _bind from 'fast.js/function/bind'; 2 | import _check from 'check-types'; 3 | import _values from 'fast.js/object/values'; 4 | import _map from 'fast.js/map'; 5 | import Cursor from './Cursor'; 6 | import EJSON from './EJSON'; 7 | import PromiseQueue from './PromiseQueue'; 8 | import debounce from './debounce'; 9 | 10 | 11 | // Defaults 12 | let _defaultDebounce = 1000 / 60; 13 | let _defaultBatchSize = 10; 14 | 15 | /** 16 | * Observable cursor is used for making request auto-updatable 17 | * after some changes is happen in a database. 18 | */ 19 | export class CursorObservable extends Cursor { 20 | constructor(db, query, options) { 21 | super(db, query, options); 22 | this.maybeUpdate = _bind(this.maybeUpdate, this); 23 | this._observers = 0; 24 | this._updateQueue = new PromiseQueue(1); 25 | this._propagateUpdate = debounce(_bind(this._propagateUpdate, this), 0, 0); 26 | this._doUpdate = debounce( 27 | _bind(this._doUpdate, this), 28 | _defaultDebounce, 29 | _defaultBatchSize 30 | ); 31 | } 32 | 33 | static defaultDebounce() { 34 | if (arguments.length > 0) { 35 | _defaultDebounce = arguments[0]; 36 | } else { 37 | return _defaultDebounce; 38 | } 39 | } 40 | 41 | static defaultBatchSize() { 42 | if (arguments.length > 0) { 43 | _defaultBatchSize = arguments[0]; 44 | } else { 45 | return _defaultBatchSize; 46 | } 47 | } 48 | 49 | /** 50 | * Change a batch size of updater. 51 | * Btach size is a number of changes must be happen 52 | * in debounce interval to force execute debounced 53 | * function (update a result, in our case) 54 | * 55 | * @param {Number} batchSize 56 | * @return {CursorObservable} 57 | */ 58 | batchSize(batchSize) { 59 | this._doUpdate.updateBatchSize(batchSize); 60 | return this; 61 | } 62 | 63 | /** 64 | * Change debounce wait time of the updater 65 | * @param {Number} waitTime 66 | * @return {CursorObservable} 67 | */ 68 | debounce(waitTime) { 69 | this._doUpdate.updateWait(waitTime); 70 | return this; 71 | } 72 | 73 | /** 74 | * Observe changes of the cursor. 75 | * It returns a Stopper – Promise with `stop` function. 76 | * It is been resolved when first result of cursor is ready and 77 | * after first observe listener call. 78 | * 79 | * @param {Function} 80 | * @param {Object} options 81 | * @return {Stopper} 82 | */ 83 | observe(listener, options = {}) { 84 | // Make possible to obbserver w/o callback 85 | listener = listener || function() {}; 86 | 87 | // Start observing when no observers created 88 | if (this._observers <= 0) { 89 | this.db.on('insert', this.maybeUpdate); 90 | this.db.on('update', this.maybeUpdate); 91 | this.db.on('remove', this.maybeUpdate); 92 | } 93 | 94 | // Create observe stopper for current listeners 95 | let running = true; 96 | const self = this; 97 | function stopper() { 98 | if (running) { 99 | running = false; 100 | self._observers -= 1; 101 | self.removeListener('update', listener); 102 | self.removeListener('stop', stopper); 103 | 104 | // Stop observing a cursor if no more observers 105 | if (self._observers === 0) { 106 | self._latestIds = null; 107 | self._latestResult = null; 108 | self._updatePromise = null; 109 | self.emit('observeStopped'); 110 | self.db.removeListener('insert', self.maybeUpdate); 111 | self.db.removeListener('update', self.maybeUpdate); 112 | self.db.removeListener('remove', self.maybeUpdate); 113 | } 114 | } 115 | } 116 | 117 | // Start listening for updates and global stop 118 | this._observers += 1; 119 | this.on('update', listener); 120 | this.on('stop', stopper); 121 | 122 | // Get first result for observer or initiate 123 | // update at first time 124 | if (!this._updatePromise) { 125 | this.update(true, true); 126 | } else if (this._latestResult !== null) { 127 | listener(this._latestResult); 128 | } 129 | 130 | // Wrap returned promise with useful fields 131 | const cursorPromiseMixin = { stop: stopper }; 132 | return this._createCursorPromise( 133 | this._updatePromise, cursorPromiseMixin 134 | ); 135 | } 136 | 137 | /** 138 | * Stop all observers of the cursor by one call 139 | * of this function. 140 | * It also stops any delaied update of the cursor. 141 | */ 142 | stopObservers() { 143 | this._doUpdate.cancel(); 144 | this.emit('stop'); 145 | return this; 146 | } 147 | 148 | /** 149 | * Executes an update. It is guarantee that 150 | * one `_doUpdate` will be executed at one time. 151 | * @return {Promise} 152 | */ 153 | update(firstRun = false, immidiatelly = false) { 154 | if (!immidiatelly) { 155 | if (this._updateDebPromise && !this._updateDebPromise.debouncePassed) { 156 | this._doUpdate(firstRun); 157 | return this._updatePromise; 158 | } else if ( 159 | this._updateDebAdded && 160 | (!this._updateDebPromise || !this._updateDebPromise.debouncePassed) 161 | ) { 162 | return this._updatePromise; 163 | } else { 164 | this._updateDebAdded = true; 165 | } 166 | } 167 | 168 | this._updatePromise = this._updateQueue.add(() => { 169 | if (immidiatelly) { 170 | return this._doUpdate.func(firstRun); 171 | } else { 172 | this._updateDebAdded = true; 173 | this._updateDebPromise = this._doUpdate(firstRun); 174 | return this._updateDebPromise.then(() => { 175 | this._updateDebAdded = false; 176 | this._updateDebPromise = null; 177 | }); 178 | } 179 | }); 180 | 181 | return this._updatePromise; 182 | } 183 | 184 | /** 185 | * Consider to update a query by given newDoc and oldDoc, 186 | * received form insert/udpate/remove oparation. 187 | * Should make a decision as smart as possible. 188 | * (Don't update a cursor if it does not change a result 189 | * of a cursor) 190 | * 191 | * TODO we should update _latestResult by hands in some cases 192 | * without a calling of `update` method 193 | * 194 | * @param {Object} newDoc 195 | * @param {Object} oldDoc 196 | */ 197 | maybeUpdate(newDoc, oldDoc) { 198 | // When no newDoc and no oldDoc provided then 199 | // it's a special case when no data about update 200 | // available and we always need to update a cursor 201 | const alwaysUpdateCursor = newDoc === null && oldDoc === null; 202 | 203 | // When it's remove operation we just check 204 | // that it's in our latest result ids list 205 | const removedFromResult = alwaysUpdateCursor || ( 206 | !newDoc && oldDoc && 207 | (!this._latestIds || this._latestIds.has(oldDoc._id)) 208 | ); 209 | 210 | // When it's an update operation we check four things 211 | // 1. Is a new doc or old doc matched by a query? 212 | // 2. Is a new doc has different number of fields then an old doc? 213 | // 3. Is a new doc not equals to an old doc? 214 | const updatedInResult = removedFromResult || (newDoc && oldDoc && ( 215 | this._matcher.documentMatches(newDoc).result || 216 | this._matcher.documentMatches(oldDoc).result 217 | ) && !EJSON.equals(newDoc, oldDoc) 218 | ); 219 | 220 | // When it's an insert operation we just check 221 | // it's match a query 222 | const insertedInResult = updatedInResult || (newDoc && !oldDoc && ( 223 | this._matcher.documentMatches(newDoc).result 224 | )); 225 | 226 | if (insertedInResult) { 227 | return this.update(); 228 | } 229 | } 230 | 231 | /** 232 | * DEBOUNCED 233 | * Emits an update event with current result of a cursor 234 | * and call this method on parent cursor if it exists 235 | * and if it is not first run of update. 236 | * @return {Promise} 237 | */ 238 | _propagateUpdate(firstRun = false) { 239 | const updatePromise = this.emitAsync( 240 | 'update', this._latestResult, firstRun 241 | ); 242 | 243 | let parentUpdatePromise; 244 | if (!firstRun) { 245 | parentUpdatePromise = Promise.all( 246 | _values(_map(this._parentCursors, (v, k) => { 247 | if (v._propagateUpdate) { 248 | return v._propagateUpdate(false); 249 | } 250 | })) 251 | ); 252 | } 253 | 254 | return updatePromise.then(() => parentUpdatePromise); 255 | } 256 | 257 | /** 258 | * DEBOUNCED 259 | * Execute query and propagate result to observers. 260 | * Resolved with result of execution. 261 | * @param {Boolean} firstRun 262 | * @return {Promise} 263 | */ 264 | _doUpdate(firstRun = false) { 265 | if (!firstRun) { 266 | this.emit('cursorChanged'); 267 | } 268 | 269 | return this.exec().then((result) => { 270 | this._updateLatestIds(); 271 | return this._propagateUpdate(firstRun) 272 | .then(() => result); 273 | }); 274 | } 275 | 276 | /** 277 | * By a `_latestResult` update a `_latestIds` field of 278 | * the object 279 | */ 280 | _updateLatestIds() { 281 | const idsArr = _check.array(this._latestResult) 282 | ? _map(this._latestResult, x => x._id) 283 | : this._latestResult && [this._latestResult._id]; 284 | this._latestIds = new Set(idsArr); 285 | } 286 | 287 | /** 288 | * Track child cursor and stop child observer 289 | * if this cusros stopped or changed. 290 | * @param {CursorPromise} cursorPromise 291 | */ 292 | _trackChildCursorPromise(cursorPromise) { 293 | super._trackChildCursorPromise(cursorPromise); 294 | if (cursorPromise.stop) { 295 | this.once('cursorChanged', cursorPromise.stop); 296 | this.once('observeStopped', cursorPromise.stop); 297 | this.once('beforeExecute', cursorPromise.stop); 298 | } 299 | } 300 | } 301 | 302 | export default CursorObservable; 303 | --------------------------------------------------------------------------------