├── spec ├── .keep ├── example.js ├── csv.js ├── moment.js ├── _runner.js ├── jsonpath.js ├── cursor.js └── distinct_and_count.js ├── .gitignore ├── src ├── lodash.js ├── banner.js ├── collection.js ├── storable_cursors.js ├── cursor.js ├── temporary_collections.js ├── distinct_and_count.js ├── commands.js ├── csv.js ├── jsonpath.js └── moment.js ├── .vimrc ├── .jshintrc ├── bower.json ├── .travis.yml ├── LICENSE ├── package.json ├── CHANGELOG.md ├── Gruntfile.js └── README.md /spec/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules 3 | bower_components 4 | -------------------------------------------------------------------------------- /src/lodash.js: -------------------------------------------------------------------------------- 1 | _.mixin({ 2 | shellPrint: function(thing) { 3 | return tojson(thing.valueOf()) 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /src/banner.js: -------------------------------------------------------------------------------- 1 | /* global chatty */ 2 | 3 | chatty('\033[1;32m+ MongoDB Shell Extensions (###version###) by Gabriele Lana \033[0m') 4 | -------------------------------------------------------------------------------- /src/collection.js: -------------------------------------------------------------------------------- 1 | DBCollection.prototype.last = function(n) { 2 | return this.find().sortAsInserted().last(n) 3 | } 4 | 5 | DBCollection.prototype.first = function(n) { 6 | return this.find().sortAsInserted().first(n) 7 | } 8 | -------------------------------------------------------------------------------- /src/storable_cursors.js: -------------------------------------------------------------------------------- 1 | DBQuery.prototype.save = function(collection) { 2 | collection = (typeof collection === 'string') ? this._db.getCollection(collection) : collection 3 | collection = collection || this._db.createTemporaryCollection() 4 | assert(collection && collection.constructor.name === 'DBCollection', 'need a collection') 5 | while (this.hasNext()) { 6 | collection.save(this.next()) 7 | } 8 | return collection 9 | } 10 | -------------------------------------------------------------------------------- /spec/example.js: -------------------------------------------------------------------------------- 1 | assert.that('1+1 should be 2', function(c) { 2 | assert.eq(2, 1+1) 3 | }) 4 | 5 | assert.that( 6 | 'tearDown will be called', 7 | function(c) { 8 | this.originalAssertEqual = assert.eq 9 | assert.eq = false 10 | }, 11 | function(c) { 12 | assert.eq = this.originalAssertEqual 13 | } 14 | ) 15 | 16 | assert.that('previous tearDown was called', function(c) { 17 | assert.neq(false, assert.eq, 'previous tearDown was not called') 18 | }) 19 | -------------------------------------------------------------------------------- /.vimrc: -------------------------------------------------------------------------------- 1 | " autoload the local .vimrc file you need to have 2 | " https://github.com/MarcWeber/vim-addon-local-vimrc 3 | " plugin installed 4 | 5 | let g:ctrlp_custom_ignore = '\.git$\|\.tmp$\|node_modules$\|bower_components$' 6 | 7 | let g:syntastic_mode_map = {'mode': 'passive', 'active_filetypes': ['javascript']} 8 | let g:syntastic_javascript_jshint_args = '-c .jshintrc' 9 | let g:syntastic_check_on_open = 1 10 | 11 | nnoremap t :!mocha --recursive --reporter spec --colors test/ 12 | nnoremap f :!mocha --reporter spec --colors % 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "_": false, 4 | "moment": false, 5 | "it": false, 6 | "db": false, 7 | "tojson": false, 8 | "print": false, 9 | "printjson": false, 10 | "assert": false, 11 | "DB": false, 12 | "DBQuery": false, 13 | "DBCollection": false, 14 | "ObjectId": false 15 | }, 16 | "evil": true, 17 | "node": true, 18 | "browser": true, 19 | "asi": true, 20 | "strict": false, 21 | "eqeqeq": true, 22 | "latedef": false, 23 | "immed": true, 24 | "undef": true, 25 | "unused": false, 26 | "trailing": true, 27 | "funcscope": true, 28 | "camelcase": true, 29 | "quotmark": "single", 30 | "curly": true, 31 | "expr": true, 32 | "sub": true 33 | } 34 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongodb-shell-extensions", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/gabrielelana/mongodb-shell-extensions", 5 | "authors": [ 6 | "gabriele.lana " 7 | ], 8 | "description": "Useful MongoDB shell extensions", 9 | "keywords": [ 10 | "mongodb", 11 | "shell", 12 | "extensions" 13 | ], 14 | "license": "MIT", 15 | "private": true, 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ], 23 | "dependencies": { 24 | "lodash": "^2.4.0", 25 | "moment": "^2.10.0", 26 | "moment-timezone": "^0.4.0", 27 | "moment-range": "^1.0.0", 28 | "sprintf": "~1.0.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | env: 5 | - MONGODB_VERSION=2.6.10 6 | - MONGODB_VERSION=3.0.3 7 | before_install: 8 | - npm install --global bower grunt-cli 9 | - bower --quiet install 10 | before_script: 11 | - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 12 | - echo "deb http://repo.mongodb.org/apt/ubuntu `lsb_release -sc`/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb_org.list 13 | - sudo apt-get update 14 | - sudo apt-get install -y mongodb-org=$MONGODB_VERSION mongodb-org-server=$MONGODB_VERSION mongodb-org-shell=$MONGODB_VERSION mongodb-org-mongos=$MONGODB_VERSION mongodb-org-tools=$MONGODB_VERSION 15 | - sleep 15 16 | - mongo --version 17 | script: 18 | - grunt spec-on-head 19 | -------------------------------------------------------------------------------- /spec/csv.js: -------------------------------------------------------------------------------- 1 | /* global tocsv, flatten */ 2 | 3 | assert.that('CSV constructor should have a name', function(c) { 4 | assert.eq(tocsv([{a:1}]).constructor.name, 'CSV') 5 | }) 6 | 7 | assert.that('CSV first line contains field names', function(c) { 8 | assert.eq(tocsv([{a:1}]).lines[0], 'a') 9 | assert.eq(tocsv([{a:1, b: 2}]).lines[0], 'a,b') 10 | }) 11 | 12 | assert.that('CSV documents could have different fields', function(c) { 13 | assert.eq(tocsv([{a:1}, {b:2}]).lines[0], 'a,b') 14 | assert.eq(tocsv([{a:1}, {b:2}]).lines[1], '1,') 15 | assert.eq(tocsv([{a:1}, {b:2}]).lines[2], ',2') 16 | }) 17 | 18 | assert.that('CSV support nested fields', function(c) { 19 | assert.eq(tocsv([{a:{b:2}}]).lines[0], 'a.b') 20 | assert.eq(tocsv([{a:{b:2}}]).lines[1], '2') 21 | assert.eq(tocsv([{a:{b:2,c:3}}]).lines[0], 'a.b,a.c') 22 | assert.eq(tocsv([{a:{b:2,c:3}}]).lines[1], '2,3') 23 | }) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Gabriele Lana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongodb-shell-extensions", 3 | "version": "0.2.9", 4 | "description": "Useful MongoDB shell extensions", 5 | "files": [ 6 | "src", 7 | "spec", 8 | "released", 9 | ".jshintrc", 10 | "LICENSE", 11 | "README.md", 12 | "CHANGELOG.md", 13 | "package.json", 14 | "bower.json", 15 | "Gruntfile.js" 16 | ], 17 | "scripts": { 18 | "install": "grunt install-released", 19 | "test": "grunt spec-on-installed" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/gabrielelana/mongodb-shell-extensions.git" 24 | }, 25 | "keywords": [ 26 | "mongo", 27 | "mongodb", 28 | "shell", 29 | "extensions" 30 | ], 31 | "license": "MIT", 32 | "author": "gabriele.lana@gmail.com", 33 | "dependencies": { 34 | "grunt": "^0.4.5", 35 | "grunt-cli": "^0.1.13", 36 | "grunt-release": "^0.7.0", 37 | "grunt-bower-concat": "^0.3.0", 38 | "grunt-contrib-concat": "^0.5.0", 39 | "grunt-contrib-jshint": "^0.10.0", 40 | "grunt-contrib-uglify": "^0.5.1", 41 | "grunt-contrib-clean": "^0.6.0", 42 | "grunt-contrib-copy": "^0.5.0", 43 | "grunt-attention": "^0.0.5", 44 | "semver": "^3.0.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spec/moment.js: -------------------------------------------------------------------------------- 1 | assert.that('moment.$in creates query for ranges of time', function(c) { 2 | var aMinuteAgo = (1).minute().ago().toDate() 3 | var aDayAgo = (1).day().ago().toDate() 4 | 5 | c.save({'created_at': aMinuteAgo}) 6 | c.save({'created_at': aDayAgo}) 7 | 8 | var createdInLast3Minutes = c.find({'created_at': moment.$in(moment.last(3).minutes())}) 9 | 10 | assert.eq(createdInLast3Minutes.count(), 1) 11 | }) 12 | 13 | assert.that('moment.$in requires a date range', function(c) { 14 | assert.throws(function() { 15 | moment.$in(null) 16 | }, [], 'null should not be accepted as date range') 17 | assert.throws(function() { 18 | moment.$in({}) 19 | }, [], 'and empty object should not be accepted as date range') 20 | assert.throws(function() { 21 | moment.$in(moment()) 22 | }, [], 'a Moment should not be accepted as date range') 23 | assert.doesNotThrow(function() { 24 | moment.$in(moment().range(moment.now(), moment.now())) 25 | }, [], 'a DateRange should be accepted as date range') 26 | }) 27 | 28 | assert.that('moment.last (incomplete range) when printed gives useful informations', function() { 29 | assert.eq(moment.last(5).toString(), '5 of what?') 30 | }) 31 | 32 | assert.that('moment.next (incomplete range) when printed gives useful informations', function() { 33 | assert.eq(moment.next(5).toString(), '5 of what?') 34 | }) 35 | 36 | assert.that('incomplete duration when printed gives useful informations', function() { 37 | assert.eq((5).days(), '5 days') 38 | }) 39 | -------------------------------------------------------------------------------- /src/cursor.js: -------------------------------------------------------------------------------- 1 | /* global shellHelper, __prettyShell:true */ 2 | 3 | DBQuery.prototype.reverse = function() { 4 | this._checkModify(); 5 | if (!this._query.orderby || _.isEmpty(this._query.orderby)) { 6 | this._addSpecial('orderby', {'$natural': 1}) 7 | } 8 | for (var field in this._query.orderby) { 9 | this._query.orderby[field] = this._query.orderby[field] * -1 10 | } 11 | return this 12 | } 13 | 14 | DBQuery.prototype.sortAsInserted = function() { 15 | this._checkModify(); 16 | this._addSpecial('orderby', {'$natural': 1}) 17 | return this 18 | } 19 | 20 | DBQuery.prototype.sortById = function() { 21 | this._checkModify(); 22 | this._addSpecial('orderby', {'_id': 1}) 23 | return this 24 | } 25 | 26 | DBQuery.prototype.last = DBQuery.prototype.tail = 27 | function(n) { 28 | return this.reverse().first(n) 29 | } 30 | 31 | DBQuery.prototype.first = DBQuery.prototype.head = 32 | function(n) { 33 | return this.limit(n || 1) 34 | } 35 | 36 | DBQuery.prototype.sample = function(n) { 37 | return _.sample(this.toArray(), n || 1) 38 | } 39 | 40 | DBQuery.prototype.tojson = function() { 41 | return tojson(this.toArray()) 42 | } 43 | 44 | DBQuery.prototype.ugly = function(){ 45 | this._prettyShell = false; 46 | return this; 47 | } 48 | 49 | ;(function(shellPrint, prettyShell) { 50 | DBQuery.prototype.shellPrint = function() { 51 | prettyShell = this._prettyShell 52 | this._prettyShell = this._prettyShell || __prettyShell 53 | var result = shellPrint.call(this) 54 | this._prettyShell = prettyShell 55 | return result 56 | } 57 | })(DBQuery.prototype.shellPrint) 58 | -------------------------------------------------------------------------------- /spec/_runner.js: -------------------------------------------------------------------------------- 1 | /* global listFiles, load, assert, doassert */ 2 | 3 | var files = listFiles('.'), 4 | startedAt = new Date(), 5 | testFilesToSkip = [] 6 | 7 | if (db) { 8 | print(' ### MongoDB(' + db.version() + ')') 9 | } 10 | 11 | assert.that = function(description, assertion, tearDown) { 12 | assert( 13 | 'test' === db.getName(), 14 | 'You cannot run this tests in db \'' + db.getName() + '\'.\n' + 15 | 'Retry with `mongo --quiet _runner.js`' 16 | ) 17 | var context = {}, runInCollection = db.getCollection('mongodb-shell-extensions') 18 | runInCollection.drop() 19 | try { 20 | assertion.call(context, runInCollection, db) 21 | } finally { 22 | if (tearDown) { 23 | tearDown.call(context, runInCollection, db) 24 | } 25 | } 26 | } 27 | 28 | if (!assert.doesNotThrow) { 29 | assert.doesNotThrow = function(func, params, msg) { 30 | if (assert._debug && msg) { 31 | print('in assert for: ' + msg); 32 | } 33 | if (params && typeof(params) === 'string') { 34 | throw ('2nd argument to assert.throws has to be an array, not ' + params); 35 | } 36 | try { 37 | func.apply(null, params); 38 | } 39 | catch (e) { 40 | doassert('threw unexpected exception: ' + e + ' : ' + msg); 41 | } 42 | return; 43 | }; 44 | } 45 | 46 | _(files).sortBy('name').forEach(function(x) { 47 | var testFileName = x.name.replace(/^\.\//, '') 48 | if (/[\/\\]_/.test(x.name) || !/\.js$/.test(x.name) || testFilesToSkip.indexOf(testFileName) >= 0) { 49 | return print(' ### skipping: ' + testFileName) 50 | } 51 | print(' >>> running: ' + testFileName + '... ok! in ' + Date.timeFunc(function() {load(x.name)}, 1) + 'ms') 52 | }) 53 | 54 | var endedAt = new Date() 55 | 56 | print('time: ' + ((endedAt.getTime() - startedAt.getTime()) / 1000 ) + 's') 57 | -------------------------------------------------------------------------------- /spec/jsonpath.js: -------------------------------------------------------------------------------- 1 | 2 | assert.that('DBQuery#select selects and flattens nested fields', function(c) { 3 | c.save({field: {nested: {nested: 'value_1', another: 'value_2'}}}) 4 | c.save({field: {nested: {nested: 'value_1', another: 'value_2'}}}) 5 | c.save({field: {nested: {nested: 'value_1', another: 'value_2'}}}) 6 | 7 | var resultWithoutSelect = c.find() 8 | 9 | assert.eq(resultWithoutSelect[0].field.nested.nested, 'value_1') 10 | assert.eq(resultWithoutSelect[0].field.nested.another, 'value_2') 11 | 12 | var result = c.find().select('field.nested.nested') 13 | 14 | assert.eq(result[0], {'field.nested.nested': 'value_1'}) 15 | assert.eq(result[1], {'field.nested.nested': 'value_1'}) 16 | assert.eq(result[2], {'field.nested.nested': 'value_1'}) 17 | }) 18 | 19 | assert.that('DBQuery#select selects multiple things', function(c) { 20 | c.save({field: {nested: {nested: 'value_1', another: 'value_2'}}}) 21 | c.save({field: {nested: {nested: 'value_1', another: 'value_2'}}}) 22 | c.save({field: {nested: {nested: 'value_1', another: 'value_2'}}}) 23 | 24 | var result = c.find().select(['field.nested.nested', 'field.nested.another']) 25 | 26 | assert.eq(result[0], {'field.nested.nested': 'value_1', 'field.nested.another': 'value_2'}) 27 | assert.eq(result[1], {'field.nested.nested': 'value_1', 'field.nested.another': 'value_2'}) 28 | assert.eq(result[2], {'field.nested.nested': 'value_1', 'field.nested.another': 'value_2'}) 29 | }) 30 | 31 | assert.that('DBQuery#select selects and rename multiple things', function(c) { 32 | c.save({field: {f1: 1, f2: 2, f3: 3}}) 33 | c.save({field: {f1: 1, f2: 2, f3: 3}}) 34 | c.save({field: {f1: 1, f2: 2, f3: 3}}) 35 | 36 | var result = c.find().select({'field_1': 'field.f1', 'field_2': 'field.f2'}) 37 | 38 | assert.eq(result[0], {'field_1': 1, 'field_2': 2}) 39 | assert.eq(result[1], {'field_1': 1, 'field_2': 2}) 40 | assert.eq(result[2], {'field_1': 1, 'field_2': 2}) 41 | }) 42 | -------------------------------------------------------------------------------- /src/temporary_collections.js: -------------------------------------------------------------------------------- 1 | DB.prototype.collection = function() { 2 | var _arguments = Array.prototype.slice.call(arguments) 3 | if (_arguments.length === 2 && _.isString(_arguments[0]) && _.isFunction(_arguments[1])) { 4 | var collection = this.getCollection(_arguments[0]), 5 | callback = _arguments[1], 6 | result = callback(collection) 7 | return result === undefined ? collection : result 8 | } 9 | if (_arguments.length >= 1 && _.isString(_arguments[0])) { 10 | return this.getCollection(_arguments[0]) 11 | } 12 | if (_arguments.length >= 1 && _.isFunction(_arguments[0])) { 13 | return this.createTemporaryCollection(_arguments[0]) 14 | } 15 | return this 16 | } 17 | 18 | DB.prototype.createTemporaryCollection = function(callback, options) { 19 | var uniquePrefixedName = '__t' + (new ObjectId().valueOf()), 20 | collection = this.getCollection(uniquePrefixedName), 21 | aCallbackIsGiven = !!callback, 22 | result = collection 23 | 24 | options = _({deleteAfter: aCallbackIsGiven}, options) 25 | try { 26 | if (aCallbackIsGiven) { 27 | result = callback(collection) 28 | if ((result === undefined) && !options.deleteAfter) { 29 | result = collection 30 | } 31 | } 32 | } finally { 33 | if (options.deleteAfter) { 34 | collection.drop() 35 | } 36 | } 37 | 38 | return result 39 | } 40 | 41 | DB.prototype.getCollections = function() { 42 | return _(this.getCollectionNames()).map(_.bind(this.getCollection, this)).valueOf() 43 | } 44 | 45 | DB.prototype.getTemporaryCollections = function() { 46 | return _(this.getCollections()).filter(function(c) {return c.isTemporary()}).valueOf() 47 | } 48 | 49 | DB.prototype.dropTemporaryCollections = function() { 50 | this.getTemporaryCollections().forEach(function(c) { 51 | c.drop() 52 | }) 53 | return true 54 | } 55 | 56 | DBCollection.prototype.isTemporary = function() { 57 | return this.getName().substr(0, 3) === '__t' 58 | } 59 | -------------------------------------------------------------------------------- /src/distinct_and_count.js: -------------------------------------------------------------------------------- 1 | /* global isObject: false */ 2 | 3 | DBCollection.prototype.distinctAndCount = function(field, query) { 4 | field = [].concat(field) 5 | query = query || {} 6 | 7 | var groupById = _([].concat(field)).reduce(function(result, key) { 8 | result[key.replace(/\./g, '_')] = '$' + key; return result 9 | }, {}) 10 | 11 | var it = this.aggregate( 12 | {$match: query}, 13 | {$group: {_id: groupById, count: {$sum: 1}}}, 14 | {$project: {values: '$_id', count: 1, _id: 0}} 15 | ) 16 | 17 | var resultIsAnObject = (it.result !== undefined) && (it.ok !== undefined) 18 | if (resultIsAnObject && it.ok === 0) { 19 | return it 20 | } 21 | 22 | var result = it.result || it.toArray() 23 | return _.reduce(result, function(all, r) { 24 | 25 | var isValidValue = 26 | _(r.values) 27 | .chain() 28 | .values() 29 | .map(function(value) { 30 | if (value === null) { 31 | return {valueOf: function() {return value}} 32 | } 33 | return value 34 | }) 35 | .any(function(value) { 36 | if (_(value).isArray()) { 37 | return _(value).all(function(value) { 38 | return !_(value).isObject() 39 | }) 40 | } 41 | // we support values like Number or Date but not {} 42 | return value.constructor.name !== '' 43 | }) 44 | .valueOf() 45 | 46 | if (!isValidValue) { 47 | throw 'distinctAndCount could not work when one or more fields are objects: ' + tojson(r.values) 48 | } 49 | 50 | var key = 51 | _(r.values) 52 | .chain() 53 | .values() 54 | .map(function(value) { 55 | if (value === null) { 56 | return '#null#' 57 | } 58 | if (_(value.valueOf).isFunction()) { 59 | value = value.valueOf() 60 | } 61 | if (_(value).isArray()) { 62 | value = _(value).sort().valueOf() 63 | } 64 | return value 65 | }) 66 | .valueOf() 67 | .join(',') 68 | 69 | all[key] = (all[key] || 0) + r.count 70 | 71 | return all 72 | }, {}) 73 | } 74 | -------------------------------------------------------------------------------- /src/commands.js: -------------------------------------------------------------------------------- 1 | /* global shellHelper, __prettyShell:true, sprintf, rs */ 2 | 3 | __prettyShell = false 4 | 5 | shellHelper.pretty = function() { 6 | __prettyShell = true 7 | print('pretty printing: enabled'); 8 | } 9 | 10 | shellHelper.ugly = function() { 11 | __prettyShell = false 12 | print('pretty printing: disabled'); 13 | } 14 | 15 | shellHelper.so = function() { 16 | rs.slaveOk() 17 | } 18 | 19 | ;(function() { 20 | 21 | shellHelper.databases = shellHelper.dbs = shellHelper.d = function() { 22 | var currentDbName = db.getName() 23 | var highlight = function(string) { 24 | return String.fromCharCode(0x1B) + '[32;1m' + string + String.fromCharCode(0x1B) + '[0m' 25 | } 26 | db.getMongo().getDBs().databases.forEach(function(d) { 27 | var numberOfCollections = db.getMongo().getDB(d.name).getCollectionNames().length 28 | print( 29 | sprintf(d.name === currentDbName ? highlight('%-30s\t%s') : '%-30s\t%s', 30 | d.name, 31 | ((d.sizeOnDisk > 1) ? 32 | numberOfCollections + '/' + bytesToSize(d.sizeOnDisk) : 33 | '(empty)' 34 | )) 35 | ); 36 | }) 37 | } 38 | 39 | shellHelper.collections = shellHelper.colls = shellHelper.c = function() { 40 | db.getCollections().forEach(function(c) { 41 | print( 42 | sprintf('%-30s\t%s', c.getName(), 43 | ((c.totalSize() > 0) ? 44 | c.count() + '/' + bytesToSize(c.totalSize()) : 45 | '(empty)' 46 | )) 47 | ); 48 | }) 49 | } 50 | 51 | 52 | function bytesToSize(bytes, precision) { 53 | var kilobyte = 1024; 54 | var megabyte = kilobyte * 1024; 55 | var gigabyte = megabyte * 1024; 56 | var terabyte = gigabyte * 1024; 57 | 58 | precision = precision || 0 59 | 60 | if ((bytes >= 0) && (bytes < kilobyte)) { 61 | return bytes + 'B'; 62 | 63 | } else if ((bytes >= kilobyte) && (bytes < megabyte)) { 64 | return (bytes / kilobyte).toFixed(precision) + 'KB'; 65 | 66 | } else if ((bytes >= megabyte) && (bytes < gigabyte)) { 67 | return (bytes / megabyte).toFixed(precision) + 'MB'; 68 | 69 | } else if ((bytes >= gigabyte) && (bytes < terabyte)) { 70 | return (bytes / gigabyte).toFixed(precision) + 'GB'; 71 | 72 | } else if (bytes >= terabyte) { 73 | return (bytes / terabyte).toFixed(precision) + 'TB'; 74 | 75 | } else { 76 | return bytes + 'B'; 77 | } 78 | } 79 | })() 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release 0.2.5 2 | * `DBCollection.distinctAndCount` on array fields 3 | * `DBCollection.distinctAndCount` on primitive like (ex. `Number`, `ISODate`, …) fields 4 | 5 | # Release 0.2.4 6 | * `toCSV` support for documents with nested fields 7 | 8 | # Release 0.2.3 9 | * Support aggregate framework changes in 2.6 10 | 11 | # Release 0.2.2 12 | * Add release version in startup message 13 | 14 | # Release 0.2.0 15 | * `DBCollection.distinctAndCount` on multiple fields 16 | 17 | # Release 0.1.5 18 | * Fixed CSV constructor without a name 19 | 20 | # Release 0.1.4 21 | * Initial documentation 22 | * Fixed spec run on Travis 23 | 24 | # Release 0.1.3 25 | * Fixed npm install process 26 | 27 | # Release 0.1.2 28 | * Setup spec harness 29 | * Setup continuous integration with `travis-ci` 30 | * User install process doesn't require to build the whole thing 31 | 32 | # Release 0.1.1 33 | * Fixed release process 34 | 35 | # Release 0.1.0 36 | * `DB#getCollections` - get all collections in current db 37 | * `Collection#distinctAndCount` - count how many distinct values 38 | * `Collection#first` - first element inserted in collection 39 | * `Collection#last` - last element inserted in collection 40 | * `Query#reverse` - reverse query sort order 41 | * `Query#first` - return only the first element in result 42 | * `Query#last` - return only the last element in result 43 | * `Query#tojson` - serialize query result using json format 44 | * CSV support 45 | * `Query#tocsv` - serialize query result using csv format 46 | * `Query#printcsv` - print query result using csv format 47 | * `tocsv(x)` - serialize `x` using csv format 48 | * `printcsv(x)` - print `x` using csv format 49 | * JSONPath support 50 | * `Query#select(x)` - filter result using jsonpath expression `x` 51 | * `jsonpath(o, x)` - filter object `o` using jsonpath expression `x` 52 | * Temporary collections 53 | * `DB#collection(f)` - creates a temporary collection that will be available to `f` and automatically destroyed immediately after 54 | * `DB#getTemporaryCollections()` - get all temporary collections 55 | * `DB#dropTempraryCollections()` - drop all temporary collections 56 | * `Collection#isTemporary` - returns true if the collection is temporary 57 | * Storable cursors 58 | * `Query#save(c)` - save the query result into a collection `c` 59 | * Better time manipulation support using moment.js library 60 | * `db.orders.find({created_at: moment.$today()})` - find orders created today, it will generate something like `{$gte: ISODate('2014-02-25'), $lte: ISODate()}` 61 | * `db.orders.find({created_at: moment.$last(3, 'days')})` - find orders created in the last 3 days 62 | * `moment.last(30, 'days').forEach('day', function(m) { db.orders.count({created_at: moment.$inDay(m)}) })` - how many orders per day where created in the last 30 days 63 | -------------------------------------------------------------------------------- /src/csv.js: -------------------------------------------------------------------------------- 1 | DBQuery.prototype.tocsv = function() { 2 | return tocsv(this) 3 | } 4 | 5 | DBQuery.prototype.printcsv = function() { 6 | return printcsv(this) 7 | } 8 | 9 | var CSV = (function(CSV) { 10 | 11 | CSV = function CSV(lines) { 12 | this.lines = lines 13 | } 14 | 15 | CSV.prototype.shellPrint = function() { 16 | this.lines.forEach(function(line) { 17 | print(line) 18 | }) 19 | } 20 | 21 | ;_([ 22 | 'all', 'any', 'at', 'collect', 'contains', 'countBy', 'detect', 'each', 'eachRight', 'every', 23 | 'filter', 'find', 'findLast', 'findWhere', 'foldl', 'foldr', 'forEach', 'forEachRight', 24 | 'groupBy', 'include', 'indexBy', 'inject', 'invoke', 'map', 'max', 'min', 'pluck', 'reduce', 25 | 'reduceRight', 'reject', 'sample', 'select', 'shuffle', 'size', 'some', 'sortBy', 'toArray', 26 | 'where', 'compact', 'difference', 'drop', 'findIndex', 'findLastIndex', 'first', 'flatten', 27 | 'head', 'indexOf', 'initial', 'intersection', 'last', 'lastIndexOf', 'object', 'pull', 28 | 'range', 'remove', 'rest', 'sortedIndex', 'tail', 'take', 'union', 'uniq', 'unique', 29 | 'unzip', 'without', 'xor', 'zip', 'zipObject' 30 | ]).forEach(function(method) { 31 | CSV.prototype[method] = function() { 32 | return _[method].apply(_, [this.lines].concat(_.toArray(arguments))) 33 | } 34 | }) 35 | 36 | CSV.prototype.toString = function() { 37 | return this.lines.join('\n') 38 | } 39 | 40 | return CSV 41 | })() 42 | 43 | 44 | var printcsv = function(x) { 45 | tocsv(x).forEach(function(line) { 46 | print(line) 47 | }) 48 | } 49 | 50 | var tocsv = (function() { 51 | var flatten = function(o) { 52 | return _.reduce(o, function(flattened, value, field) { 53 | if (_.isPlainObject(value)) { 54 | _.forEach(flatten(value), function(nestedValue, nestedField) { 55 | flattened[[field, nestedField].join('.')] = nestedValue 56 | }) 57 | } else { 58 | flattened[field] = value 59 | } 60 | return flattened 61 | }, {}) 62 | } 63 | 64 | return function(x) { 65 | var lines = [], 66 | fieldNames = {}, 67 | encodedDocuments = x.map(function(doc) { 68 | return _.reduce(flatten(doc), function(values, value, field) { 69 | fieldNames[field] = true 70 | values[field] = tojson(value).replace( 71 | /^(?:ISODate|ObjectId)\((.*)\)$/, 72 | function(_, contentAsString) { 73 | return contentAsString 74 | } 75 | ) 76 | return values 77 | }, {}) 78 | }) 79 | 80 | fieldNames = _.keys(fieldNames) 81 | 82 | lines.push(fieldNames.join(',')) 83 | encodedDocuments.forEach(function(encodedDocument) { 84 | lines.push( 85 | fieldNames.map(function(fieldName) { 86 | if (encodedDocument[fieldName] !== undefined) { 87 | return encodedDocument[fieldName] 88 | } 89 | return '' 90 | }).join(',') 91 | ) 92 | }) 93 | 94 | return new CSV(lines) 95 | } 96 | })() 97 | -------------------------------------------------------------------------------- /spec/cursor.js: -------------------------------------------------------------------------------- 1 | // NOTE: Cursor#toArray() is needed because tojson of Cursors is different than 2 | // tojson of Array in some shell versions and that will break assert.eq which 3 | // use tojson to compare values 4 | 5 | assert.that('DBQuery#first returns the first document in natural order', function(c) { 6 | // natural order == the order in which documents are inserted 7 | var o1 = {_id: ObjectId()} // created first -> with smaller _id 8 | var o2 = {_id: ObjectId()} // created last -> with bigger _id 9 | c.save(o2) // inserted first -> should be returned this even if o2._id > o1._id 10 | c.save(o1) 11 | 12 | assert.eq(c.find().first().toArray(), [o2]) 13 | // same as 14 | assert.eq(c.find().sortAsInserted().first().toArray(), [o2]) 15 | // same as 16 | assert.eq(c.first().toArray(), [o2]) 17 | }) 18 | 19 | assert.that('DBQuery#last returns the last document in natural order', function(c) { 20 | // natural order == the order in which documents are inserted 21 | var o1 = {_id: ObjectId()} // created first -> with smaller _id 22 | var o2 = {_id: ObjectId()} // created last -> with bigger _id 23 | c.save(o2) 24 | c.save(o1) // inserted last -> should be returned this even if o1._id < o2._id 25 | 26 | assert.eq(c.find().last().toArray(), [o1]) 27 | // same as 28 | assert.eq(c.find().sortAsInserted().last().toArray(), [o1]) 29 | // same as 30 | assert.eq(c.last().toArray(), [o1]) 31 | }) 32 | 33 | assert.that('DBQuery#first/last maintains the sort order previously given', function(c) { 34 | var o1 = {_id: ObjectId(), value: 2} 35 | var o2 = {_id: ObjectId(), value: 1} 36 | c.save(o1) 37 | c.save(o2) 38 | 39 | assert.eq(c.find().first().toArray(), [o1]) 40 | assert.eq(c.find().sort({value: 1}).first().toArray(), [o2]) 41 | 42 | assert.eq(c.find().last().toArray(), [o2]) 43 | assert.eq(c.find().sort({value: 1}).last().toArray(), [o1]) 44 | }) 45 | 46 | assert.that('DBQuery#sortById', function(c) { 47 | var o1 = {_id: ObjectId()} // created first -> with smaller _id 48 | var o2 = {_id: ObjectId()} // created last -> with bigger _id 49 | c.save(o2) 50 | c.save(o1) 51 | 52 | assert.eq(c.find().sortById().first().toArray(), [o1]) 53 | assert.eq(c.find().sortById().last().toArray(), [o2]) 54 | }) 55 | 56 | 57 | assert.that('DBQuery#reverse the original sort', function(c) { 58 | c.save({field: 21}) 59 | c.save({field: 42}) 60 | 61 | var orderByAsc = c.find().sort({field: 1}) 62 | assert.eq(orderByAsc[0].field, 21) 63 | assert.eq(orderByAsc[1].field, 42) 64 | 65 | var orderByDesc = orderByAsc.clone().reverse() 66 | assert.eq(orderByDesc[0].field, 42) 67 | assert.eq(orderByDesc[1].field, 21) 68 | }) 69 | 70 | assert.that('DBQuery#reverse keeps the original query', function(c) { 71 | c.save({field: 21}) 72 | c.save({field: 42}) 73 | 74 | var query = c.find({field: 42}) 75 | assert.eq(query.count(), 1) 76 | 77 | var reversed = query.clone().reverse() 78 | assert.eq(reversed.count(), 1) 79 | }) 80 | 81 | assert.that('DBQuery#sample takes random samples', function(c) { 82 | c.save({field: 1}) 83 | c.save({field: 2}) 84 | c.save({field: 3}) 85 | c.save({field: 4}) 86 | c.save({field: 5}) 87 | 88 | assert.eq(1, c.find().sample().length) 89 | assert.eq(2, c.find().sample(2).length) 90 | assert.eq(0, c.find({field: 10}).sample().length) 91 | }) 92 | -------------------------------------------------------------------------------- /spec/distinct_and_count.js: -------------------------------------------------------------------------------- 1 | /* global NumberLong:false */ 2 | 3 | assert.that('distinctAndCount works on one field', function(c) { 4 | c.save({field: 'value_1'}) 5 | c.save({field: 'value_2'}) 6 | c.save({field: 'value_1'}) 7 | 8 | var result = c.distinctAndCount('field') 9 | 10 | assert.eq(2, result['value_1']) 11 | assert.eq(1, result['value_2']) 12 | }) 13 | 14 | assert.that('distinctAndCount works on one nested field', function(c) { 15 | c.save({field: {nested: 'value_1'}}) 16 | c.save({field: {nested: 'value_2'}}) 17 | c.save({field: {nested: 'value_1'}}) 18 | 19 | var result = c.distinctAndCount('field.nested') 20 | 21 | assert.eq(2, result['value_1']) 22 | assert.eq(1, result['value_2']) 23 | }) 24 | 25 | assert.that('distinctAndCount works on deeply nested fields', function(c) { 26 | c.save({field: {nested: {nested: {nested: 'value_1'}}}}) 27 | c.save({field: {nested: {nested: {nested: 'value_2'}}}}) 28 | c.save({field: {nested: {nested: {nested: 'value_1'}}}}) 29 | 30 | var result = c.distinctAndCount('field.nested.nested.nested') 31 | 32 | assert.eq(2, result['value_1']) 33 | assert.eq(1, result['value_2']) 34 | }) 35 | 36 | assert.that('distinctAndCount works on multiple fields', function(c) { 37 | c.save({'field_1': 'value_1', 'field_2': 'value_1'}) 38 | c.save({'field_1': 'value_2', 'field_2': 'value_2'}) 39 | c.save({'field_1': 'value_1', 'field_2': 'value_3'}) 40 | c.save({'field_1': 'value_1', 'field_2': 'value_3'}) 41 | 42 | var result = c.distinctAndCount(['field_1', 'field_2']) 43 | 44 | assert.eq(2, result['value_1,value_3']) 45 | assert.eq(1, result['value_2,value_2']) 46 | assert.eq(1, result['value_1,value_1']) 47 | }) 48 | 49 | assert.that('distinctAndCount works on array fields', function(c) { 50 | c.save({field: 'value_1'}) 51 | c.save({field: ['value_2', 'value_3']}) 52 | c.save({field: ['value_3', 'value_2']}) 53 | c.save({field: ['value_1', 'value_2']}) 54 | 55 | var result = c.distinctAndCount('field') 56 | 57 | assert.eq(1, result['value_1']) 58 | assert.eq(2, result['value_2,value_3']) 59 | assert.eq(1, result['value_1,value_2']) 60 | }) 61 | 62 | assert.that('distinctAndCount works on multiple array fields', function(c) { 63 | c.save({'field_1': ['value_1','value_2'], 'field_2': 'value_1'}) 64 | c.save({'field_1': ['value_1','value_2'], 'field_2': 'value_2'}) 65 | c.save({'field_1': ['value_2','value_1'], 'field_2': 'value_2'}) 66 | 67 | var result = c.distinctAndCount(['field_1', 'field_2']) 68 | 69 | assert.eq(1, result['value_1,value_2,value_1']) 70 | assert.eq(2, result['value_1,value_2,value_2']) 71 | }) 72 | 73 | assert.that('distinctAndCount works when distinct field is null', function(c) { 74 | c.save({field: 'value_1'}) 75 | c.save({field: null}) 76 | c.save({field: undefined}) // undefined is saved as null 77 | c.save({field: 'value_1'}) 78 | 79 | var result = c.distinctAndCount('field') 80 | 81 | assert.eq(2, result['#null#']) 82 | }) 83 | 84 | assert.that('distinctAndCount throws an exception on object fields', function(c) { 85 | c.save({field: 'value_1'}) 86 | c.save({field: {key: 'value_2'}}) 87 | 88 | assert.throws(function() { 89 | c.distinctAndCount('field') 90 | }) 91 | }) 92 | 93 | assert.that('distinctAndCount throws an exception on array fields that contains objects', function(c) { 94 | c.save({field: 'value_1'}) 95 | c.save({field: [{key: 'value_2'}]}) 96 | 97 | assert.throws(function() { 98 | c.distinctAndCount('field') 99 | }) 100 | }) 101 | 102 | assert.that('distinctAndCount works with Number fields', function(c) { 103 | c.save({field: 'value_1'}) 104 | c.save({field: NumberLong(200)}) 105 | 106 | var result = c.distinctAndCount('field') 107 | 108 | assert.eq(1, result['value_1']) 109 | assert.eq(1, result[200]) 110 | }) 111 | 112 | assert.that('distinctAndCount takes a query as second parameter', function(c) { 113 | c.save({field: 'value_1', tag: 1}) 114 | c.save({field: 'value_2', tag: 2}) 115 | c.save({field: 'value_1', tag: 1}) 116 | c.save({field: 'value_2', tag: 1}) 117 | 118 | var result = c.distinctAndCount('field', {tag: 1}) 119 | 120 | assert.eq(2, result['value_1']) 121 | assert.eq(1, result['value_2']) 122 | }) 123 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* jshint camelcase: false */ 2 | 3 | module.exports = function(grunt) { 4 | 5 | var chalk = require('grunt-attention/node_modules/chalk') 6 | 7 | grunt.initConfig({ 8 | install_at: (process.env.HOME || process.evn.HOMEPATH || process.env.USERPROFILE) + '/.mongorc.js', 9 | bower_concat: { 10 | dist: { 11 | dest: '.work/bower_components.js' 12 | } 13 | }, 14 | concat: { 15 | dist: { 16 | src: ['.work/*.js', 'src/**/*.js'], 17 | dest: '.dist/mongorc.js', 18 | options: { 19 | process: function (content, srcpath) { 20 | return content.replace( 21 | '###version###', grunt.file.readJSON('package.json')['version'] 22 | ) 23 | } 24 | } 25 | } 26 | }, 27 | copy: { 28 | release: { 29 | src: '<%= concat.dist.dest %>', 30 | dest: '<%= copy.released.src %>' 31 | }, 32 | builded: { 33 | src: '<%= concat.dist.dest %>', 34 | dest: '<%= install_at %>', 35 | }, 36 | released: { 37 | src: './released/mongorc.js', 38 | dest: '<%= install_at %>', 39 | } 40 | }, 41 | attention: { 42 | installed: { 43 | options: { 44 | borderColor: 'bgGreen', 45 | message: 46 | chalk.green.bold('MongoDB shell extensions installed in your home directory\n') + 47 | chalk.green('(see <%= install_at %>) \n\n') + 48 | chalk.green('Next time you\'ll open your mongo shell you\'ll have all the extensions automatically loaded') 49 | } 50 | } 51 | }, 52 | jshint: { 53 | options: grunt.file.readJSON('.jshintrc'), 54 | all: ['Gruntfile.js', 'spec/**/*.js', 'src/**/*.js'] 55 | }, 56 | release: { 57 | options: { 58 | add: false, 59 | bump: false, 60 | commit: false, 61 | tag: true, 62 | tagName: '<%= version %>', 63 | tagMessage: 'Release <%= version %>', 64 | push: true, 65 | pushTags: true, 66 | npm: true 67 | } 68 | }, 69 | clean: ['.work', '.dist'] 70 | }); 71 | 72 | grunt.loadNpmTasks('grunt-bower-concat') 73 | grunt.loadNpmTasks('grunt-contrib-concat') 74 | grunt.loadNpmTasks('grunt-contrib-clean') 75 | grunt.loadNpmTasks('grunt-contrib-jshint') 76 | grunt.loadNpmTasks('grunt-contrib-copy') 77 | grunt.loadNpmTasks('grunt-attention') 78 | grunt.loadNpmTasks('grunt-release') 79 | 80 | grunt.registerTask('build', ['clean', 'jshint', 'bower_concat', 'concat']) 81 | 82 | grunt.registerTask('install-head', ['build', 'copy:builded', 'attention:installed']) 83 | grunt.registerTask('install-released', ['copy:released', 'attention:installed']) 84 | 85 | grunt.registerTask('spec', ['spec-on-head']) 86 | grunt.registerTask('spec-on-head', ['build', 'run-all-specs:head']) 87 | grunt.registerTask('spec-on-installed', ['run-all-specs:installed']) 88 | 89 | // To do a release you need to: 90 | // * change the version in package.json 91 | // * execute `grunt prepare-release` 92 | // * execute `git add --all && git commit -m "Release "` 93 | // * execute `grunt release-and-publish` 94 | grunt.registerTask('prepare-release', ['build', 'copy:release']) 95 | grunt.registerTask('release-and-publish', ['release']) 96 | 97 | grunt.registerTask('default', ['spec']) 98 | 99 | // !!! I need to automate this, but it's not easy 100 | // How to run tests on a different MongoDB 101 | // * start mongod on a different port with a command like 102 | // `~/opt/mongodb-2.2.7/bin/mongod --port 3100 103 | // --dbpath .tmp/db --logpath .tmp/log --fork 104 | // --quiet --nojournal --noprealloc --smallfiles 105 | // ` 106 | // * change the definition 'spec-on-head' spec to `['build', 'run-all-specs:head:3100']` 107 | // * [optional] to use the shell shipped with the server, change the spawn to use the full path of shell executable 108 | // * run `grunt spec` 109 | 110 | grunt.registerTask('run-all-specs', 'Run all specs in MongoDB Shell', function(onWhat, onPort) { 111 | var done = this.async(), 112 | path = require('path'), 113 | spawn = require('child_process').spawn, 114 | fileToLoad = (onWhat || 'head') === 'head' ? 115 | path.join(__dirname, './.dist/mongorc.js') : 116 | grunt.config('install_at'), 117 | portToConnectTo = (onPort ? onPort : '27017'), 118 | commandArguments = ['--quiet', '--port', portToConnectTo, fileToLoad, '_runner.js'], 119 | runner = spawn('mongo', commandArguments, {cwd: path.join(__dirname, 'spec')}) 120 | 121 | runner.stdout.on('data', function(data) { 122 | grunt.log.write(data.toString()) 123 | }) 124 | runner.stderr.on('data', function(data) { 125 | grunt.log.error(data.toString()) 126 | }) 127 | runner.on('close', function(code) { 128 | if (code !== 0) { 129 | grunt.util.exit(code) 130 | } 131 | done() 132 | }) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /src/jsonpath.js: -------------------------------------------------------------------------------- 1 | DBQuery.prototype.select = function(expression) { 2 | if (_.isString(expression)) { 3 | expression = [].concat(expression) 4 | } 5 | if (_.isArray(expression)) { 6 | expression = _.reduce(expression, function(hash, key) { 7 | hash[key] = key 8 | return hash 9 | }, {}) 10 | } 11 | if (_.isObject(expression)) { 12 | var originalNextMethod = this.next 13 | 14 | this.next = function() { 15 | var doc = originalNextMethod.call(this) 16 | return _.reduce(expression, function(selected, fieldSelector, fieldName) { 17 | var fieldValue = jsonpath(doc, fieldSelector) 18 | if (fieldValue && fieldValue.length === 1) { 19 | fieldValue = fieldValue[0] 20 | } 21 | selected[fieldName] = fieldValue 22 | return selected 23 | }, {}) 24 | } 25 | } 26 | return this 27 | } 28 | 29 | 30 | /* JSONPath 0.8.0 - XPath for JSON 31 | * 32 | * Copyright (c) 2007 Stefan Goessner (goessner.net) 33 | * Licensed under the MIT (MIT-LICENSE.txt) licence. 34 | */ 35 | function jsonpath(obj, expr, arg) { 36 | var P = { 37 | resultType: arg && arg.resultType || 'VALUE', 38 | notFoundValue: arg && (arg.notFoundValue !== undefined) ? arg.notFoundValue : undefined, 39 | result: [], 40 | normalize: function(expr) { 41 | var subx = []; 42 | return expr.replace(/[\['](\??\(.*?\))[\]']/g, function($0,$1){return '[#'+(subx.push($1)-1)+']';}) 43 | .replace(/'?\.'?|\['?/g, ';') 44 | .replace(/;;;|;;/g, ';..;') 45 | .replace(/;$|'?\]|'$/g, '') 46 | .replace(/#([0-9]+)/g, function($0,$1){return subx[$1];}); 47 | }, 48 | asPath: function(path) { 49 | var x = path.split(';'), p = '$'; 50 | for (var i=1,n=x.length; i db.visits.findOne() 10 | { 11 | "_id" : "a0039342e1cda7446cbb55aac2108491-20140306", 12 | "at" : ISODate("2014-03-06T11:04:59.524Z"), 13 | "digest" : "a0039342e1cda7446cbb55aac2108491", 14 | "duration" : 150, 15 | "hits" : 5, 16 | "url" : "http://roob.biz/pearline" 17 | } 18 | ``` 19 | You need to find how many visits there have been in the last 10 day... You know that dealing with dates is a mess, unless you have loaded the mighty `MongoDB Shell Extensions` in that case your life would be much, much easier 20 | ``` 21 | > moment.last(10).days().forEach('day', function(m) { 22 | > print(m.format('YYYYDDMM') + ': ' + db.visits.count({at: moment.$inDay(m)})) 23 | > }) 24 | 25 | 20140224: 153 26 | 20140225: 228 27 | 20140226: 228 28 | 20140227: 209 29 | 20140228: 246 30 | 20140301: 247 31 | 20140302: 243 32 | 20140303: 240 33 | 20140304: 208 34 | 20140305: 139 35 | 20140306: 204 36 | ``` 37 | You will have helpful output 38 | ``` 39 | > moment.last(10) 40 | 10 of what? 41 | > moment.last(10).days() 42 | "2014-02-24T11:36:50.509Z/2014-03-06T11:36:50.509Z" 43 | ``` 44 | You will have various helpful methods to reduce query verbosity 45 | ``` 46 | // Suppose we have a day d 47 | > d 48 | ISODate("2014-03-06T11:49:12.383Z") 49 | > startOfDay = ISODate(d.toISOString()) 50 | > startOfDay.setUTCHours(0) 51 | > startOfDay.setUTCMinutes(0) 52 | > startOfDay.setUTCSeconds(0) 53 | > startOfDay.setUTCMicroseconds(0) 54 | > endOfDay = ISODate(d.toISOString()) 55 | > endOfDay.setUTCHours(23) 56 | > endOfDay.setUTCMinutes(59) 57 | > endOfDay.setUTCSeconds(59) 58 | > endOfDay.setUTCMilliseconds(999) 59 | > db.visits.count({at: {$gte: startOfDay, $lte: endOfDay}}) 60 | 204 61 | 62 | // YUCK! Can we do better? 63 | // Yes, using dates manipulation functions 64 | > db.visits.count({at: { 65 | > $gte: moment(d).startOf('day').toDate(), 66 | > $lte: moment(d).endOf('day').toDate()} 67 | > }) 68 | 204 69 | 70 | // Can we do better? 71 | // Yes, using moment.$between to generate $gte and $lte range 72 | > db.visits.count({ 73 | > at: moment.$between( 74 | > moment(d).startOf('day'), 75 | > moment(d).endOf('day')) 76 | > } 77 | > ) 78 | 204 79 | 80 | // Can we do better? 81 | // Yes, using moment.$inDay to use moment.$between and call startOf('day') and endOf('day') 82 | > db.visits.count({at: moment.$inDay(d)}) 83 | 204 84 | 85 | // WOW! That's what I call an improvement! 86 | ``` 87 | Be mind, we only have scratched the surface of what we can do 88 | 89 | # Supported MongoDB Versions 90 | * `2.2.X` 91 | * `2.4.X` 92 | * `2.6.X` 93 | * `3.0.X` 94 | 95 | # How to Install 96 | Download `mongorc.js` from the latest [release](https://raw.github.com/gabrielelana/mongodb-shell-extensions/master/released/mongorc.js) and copy it into your home directory as `.mongorc.js` 97 | ``` 98 | curl -sL https://raw.github.com/gabrielelana/mongodb-shell-extensions/master/released/mongorc.js > ~/.mongorc.js 99 | ``` 100 | Or if you want you can install it using npm (N.B. This is going to install a bunch of dependencies, if you care about your disk space then prefer the first option) 101 | ``` 102 | npm install --global mongodb-shell-extensions 103 | ``` 104 | 105 | Now you have a `.mongorc` file in your home directory that contains all the extensions. This file will be loaded automatically in the next MongoDB shell session 106 | 107 | The next time you'll start a MongoDB shell you should see a message like this (the message will not be displayed if the shell is in quiet mode `mongo --quiet`) 108 | ``` 109 | $ mongo 110 | MongoDB shell version: 2.4.8 111 | connecting to: test 112 | + MongoDB Shell Extensions by Gabriele Lana 113 | > 114 | ``` 115 | 116 | # How to Temporary Disable 117 | If you want to temporary disable the extensions you can start the MongoDB shell with the `--norc` flag 118 | ``` 119 | $ mongo --norc 120 | MongoDB shell version: 2.4.8 121 | connecting to: test 122 | > 123 | ``` 124 | 125 | # How to Uninstall 126 | Remove `.mongorc` from your home directory 127 | ``` 128 | $ rm ~/.mongorc.js 129 | ``` 130 | 131 | 132 | # Thanks To 133 | This is really a bunch of wonderful open source projects put together with a little glue, so, many thanks to: 134 | * [MomentJS](http://momentjs.com/) with [DateRange](https://github.com/gf3/moment-range) plugin 135 | * [LoDash](http://lodash.com/) 136 | * [JSONPath](http://code.google.com/p/jsonpath/) 137 | * [sprintf.js](https://github.com/alexei/sprintf.js) 138 | 139 | # Documentation 140 | Sorry, this is a work in progress, in the meantime, if you don't find what you are looking for _"look at the source Luke"_ or drop me an email :wink: 141 | * [`command pretty`](#Command-Pretty) - switch shell to pretty printing mode 142 | * [`command ugly`](#Command-Ugly) - switch shell off from pretty printing mode 143 | * [`command d|dbs|databases`](#Command-ListDatabases) - list all databases and storage data 144 | * [`command c|colls|collections`](#Command-ListCollections) - list all collections in the current db 145 | * [`command so`](#Command-SlaveOk) - alias for `rs.slaveOk()` 146 | * [`DB#getCollections()`](#DB-getCollections) - get all collections in current db 147 | * [`Collection#distinctAndCount()`](#Collection-distinctAndCount) - count how many distinct values 148 | * [`Collection#first()`](#Collection-first) - first element inserted in collection 149 | * [`Collection#last()`](#Collection-last) - last element inserted in collection 150 | * [`Query#first()`](#Query-first) - return only the first element in result 151 | * [`Query#last()`](#Query-last) - return only the last element in result 152 | * [`Query#reverse()`](#Query-reverse) - reverse query sort order 153 | * [`Query#tojson()`](#Query-tojson) - serialize query result using json format 154 | * [`CSV Support`](#CSV) 155 | * [`tocsv(x)`](#tocsv) - serialize `x` using csv format 156 | * [`printcsv(x)`](#printcsv) - print `x` using csv format 157 | * [`Query#tocsv()`](#Query-tocsv) - serialize query result using csv format 158 | * [`Query#printcsv()`](#Query-printcsv) - print query result using csv format 159 | * [`JSONPath Support`](#JSONPath) 160 | * [`Query#select(x)`](#Query-select) - filter result using jsonpath expression `x` 161 | * [`jsonpath(o, x)`](#jsonpath) - filter object `o` using jsonpath expression `x` 162 | * [`Temporary Collections`](#TemporaryCollections) 163 | * [`DB#collection(f)`](#DB-collection) - creates a temporary collection that will be available to `f` and automatically destroyed immediately after 164 | * [`DB#getTemporaryCollections()`](#DB-getTemporaryCollections) - get all temporary collections 165 | * [`DB#dropTempraryCollections()`](#DB-dropTempraryCollections) - drop all temporary collections 166 | * [`Collection#isTemporary`](#Collection-isTemporary) - returns true if the collection is temporary 167 | * [`Storable Cursors`](#StorableCursors) 168 | * [`Query#save(c)`](#Query-save) - save the query result into a collection `c` 169 | * [`LoDash Integration`](#LoDash) 170 | * [`MomentJS Integration`](#MomentJS) 171 | 172 | 173 | 174 | ### `command pretty` 175 | 176 | Switch shell to pretty printing mode. Everything that could be pretty printed it will be automatically without asking for it 177 | ``` 178 | > db.users.first() 179 | { "_id" : ObjectId("53e0f55eca4f6f6589000001"), "name" : "Mervin", "surname" : "Witting", "job" : "Journalist" } 180 | > db.users.first().pretty() 181 | { 182 | "_id" : ObjectId("53e0f55eca4f6f6589000001"), 183 | "name" : "Mervin", 184 | "surname" : "Witting", 185 | "job" : "Journalist" 186 | } 187 | > pretty 188 | pretty printing: enabled 189 | > db.users.first() 190 | { 191 | "_id" : ObjectId("53e0f55eca4f6f6589000001"), 192 | "name" : "Mervin", 193 | "surname" : "Witting", 194 | "job" : "Journalist" 195 | } 196 | ``` 197 | 198 | 199 | 200 | ### `command ugly` 201 | 202 | Switch shell off from pretty printing mode 203 | ``` 204 | > db.users.first() 205 | { 206 | "_id" : ObjectId("53e0f55eca4f6f6589000001"), 207 | "name" : "Mervin", 208 | "surname" : "Witting", 209 | "job" : "Journalist" 210 | } 211 | > ugly 212 | pretty printing: disabled 213 | > db.users.first() 214 | { "_id" : ObjectId("53e0f55eca4f6f6589000001"), "name" : "Mervin", "surname" : "Witting", "job" : "Journalist" } 215 | ``` 216 | 217 | 218 | 219 | ### `command d|dbs|databases` 220 | 221 | List all databases and storage data. The format is NUMBER_OF_COLLECTIONS/SIZE_ON_DISK 222 | ``` 223 | > d 224 | recruiter 4/208MB 225 | playground 2/208MB 226 | waitress-test 3/208MB 227 | mongoose-trackable-test 2/208MB 228 | mongoose-eventful-test 6/80MB 229 | hangman 2/208MB 230 | ``` 231 | 232 | 233 | 234 | ### `command d|dbs|databases` 235 | 236 | List all collections and storage data. The format is NUMBER_OF_DOCUMENTS/SIZE_ON_DISK 237 | ``` 238 | > c 239 | archived 1/32KB 240 | roster 1/16KB 241 | scheduled 0/32KB 242 | system.indexes 3/4KB 243 | ``` 244 | 245 | 246 | 247 | ### `command so` 248 | 249 | Alias for `rs.slaveOk()` nothing fancy, I was just tired of typing it 250 | 251 | 252 | 253 | ### `DB#getCollections()` 254 | 255 | Returns an array of all collection instances 256 | ``` 257 | > db.getCollections().map(function(c) {return c.count()}) 258 | [ 5793, 4, 1003, 4373 ] 259 | ``` 260 | 261 | 262 | 263 | ### `Collection#distinctAndCount(field, query)` 264 | 265 | For each distinct value of `field` counts the occurrences in documents optionally filtered by `query` 266 | ``` 267 | > db.users.distinctAndCount('name', {name: /^a/i}) 268 | { 269 | "Abagail": 1, 270 | "Abbey": 3, 271 | "Abbie": 1, 272 | "Abdiel": 2, 273 | "Abdullah": 1, 274 | "Adah": 1, 275 | "Adalberto": 5, 276 | "Adela": 1, 277 | ... 278 | } 279 | ``` 280 | The `field` parameter could be an array of fields 281 | ``` 282 | > db.users.distinctAndCount(['name','job'], {name: /^a/i}) 283 | { 284 | "Austin,Educator" : 1, 285 | "Aurelia,Educator" : 1, 286 | "Augustine,Carpenter" : 1, 287 | "Augusta,Carpenter" : 2, 288 | "Audreanne,Zoologist" : 1, 289 | "Audreanne,Farmer" : 1, 290 | "Aubree,Lawyer" : 1, 291 | ... 292 | } 293 | ``` 294 | 295 | 296 | 297 | ### `Collection#first(n)` 298 | 299 | Returns the first `n` (ordered by `_id`) elements inserted in the collection 300 | ``` 301 | > db.users.first().length() 302 | 1 303 | > db.users.first(3).length() 304 | 3 305 | ``` 306 | 307 | 308 | 309 | ### `Collection#last(n)` 310 | 311 | Returns the last `n` (ordered by `_id`) elements inserted in the collection 312 | ``` 313 | > db.users.save({name: "Gabriele", surname: "Lana", job: "Software Craftsman"}) 314 | > db.users.last().pretty() 315 | { 316 | "_id" : ObjectId("531879529c812de54e6711e1"), 317 | "name" : "Gabriele", 318 | "surname" : "Lana", 319 | "job" : "Software Craftsman" 320 | } 321 | ``` 322 | 323 | 324 | 325 | ### `Query#first()` 326 | 327 | Same as [`Collection#first()`](#Collection-first) 328 | 329 | 330 | 331 | ### `Query#last()` 332 | 333 | Same as [`Collection#last()`](#Collection-last) 334 | 335 | 336 | 337 | ### `Query#reverse()` 338 | Reverse the order of the cursor 339 | ``` 340 | > db.users.first()._id === db.users.find().reverse().last()._id 341 | true 342 | ``` 343 | 344 | 345 | 346 | ### `Query#tojson()` 347 | 348 | 349 | 350 | 351 | 352 | ## `CSV` 353 | 354 | 355 | 356 | ### `tocsv(x)` 357 | 358 | Returns a `CSV` instance which is a collections of lines. The first line is the CSV header with the union of all the fields found in all the documents. The other lines are the CSV representation of the documents, one document per line. `CSV` inherits most of the collection methods implemented in `LoDash` 359 | ``` 360 | > tocsv(db.users.find({name: /^a/i})) 361 | _id,name,surname,job 362 | "5318565aca4f6f419b00001e","Abagail","Crona","Zoologist" 363 | "5318565aca4f6f419b000007","Abbey","Tromp","Writer" 364 | "5318565aca4f6f419b0000da","Abbie","Wilkinson","Carpenter" 365 | "5318565aca4f6f419b00007a","Abdiel","Schuster","Educator" 366 | "5318565aca4f6f419b0002d1","Abdiel","Schneider","Librarian" 367 | "5318565aca4f6f419b00030d","Abdullah","Baumbach","Librarian" 368 | "5318565aca4f6f419b0000dc","Adah","Lind","Dancer" 369 | "5318565aca4f6f419b0002ed","Adalberto","Reynolds","Librarian" 370 | "5318565aca4f6f419b000066","Adela","Keebler","Educator" 371 | "5318565aca4f6f419b0002d2","Adolf","Boyer","Farmer" 372 | ... 373 | ``` 374 | This is the same result of `printcsv` but don't be fooled, the shell calls `shellPrint()` method on every object that needs to be displayed by the shell itself, `shellPrint()` will print the `CSV` instance exactly as `printjson` would but you can do other things beside printing it 375 | ``` 376 | > tocsv(db.users.find({name: /^a/i})).head() 377 | _id,name,surname,job 378 | // sample will return a random line 379 | > tocsv(db.users.find({name: /^a/i})).sample() 380 | "5318565aca4f6f419b000098","Arlo","Huels","Lawyer" 381 | ``` 382 | It will work with everything has a `map` method 383 | ``` 384 | > tocsv([{name: "Gabriele", surname: "Lana"}]) 385 | name,surname 386 | "Gabriele","Lana" 387 | ``` 388 | 389 | 390 | 391 | ### `printcsv(x)` 392 | 393 | It will print each line of the CSV returned by `tocsv()`. This is useful when you want to export redirecting the output. Unfortunately `--eval` option will be evaluated **before** any script so it cannot be used to execute something defined in your `~/.mongorc.js` 394 | ``` 395 | $ cat > exportUsersToCSV.js <