From e12518ab8738ffb275b985530a95ab4bf5a0978a Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Thu, 27 Dec 2018 14:11:23 -0800 Subject: [PATCH 1/7] promise support --- lib/collection.js | 269 ++++++++++++++++--------- lib/cursor.js | 120 ++++++----- test/tape.js | 7 +- test/test-bulk-replace-one-promise.js | 30 +++ test/test-find-by-objectid-promise.js | 13 ++ test/test-find-one-promise.js | 12 ++ test/test-find-promise.js | 10 + test/test-insert-promise.js | 24 +++ test/test-optional-callback-promise.js | 12 ++ test/test-save-promise.js | 18 ++ test/test-update-promise.js | 13 ++ 11 files changed, 388 insertions(+), 140 deletions(-) create mode 100644 test/test-bulk-replace-one-promise.js create mode 100644 test/test-find-by-objectid-promise.js create mode 100644 test/test-find-one-promise.js create mode 100644 test/test-find-promise.js create mode 100644 test/test-insert-promise.js create mode 100644 test/test-optional-callback-promise.js create mode 100644 test/test-save-promise.js create mode 100644 test/test-update-promise.js diff --git a/lib/collection.js b/lib/collection.js index 6efd923..9bce097 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -4,10 +4,24 @@ var xtend = require('xtend') var Cursor = require('./cursor') var Bulk = require('./bulk') // TODO: Make this configurable by users -var writeOpts = {writeConcern: {w: 1}, ordered: true} -var noop = function () {} +var writeOpts = { writeConcern: { w: 1 }, ordered: true } +var noop = function () { } var oid = mongodb.ObjectID.createPk +const _wrap = function (fn, cb) { + if (cb === noop || !cb) { + return new Promise(function (resolve, reject) { + let cbp = function (err, result) { + if (err) reject(err) + resolve(result) + } + fn(cbp) + }) + } else { + return fn(cb) + } +} + var Collection = function (opts, getConnection) { this._name = opts.name this._getConnection = getConnection @@ -15,7 +29,9 @@ var Collection = function (opts, getConnection) { var collectionName = this._name this._getConnection(function (err, connection) { - if (err) { return cb(err) } + if (err) { + return cb(err) + } cb(null, connection.collection(collectionName)) }) @@ -24,18 +40,20 @@ var Collection = function (opts, getConnection) { Collection.prototype.find = function (query, projection, opts, cb) { if (typeof query === 'function') return this.find({}, null, null, query) - if (typeof projection === 'function') return this.find(query, null, null, projection) - if (typeof opts === 'function') return this.find(query, projection, null, opts) + if (typeof projection === 'function') { return this.find(query, null, null, projection) } + if (typeof opts === 'function') { return this.find(query, projection, null, opts) } var self = this function getCursor (cb) { self._getCollection(function (err, collection) { - if (err) { return cb(err) } + if (err) { + return cb(err) + } // projection is now an option on find if (projection) { if (opts) opts.projection = projection - else opts = {projection: projection} + else opts = { projection: projection } } cb(null, collection.find(query, opts)) }) @@ -43,23 +61,28 @@ Collection.prototype.find = function (query, projection, opts, cb) { var cursor = new Cursor(getCursor) - if (cb) return cursor.toArray(cb) + if (cb) { + return cursor.toArray(cb) + } return cursor } Collection.prototype.findOne = function (query, projection, cb) { if (typeof query === 'function') return this.findOne({}, null, query) - if (typeof projection === 'function') return this.findOne(query, null, projection) - this.find(query, projection).next(function (err, doc) { - if (err) return cb(err) - cb(null, doc) - }) + if (typeof projection === 'function') { return this.findOne(query, null, projection) } + return _wrap((cb) => { + var cursor = this.find(query, projection).next(function (err, doc) { + if (err) return cb(err) + cb(null, doc) + }) + return cursor + }, cb) } Collection.prototype.findAndModify = function (opts, cb) { - this.runCommand('findAndModify', opts, function (err, result) { + return this.runCommand('findAndModify', opts, function (err, result) { if (err) return cb(err) - cb(null, result.value, result.lastErrorObject || {n: 0}) + cb(null, result.value, result.lastErrorObject || { n: 0 }) }) } @@ -69,7 +92,10 @@ Collection.prototype.count = function (query, cb) { } Collection.prototype.distinct = function (field, query, cb) { - this.runCommand('distinct', {key: field, query: query}, function (err, result) { + return this.runCommand('distinct', { key: field, query: query }, function ( + err, + result + ) { if (err) return cb(err) cb(null, result.values) }) @@ -79,22 +105,23 @@ Collection.prototype.insert = function (docOrDocs, opts, cb) { if (!opts && !cb) return this.insert(docOrDocs, {}, noop) if (typeof opts === 'function') return this.insert(docOrDocs, {}, opts) if (opts && !cb) return this.insert(docOrDocs, opts, noop) + return _wrap(cb => { + this._getCollection(function (err, collection) { + if (err) return cb(err) - this._getCollection(function (err, collection) { - if (err) return cb(err) - - var docs = Array.isArray(docOrDocs) ? docOrDocs : [docOrDocs] - for (var i = 0; i < docs.length; i++) { - if (!docs[i]._id) docs[i]._id = oid() - } + var docs = Array.isArray(docOrDocs) ? docOrDocs : [docOrDocs] + for (var i = 0; i < docs.length; i++) { + if (!docs[i]._id) docs[i]._id = oid() + } - collection.insert(docs, xtend(writeOpts, opts), function (err) { - if (err) return cb(err) - // TODO: Add a test for this - is this really not needed anymore? - // if (res && res.result && res.result.writeErrors && res.result.writeErrors.length > 0) return cb(res.result.writeErrors[0]) - cb(null, docOrDocs) + collection.insert(docs, xtend(writeOpts, opts), function (err) { + if (err) return cb(err) + // TODO: Add a test for this - is this really not needed anymore? + // if (res && res.result && res.result.writeErrors && res.result.writeErrors.length > 0) return cb(res.result.writeErrors[0]) + cb(null, docOrDocs) + }) }) - }) + }, cb) } Collection.prototype.update = function (query, update, opts, cb) { @@ -102,14 +129,23 @@ Collection.prototype.update = function (query, update, opts, cb) { if (typeof opts === 'function') return this.update(query, update, {}, opts) cb = cb || noop - this._getCollection(function (err, collection) { - if (err) return cb(err) + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) return cb(err) - collection.update(query, update, xtend(writeOpts, opts), function (err, result) { - if (err) { return cb(err) } - cb(null, result.result) + collection.update(query, update, xtend(writeOpts, opts), function ( + err, + result + ) { + if (err) { + return cb(err) + } + cb(null, result.result) + }) }) - }) + }, + cb + ) } Collection.prototype.save = function (doc, opts, cb) { @@ -118,33 +154,45 @@ Collection.prototype.save = function (doc, opts, cb) { if (!cb) return this.save(doc, opts, noop) if (doc._id) { - this.update({_id: doc._id}, doc, xtend({upsert: true}, opts), function (err) { - if (err) return cb(err) - cb(null, doc) - }) + return _wrap((cb) => { + return this.update( + { _id: doc._id }, + doc, + xtend({ upsert: true }, opts), + function (err) { + if (err) return cb(err) + cb(null, doc) + } + ) + }, cb) } else { - this.insert(doc, opts, cb) + return this.insert(doc, opts, cb) } } Collection.prototype.remove = function (query, opts, cb) { - if (typeof query === 'function') return this.remove({}, {justOne: false}, query) - if (typeof opts === 'function') return this.remove(query, {justOne: false}, opts) - if (typeof opts === 'boolean') return this.remove(query, {justOne: opts}, cb) - if (!opts) return this.remove(query, {justOne: false}, cb) + if (typeof query === 'function') { return this.remove({}, { justOne: false }, query) } + if (typeof opts === 'function') { return this.remove(query, { justOne: false }, opts) } + if (typeof opts === 'boolean') { return this.remove(query, { justOne: opts }, cb) } + if (!opts) return this.remove(query, { justOne: false }, cb) if (!cb) return this.remove(query, opts, noop) - this._getCollection(function (err, collection) { - if (err) return cb(err) + return _wrap(cb => { + this._getCollection(function (err, collection) { + if (err) return cb(err) - var deleteOperation = opts.justOne ? 'deleteOne' : 'deleteMany' + var deleteOperation = opts.justOne ? 'deleteOne' : 'deleteMany' - collection[deleteOperation](query, xtend(writeOpts, opts), function (err, result) { - if (err) return cb(err) - result.result.deletedCount = result.deletedCount - cb(null, result.result) + collection[deleteOperation](query, xtend(writeOpts, opts), function ( + err, + result + ) { + if (err) return cb(err) + result.result.deletedCount = result.deletedCount + cb(null, result.result) + }) }) - }) + }, cb) } Collection.prototype.rename = function (name, opts, cb) { @@ -152,29 +200,36 @@ Collection.prototype.rename = function (name, opts, cb) { if (!opts) return this.rename(name, {}, noop) if (!cb) return this.rename(name, noop) - this._getCollection(function (err, collection) { - if (err) return cb(err) - collection.rename(name, opts, cb) - }) + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) return cb(err) + collection.rename(name, opts, cb) + }) + }, cb) } Collection.prototype.drop = function (cb) { - this.runCommand('drop', cb) + return this.runCommand('drop', cb) } Collection.prototype.stats = function (cb) { - this.runCommand('collStats', cb) + return this.runCommand('collStats', cb) } Collection.prototype.mapReduce = function (map, reduce, opts, cb) { - if (typeof opts === 'function') { return this.mapReduce(map, reduce, {}, opts) } - if (!cb) { return this.mapReduce(map, reduce, opts, noop) } - - this._getCollection(function (err, collection) { - if (err) return cb(err) + if (typeof opts === 'function') { + return this.mapReduce(map, reduce, {}, opts) + } + if (!cb) { + return this.mapReduce(map, reduce, opts, noop) + } + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) return cb(err) - collection.mapReduce(map, reduce, opts, cb) - }) + collection.mapReduce(map, reduce, opts, cb) + }) + }, cb) } Collection.prototype.runCommand = function (cmd, opts, cb) { @@ -186,10 +241,12 @@ Collection.prototype.runCommand = function (cmd, opts, cb) { Object.keys(opts).forEach(function (key) { cmdObject[key] = opts[key] }) - this._getConnection(function (err, connection) { - if (err) return cb(err) - connection.command(cmdObject, cb) - }) + return _wrap((cb) => { + this._getConnection(function (err, connection) { + if (err) return cb(err) + connection.command(cmdObject, cb) + }) + }, cb) } Collection.prototype.toString = function () { @@ -197,11 +254,11 @@ Collection.prototype.toString = function () { } Collection.prototype.dropIndexes = function (cb) { - this.runCommand('dropIndexes', {index: '*'}, cb) + return this.runCommand('dropIndexes', { index: '*' }, cb) } Collection.prototype.dropIndex = function (index, cb) { - this.runCommand('dropIndexes', {index: index}, cb) + return this.runCommand('dropIndexes', { index: index }, cb) } Collection.prototype.createIndex = function (index, opts, cb) { @@ -209,11 +266,13 @@ Collection.prototype.createIndex = function (index, opts, cb) { if (!opts) return this.createIndex(index, {}, noop) if (!cb) return this.createIndex(index, opts, noop) - this._getCollection(function (err, collection) { - if (err) return cb(err) + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) return cb(err) - collection.createIndex(index, opts, cb) - }) + collection.createIndex(index, opts, cb) + }) + }, cb) } Collection.prototype.ensureIndex = function (index, opts, cb) { @@ -221,38 +280,57 @@ Collection.prototype.ensureIndex = function (index, opts, cb) { if (!opts) return this.ensureIndex(index, {}, noop) if (!cb) return this.ensureIndex(index, opts, noop) - this._getCollection(function (err, collection) { - if (err) return cb(err) - - collection.ensureIndex(index, opts, cb) - }) + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) return cb(err) + console.log(index, opts, cb) + collection.ensureIndex(index, opts, cb) + }) + }, cb) } Collection.prototype.getIndexes = function (cb) { - this._getCollection(function (err, collection) { - if (err) { return cb(err) } + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) { + return cb(err) + } - collection.indexes(cb) - }) + collection.indexes(cb) + }) + }, cb) } Collection.prototype.reIndex = function (cb) { - this.runCommand('reIndex', cb) + return this.runCommand('reIndex', cb) } Collection.prototype.isCapped = function (cb) { - this._getCollection(function (err, collection) { - if (err) { return cb(err) } + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) { + return cb(err) + } - collection.isCapped(cb) - }) + collection.isCapped(cb) + }) + }, cb) } Collection.prototype.group = function (doc, cb) { - this._getCollection(function (err, collection) { - if (err) return cb(err) - collection.group(doc.key || doc.keyf, doc.cond, doc.initial, doc.reduce, doc.finalize, cb) - }) + return _wrap((cb) => { + this._getCollection(function (err, collection) { + if (err) return cb(err) + collection.group( + doc.key || doc.keyf, + doc.cond, + doc.initial, + doc.reduce, + doc.finalize, + cb + ) + }) + }, cb) } Collection.prototype.aggregate = function () { @@ -264,7 +342,10 @@ Collection.prototype.aggregate = function () { cb = once(pipeline.pop()) } - if ((pipeline.length === 1 || pipeline.length === 2) && Array.isArray(pipeline[0])) { + if ( + (pipeline.length === 1 || pipeline.length === 2) && + Array.isArray(pipeline[0]) + ) { opts = pipeline[1] pipeline = pipeline[0] } diff --git a/lib/cursor.js b/lib/cursor.js index d30609d..703eab7 100644 --- a/lib/cursor.js +++ b/lib/cursor.js @@ -1,6 +1,7 @@ var util = require('util') var thunky = require('thunky') var Readable = require('readable-stream').Readable +var noop = function () {} try { var hooks = require('async_hooks') @@ -38,18 +39,23 @@ Cursor.prototype.next = function (cb) { var self = this - this._get(function (err, cursor) { - if (err) return cb(err) - - if (cursor.cursorState.dead || cursor.cursorState.killed) { - destroy(self) - return cb(null, null) - } else { - cursor.next(cb) - } - }) + let maybePromise = _wrap((cb) => { + this._get(function (err, cursor) { + if (err) return cb(err) - return this + if (cursor.cursorState.dead || cursor.cursorState.killed) { + destroy(self) + return cb(null, null) + } else { + cursor.next(cb) + } + }) + }, cb) + if (cb) { + return this + } else { + return maybePromise // it is in fact a promise here. + } } Cursor.prototype.rewind = function (cb) { @@ -63,40 +69,57 @@ Cursor.prototype.rewind = function (cb) { return this } +const _wrap = function (fn, cb) { + if (cb === noop || !cb) { + return new Promise(function (resolve, reject) { + let cbp = function (err, result) { + if (err) reject(err) + resolve(result) + } + fn(cbp) + }) + } else { + return fn(cb) + } +} + Cursor.prototype.toArray = function (cb) { var array = [] var self = this - var loop = function () { - self.next(function (err, obj) { - if (err) return cb(err) - if (!obj) return cb(null, array) - array.push(obj) + return _wrap((cb) => { + var loop = function () { + self.next(function (err, obj) { + if (err) return cb(err) + if (!obj) return cb(null, array) + array.push(obj) - // Fix for #270 RangeError: Maximum call stack size exceeded using Collection.find - setImmediate(loop) - }) - } + // Fix for #270 RangeError: Maximum call stack size exceeded using Collection.find + setImmediate(loop) + }) + } - loop() + loop() + }, cb) } Cursor.prototype.map = function (mapfn, cb) { var array = [] var self = this - - var loop = function () { - self.next(function (err, obj) { - if (err) return cb(err) - if (!obj) return cb(null, array) - array.push(mapfn(obj)) + return _wrap((cb) => { + var loop = function () { + self.next(function (err, obj) { + if (err) return cb(err) + if (!obj) return cb(null, array) + array.push(mapfn(obj)) // Fix for #270 RangeError: Maximum call stack size exceeded using Collection.find - setImmediate(loop) - }) - } + setImmediate(loop) + }) + } - loop() + loop() + }, cb) } Cursor.prototype.forEach = function (fn) { @@ -132,35 +155,42 @@ Cursor.prototype.count = function (cb) { if (this._hook) cb = wrapHook(this, cb) var self = this - - this._get(function (err, cursor) { - if (err) { return cb(err) } - cursor.count(false, self.opts, cb) - }) + return _wrap((cb) => { + this._get(function (err, cursor) { + if (err) { return cb(err) } + cursor.count(false, self.opts, cb) + }) + }, cb) } Cursor.prototype.size = function (cb) { if (this._hook) cb = wrapHook(this, cb) var self = this - - this._get(function (err, cursor) { - if (err) { return cb(err) } - cursor.count(true, self.opts, cb) - }) + return _wrap((cb) => { + this._get(function (err, cursor) { + if (err) { return cb(err) } + cursor.count(true, self.opts, cb) + }) + }, cb) } Cursor.prototype.explain = function (cb) { + // TODO: look at what the hook will do with promises, and if that's desired. if (this._hook) cb = wrapHook(this, cb) - this._get(function (err, cursor) { - if (err) { return cb(err) } - cursor.explain(cb) - }) + return _wrap((cb) => { + this._get(function (err, cursor) { + if (err) { return cb(err) } + cursor.explain(cb) + }) + }, cb) } Cursor.prototype.destroy = function () { + // TODO: Promisify var self = this + this._get(function (err, cursor) { if (err) return done(err) if (cursor.close) { diff --git a/test/tape.js b/test/tape.js index 7a12fb5..ee2eb67 100644 --- a/test/tape.js +++ b/test/tape.js @@ -1,5 +1,4 @@ var test = require('tape') - var wait = global.setImmediate || process.nextTick wait(function () { @@ -9,4 +8,10 @@ wait(function () { }) }) +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason) + // Stack Trace + console.log(reason.stack) + process.exit(1) +}) module.exports = test diff --git a/test/test-bulk-replace-one-promise.js b/test/test-bulk-replace-one-promise.js new file mode 100644 index 0000000..b177e57 --- /dev/null +++ b/test/test-bulk-replace-one-promise.js @@ -0,0 +1,30 @@ +var insert = require('./insert') + +insert('bulk replace one', [{ + name: 'Squirtle', type: 'water' +}, { + name: 'Starmie', type: 'water' +}], function (db, t, done) { + db.runCommand('serverStatus', function (err, resp) { + t.error(err) + if (parseFloat(resp.version) < 2.6) return t.end() + + var bulk = db.a.initializeUnorderedBulkOp() + bulk.find({ name: 'Squirtle' }).replaceOne({ name: 'Charmander', type: 'fire' }) + bulk.find({ name: 'Starmie' }).replaceOne({ type: 'fire' }) + + bulk.execute(function (err, res) { + t.error(err) + t.ok(res.ok) + db.a.find(function (err, res) { + t.error(err) + t.equal(res[0].name, 'Charmander') + t.equal(res[1].name, undefined) + + t.equal(res[0].type, 'fire') + t.equal(res[1].type, 'fire') + t.end() + }) + }) + }) +}) diff --git a/test/test-find-by-objectid-promise.js b/test/test-find-by-objectid-promise.js new file mode 100644 index 0000000..41f5d2b --- /dev/null +++ b/test/test-find-by-objectid-promise.js @@ -0,0 +1,13 @@ +var insert = require('./insert') +var mongojs = require('../') + +insert('find by ObjectId', [{ + hello: 'world' +}], async function (db, t, done) { + let docs = await db.a.find({_id: db.ObjectId('abeabeabeabeabeabeabeabe')}, {hello: 1}).toArray() + t.equal(docs.length, 0) + await db.a.save({_id: mongojs.ObjectId('abeabeabeabeabeabeabeabe')}) + docs = await db.a.find({_id: db.ObjectId('abeabeabeabeabeabeabeabe')}, {hello: 1}).toArray() + t.equal(docs.length, 1) + done() +}) diff --git a/test/test-find-one-promise.js b/test/test-find-one-promise.js new file mode 100644 index 0000000..352d996 --- /dev/null +++ b/test/test-find-one-promise.js @@ -0,0 +1,12 @@ +var insert = require('./insert') + +insert('findOne', [{ + hello: 'world1' +}, { + hello: 'world2' +}], async function (db, t, done) { + let doc = await db.a.findOne() + t.equal(typeof doc, 'object') + t.ok(doc.hello === 'world1' || doc.hello === 'world2') + done() +}) diff --git a/test/test-find-promise.js b/test/test-find-promise.js new file mode 100644 index 0000000..57679fd --- /dev/null +++ b/test/test-find-promise.js @@ -0,0 +1,10 @@ +var insert = require('./insert') + +insert('find', [{ + hello: 'world' +}], async function (db, t, done) { + let docs = await db.a.find().toArray() + t.equal(docs.length, 1) + t.equal(docs[0].hello, 'world') + done() +}) diff --git a/test/test-insert-promise.js b/test/test-insert-promise.js new file mode 100644 index 0000000..3edc282 --- /dev/null +++ b/test/test-insert-promise.js @@ -0,0 +1,24 @@ +var test = require('./tape') +var mongojs = require('../index') +var db = mongojs('test', ['a', 'b']) + +test('insert', async function (t) { + let docs = await db.a.insert( + [{ name: 'Squirtle' }, { name: 'Charmander' }, { name: 'Bulbasaur' }]) + t.ok(docs[0]._id) + t.ok(docs[1]._id) + t.ok(docs[2]._id) + + // It should only return one document in the + // callback when one document is passed instead of an array + let doc = await db.a.insert({ name: 'Lapras' }) + t.equal(doc.name, 'Lapras') + + // If you pass a one element array the callback should + // have a one element array + let docs2 = await db.a.insert([{ name: 'Pidgeotto' }]) + t.equal(docs2[0].name, 'Pidgeotto') + t.equal(docs2.length, 1) + await db.a.remove() + db.close(t.end.bind(t)) +}) diff --git a/test/test-optional-callback-promise.js b/test/test-optional-callback-promise.js new file mode 100644 index 0000000..b0fc3fd --- /dev/null +++ b/test/test-optional-callback-promise.js @@ -0,0 +1,12 @@ +var test = require('./tape') +var mongojs = require('../index') +var db = mongojs('test', ['a', 'b']) + +test('optional callback', function (t) { + db.a.ensureIndex({hello: 1}) + setTimeout(function () { + db.a.count(function () { + db.close(t.end.bind(t)) + }) + }, 100) +}) diff --git a/test/test-save-promise.js b/test/test-save-promise.js new file mode 100644 index 0000000..a821ae2 --- /dev/null +++ b/test/test-save-promise.js @@ -0,0 +1,18 @@ +var test = require('./tape') +var mongojs = require('../index') +var db = mongojs('test', ['a', 'b']) + +test('save', async function (t) { + let doc = await db.a.save({hello: 'world'}) + t.equal(doc.hello, 'world') + t.ok(doc._id) + + doc.hello = 'verden' + doc = await db.a.save(doc) + console.log(doc) + t.ok(doc._id) + t.equal(doc.hello, 'verden') + await db.a.remove() + + db.close(t.end.bind(t)) +}) diff --git a/test/test-update-promise.js b/test/test-update-promise.js new file mode 100644 index 0000000..4383f53 --- /dev/null +++ b/test/test-update-promise.js @@ -0,0 +1,13 @@ +var insert = require('./insert') + +insert('update', [{ + hello: 'world' +}], async function (db, t, done) { + let info = await db.a.update({hello: 'world'}, {$set: {hello: 'verden'}}) + + t.equal(info.n, 1) + + let doc = await db.a.findOne() + t.equal(doc.hello, 'verden') + done() +}) From 55fee92648be344794efe880a0b75e63b5c36046 Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Thu, 27 Dec 2018 14:31:43 -0800 Subject: [PATCH 2/7] docs --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 6b6498c..869366d 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,29 @@ Version > 1.0.x is a major rewrite of mongojs. So expect some things not to work * __Removed__ `mongojs.connect` use `mongojs()` directly instead +# Async/Await Support + +To have mongojs behave even more like the official MongoDB repl, rather than use callbacks, you can query or update the database using async/await. This works wherever callbacks can be used. Just omit the callback and use the await keyword. Don't forget to use the "async" keyword on your caller function. + +```js + +(async function(){ // you need an async callee. + + // find everything + var docs = await db.mycollection.find() // docs is an array of all the documents in mycollection + + // find everything, but sort by name + var docs = await db.mycollection.find().sort({name: 1} + // docs is now a sorted array + + // find a document using a native ObjectId + var doc = db.mycollection.findOne({_id: mongojs.ObjectId('523209c4561c640000000001')} + // and so on, so earlier examples. + +})() + +``` + # API This API documentation is a work in progress. From ff47fc1fb6216416401e0f118dcd435189daacd5 Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Thu, 27 Dec 2018 15:04:07 -0800 Subject: [PATCH 3/7] add async/await support to db class --- lib/collection.js | 43 +++++++------------ lib/cursor.js | 31 ++++---------- lib/database.js | 57 +++++++++++++++----------- lib/promisify.js | 15 +++++++ test/test-optional-callback-promise.js | 10 ++--- test/test-optional-callback.js | 2 +- test/test-save-promise.js | 4 +- 7 files changed, 79 insertions(+), 83 deletions(-) create mode 100644 lib/promisify.js diff --git a/lib/collection.js b/lib/collection.js index 9bce097..e55c35f 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -5,22 +5,10 @@ var Cursor = require('./cursor') var Bulk = require('./bulk') // TODO: Make this configurable by users var writeOpts = { writeConcern: { w: 1 }, ordered: true } -var noop = function () { } var oid = mongodb.ObjectID.createPk -const _wrap = function (fn, cb) { - if (cb === noop || !cb) { - return new Promise(function (resolve, reject) { - let cbp = function (err, result) { - if (err) reject(err) - resolve(result) - } - fn(cbp) - }) - } else { - return fn(cb) - } -} +var promisify = require('./promisify') +var noop = promisify.noop var Collection = function (opts, getConnection) { this._name = opts.name @@ -70,7 +58,7 @@ Collection.prototype.find = function (query, projection, opts, cb) { Collection.prototype.findOne = function (query, projection, cb) { if (typeof query === 'function') return this.findOne({}, null, query) if (typeof projection === 'function') { return this.findOne(query, null, projection) } - return _wrap((cb) => { + return promisify((cb) => { var cursor = this.find(query, projection).next(function (err, doc) { if (err) return cb(err) cb(null, doc) @@ -105,7 +93,7 @@ Collection.prototype.insert = function (docOrDocs, opts, cb) { if (!opts && !cb) return this.insert(docOrDocs, {}, noop) if (typeof opts === 'function') return this.insert(docOrDocs, {}, opts) if (opts && !cb) return this.insert(docOrDocs, opts, noop) - return _wrap(cb => { + return promisify(cb => { this._getCollection(function (err, collection) { if (err) return cb(err) @@ -129,7 +117,7 @@ Collection.prototype.update = function (query, update, opts, cb) { if (typeof opts === 'function') return this.update(query, update, {}, opts) cb = cb || noop - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) return cb(err) @@ -154,7 +142,7 @@ Collection.prototype.save = function (doc, opts, cb) { if (!cb) return this.save(doc, opts, noop) if (doc._id) { - return _wrap((cb) => { + return promisify((cb) => { return this.update( { _id: doc._id }, doc, @@ -177,7 +165,7 @@ Collection.prototype.remove = function (query, opts, cb) { if (!opts) return this.remove(query, { justOne: false }, cb) if (!cb) return this.remove(query, opts, noop) - return _wrap(cb => { + return promisify(cb => { this._getCollection(function (err, collection) { if (err) return cb(err) @@ -200,7 +188,7 @@ Collection.prototype.rename = function (name, opts, cb) { if (!opts) return this.rename(name, {}, noop) if (!cb) return this.rename(name, noop) - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) return cb(err) collection.rename(name, opts, cb) @@ -223,7 +211,7 @@ Collection.prototype.mapReduce = function (map, reduce, opts, cb) { if (!cb) { return this.mapReduce(map, reduce, opts, noop) } - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) return cb(err) @@ -241,7 +229,7 @@ Collection.prototype.runCommand = function (cmd, opts, cb) { Object.keys(opts).forEach(function (key) { cmdObject[key] = opts[key] }) - return _wrap((cb) => { + return promisify((cb) => { this._getConnection(function (err, connection) { if (err) return cb(err) connection.command(cmdObject, cb) @@ -266,7 +254,7 @@ Collection.prototype.createIndex = function (index, opts, cb) { if (!opts) return this.createIndex(index, {}, noop) if (!cb) return this.createIndex(index, opts, noop) - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) return cb(err) @@ -280,17 +268,16 @@ Collection.prototype.ensureIndex = function (index, opts, cb) { if (!opts) return this.ensureIndex(index, {}, noop) if (!cb) return this.ensureIndex(index, opts, noop) - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) return cb(err) - console.log(index, opts, cb) collection.ensureIndex(index, opts, cb) }) }, cb) } Collection.prototype.getIndexes = function (cb) { - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) { return cb(err) @@ -306,7 +293,7 @@ Collection.prototype.reIndex = function (cb) { } Collection.prototype.isCapped = function (cb) { - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) { return cb(err) @@ -318,7 +305,7 @@ Collection.prototype.isCapped = function (cb) { } Collection.prototype.group = function (doc, cb) { - return _wrap((cb) => { + return promisify((cb) => { this._getCollection(function (err, collection) { if (err) return cb(err) collection.group( diff --git a/lib/cursor.js b/lib/cursor.js index 703eab7..197488b 100644 --- a/lib/cursor.js +++ b/lib/cursor.js @@ -1,7 +1,7 @@ var util = require('util') var thunky = require('thunky') var Readable = require('readable-stream').Readable -var noop = function () {} +var promisify = require('./promisify') try { var hooks = require('async_hooks') @@ -39,7 +39,7 @@ Cursor.prototype.next = function (cb) { var self = this - let maybePromise = _wrap((cb) => { + let maybePromise = promisify((cb) => { this._get(function (err, cursor) { if (err) return cb(err) @@ -69,25 +69,11 @@ Cursor.prototype.rewind = function (cb) { return this } -const _wrap = function (fn, cb) { - if (cb === noop || !cb) { - return new Promise(function (resolve, reject) { - let cbp = function (err, result) { - if (err) reject(err) - resolve(result) - } - fn(cbp) - }) - } else { - return fn(cb) - } -} - Cursor.prototype.toArray = function (cb) { var array = [] var self = this - return _wrap((cb) => { + return promisify((cb) => { var loop = function () { self.next(function (err, obj) { if (err) return cb(err) @@ -106,7 +92,7 @@ Cursor.prototype.toArray = function (cb) { Cursor.prototype.map = function (mapfn, cb) { var array = [] var self = this - return _wrap((cb) => { + return promisify((cb) => { var loop = function () { self.next(function (err, obj) { if (err) return cb(err) @@ -155,7 +141,7 @@ Cursor.prototype.count = function (cb) { if (this._hook) cb = wrapHook(this, cb) var self = this - return _wrap((cb) => { + return promisify((cb) => { this._get(function (err, cursor) { if (err) { return cb(err) } cursor.count(false, self.opts, cb) @@ -167,7 +153,7 @@ Cursor.prototype.size = function (cb) { if (this._hook) cb = wrapHook(this, cb) var self = this - return _wrap((cb) => { + return promisify((cb) => { this._get(function (err, cursor) { if (err) { return cb(err) } cursor.count(true, self.opts, cb) @@ -179,7 +165,7 @@ Cursor.prototype.explain = function (cb) { // TODO: look at what the hook will do with promises, and if that's desired. if (this._hook) cb = wrapHook(this, cb) - return _wrap((cb) => { + return promisify((cb) => { this._get(function (err, cursor) { if (err) { return cb(err) } cursor.explain(cb) @@ -225,7 +211,8 @@ function runInAsyncScope (self, cb, err, val) { self._hook.runInAsyncScope(cb, null, err, val) } else { self._hook.emitBefore() - cb(err, val) + // A callback may not exist during async/await usage. + cb && cb(err, val) self._hook.emitAfter() } } diff --git a/lib/database.js b/lib/database.js index 54fa2ab..686ee56 100644 --- a/lib/database.js +++ b/lib/database.js @@ -5,6 +5,7 @@ var thunky = require('thunky') var parse = require('parse-mongo-url') var util = require('util') var EventEmitter = require('events').EventEmitter +var promisify = require('./promisify') var noop = function () {} @@ -81,14 +82,16 @@ Database.prototype.close = function (force, cb) { var self = this cb = cb || noop - this._getConnection(function (err, server, conn) { - if (err) return cb(err) + return promisify((cb) => { + this._getConnection(function (err, server, conn) { + if (err) return cb(err) - conn.close(force) + conn.close(force) - self.emit('close') - cb() - }) + self.emit('close') + cb() + }) + }, cb) } Database.prototype.runCommand = function (opts, cb) { @@ -99,24 +102,28 @@ Database.prototype.runCommand = function (opts, cb) { opts[tmp] = 1 } - this._getConnection(function (err, connection) { - if (err) return cb(err) - connection.command(opts, function (err, result) { + return promisify((cb) => { + this._getConnection(function (err, connection) { if (err) return cb(err) - cb(null, result) + connection.command(opts, function (err, result) { + if (err) return cb(err) + cb(null, result) + }) }) - }) + }, cb) } Database.prototype.listCollections = function (cb) { - this._getConnection(function (err, connection) { - if (err) { return cb(err) } - - connection.listCollections().toArray(function (err, collections) { + return promisify((cb) => { + this._getConnection(function (err, connection) { if (err) { return cb(err) } - cb(null, collections) + + connection.listCollections().toArray(function (err, collections) { + if (err) { return cb(err) } + cb(null, collections) + }) }) - }) + }, cb) } Database.prototype.getCollectionNames = function (cb) { @@ -133,33 +140,33 @@ Database.prototype.createCollection = function (name, opts, cb) { Object.keys(opts).forEach(function (opt) { cmd[opt] = opts[opt] }) - this.runCommand(cmd, cb) + return this.runCommand(cmd, cb) } Database.prototype.stats = function (scale, cb) { if (typeof scale === 'function') return this.stats(1, scale) - this.runCommand({dbStats: 1, scale: scale}, cb) + return this.runCommand({dbStats: 1, scale: scale}, cb) } Database.prototype.dropDatabase = function (cb) { - this.runCommand('dropDatabase', cb) + return this.runCommand('dropDatabase', cb) } Database.prototype.createUser = function (usr, cb) { var cmd = xtend({createUser: usr.user}, usr) delete cmd.user - this.runCommand(cmd, cb) + return this.runCommand(cmd, cb) } Database.prototype.addUser = Database.prototype.createUser Database.prototype.dropUser = function (username, cb) { - this.runCommand({dropUser: username}, cb) + return this.runCommand({dropUser: username}, cb) } Database.prototype.removeUser = Database.prototype.dropUser Database.prototype.eval = function (fn) { var cb = arguments[arguments.length - 1] - this.runCommand({ + return this.runCommand({ eval: fn.toString(), args: Array.prototype.slice.call(arguments, 1, arguments.length - 1) }, function (err, res) { @@ -169,11 +176,11 @@ Database.prototype.eval = function (fn) { } Database.prototype.getLastErrorObj = function (cb) { - this.runCommand('getLastError', cb) + return this.runCommand('getLastError', cb) } Database.prototype.getLastError = function (cb) { - this.runCommand('getLastError', function (err, res) { + return this.runCommand('getLastError', function (err, res) { if (err) return cb(err) cb(null, res.err) }) diff --git a/lib/promisify.js b/lib/promisify.js new file mode 100644 index 0000000..81463ec --- /dev/null +++ b/lib/promisify.js @@ -0,0 +1,15 @@ +module.exports = function (fn, cb) { + if (cb === module.exports.noop || !cb) { + return new Promise(function (resolve, reject) { + let cbp = function (err, result) { + if (err) reject(err) + resolve(result) + } + fn(cbp) + }) + } else { + return fn(cb) + } +} + +module.exports.noop = function () { } diff --git a/test/test-optional-callback-promise.js b/test/test-optional-callback-promise.js index b0fc3fd..e9728e3 100644 --- a/test/test-optional-callback-promise.js +++ b/test/test-optional-callback-promise.js @@ -2,11 +2,11 @@ var test = require('./tape') var mongojs = require('../index') var db = mongojs('test', ['a', 'b']) -test('optional callback', function (t) { +test('optional callback promise', function (t) { db.a.ensureIndex({hello: 1}) - setTimeout(function () { - db.a.count(function () { - db.close(t.end.bind(t)) - }) + setTimeout(async function () { + await db.a.count() + await db.close() + t.end() }, 100) }) diff --git a/test/test-optional-callback.js b/test/test-optional-callback.js index 07a9907..b0fc3fd 100644 --- a/test/test-optional-callback.js +++ b/test/test-optional-callback.js @@ -3,7 +3,7 @@ var mongojs = require('../index') var db = mongojs('test', ['a', 'b']) test('optional callback', function (t) { - db.a.ensureIndex({hello: 'world'}) + db.a.ensureIndex({hello: 1}) setTimeout(function () { db.a.count(function () { db.close(t.end.bind(t)) diff --git a/test/test-save-promise.js b/test/test-save-promise.js index a821ae2..82dbd60 100644 --- a/test/test-save-promise.js +++ b/test/test-save-promise.js @@ -9,10 +9,10 @@ test('save', async function (t) { doc.hello = 'verden' doc = await db.a.save(doc) - console.log(doc) t.ok(doc._id) t.equal(doc.hello, 'verden') await db.a.remove() - db.close(t.end.bind(t)) + await db.close() + t.end() }) From d37cff481147497f271ff8b58e4fde69c5ed9546 Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Thu, 27 Dec 2018 15:21:18 -0800 Subject: [PATCH 4/7] test noop promise cleaned properly --- lib/database.js | 2 +- lib/promisify.js | 6 +++-- test/test-bulk-replace-one-promise.js | 34 +++++++++++++-------------- test/test-find-promise.js | 2 +- test/test-insert-promise.js | 2 +- test/test-noop-promise-cleanup.js | 27 +++++++++++++++++++++ test/test-save-promise.js | 2 +- test/test-update-promise.js | 2 +- 8 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 test/test-noop-promise-cleanup.js diff --git a/lib/database.js b/lib/database.js index 686ee56..676e14f 100644 --- a/lib/database.js +++ b/lib/database.js @@ -7,7 +7,7 @@ var util = require('util') var EventEmitter = require('events').EventEmitter var promisify = require('./promisify') -var noop = function () {} +var noop = promisify.noop var Database = function (connString, cols, options) { var self = this diff --git a/lib/promisify.js b/lib/promisify.js index 81463ec..dc3fad2 100644 --- a/lib/promisify.js +++ b/lib/promisify.js @@ -1,12 +1,14 @@ module.exports = function (fn, cb) { if (cb === module.exports.noop || !cb) { - return new Promise(function (resolve, reject) { - let cbp = function (err, result) { + var promise = new Promise(function (resolve, reject) { + let cbp = (err, result) => { + promise._done = true // just for testing. if (err) reject(err) resolve(result) } fn(cbp) }) + return promise } else { return fn(cb) } diff --git a/test/test-bulk-replace-one-promise.js b/test/test-bulk-replace-one-promise.js index b177e57..b2ca124 100644 --- a/test/test-bulk-replace-one-promise.js +++ b/test/test-bulk-replace-one-promise.js @@ -1,30 +1,28 @@ var insert = require('./insert') -insert('bulk replace one', [{ +insert('bulk replace one promise', [{ name: 'Squirtle', type: 'water' }, { name: 'Starmie', type: 'water' -}], function (db, t, done) { - db.runCommand('serverStatus', function (err, resp) { - t.error(err) - if (parseFloat(resp.version) < 2.6) return t.end() +}], async function (db, t, done) { + var resp = await db.runCommand('serverStatus') + if (parseFloat(resp.version) < 2.6) return t.end() - var bulk = db.a.initializeUnorderedBulkOp() - bulk.find({ name: 'Squirtle' }).replaceOne({ name: 'Charmander', type: 'fire' }) - bulk.find({ name: 'Starmie' }).replaceOne({ type: 'fire' }) + var bulk = db.a.initializeUnorderedBulkOp() + bulk.find({ name: 'Squirtle' }).replaceOne({ name: 'Charmander', type: 'fire' }) + bulk.find({ name: 'Starmie' }).replaceOne({ type: 'fire' }) - bulk.execute(function (err, res) { + bulk.execute(function (err, res) { + t.error(err) + t.ok(res.ok) + db.a.find(function (err, res) { t.error(err) - t.ok(res.ok) - db.a.find(function (err, res) { - t.error(err) - t.equal(res[0].name, 'Charmander') - t.equal(res[1].name, undefined) + t.equal(res[0].name, 'Charmander') + t.equal(res[1].name, undefined) - t.equal(res[0].type, 'fire') - t.equal(res[1].type, 'fire') - t.end() - }) + t.equal(res[0].type, 'fire') + t.equal(res[1].type, 'fire') + t.end() }) }) }) diff --git a/test/test-find-promise.js b/test/test-find-promise.js index 57679fd..87b99ef 100644 --- a/test/test-find-promise.js +++ b/test/test-find-promise.js @@ -1,6 +1,6 @@ var insert = require('./insert') -insert('find', [{ +insert('find promise', [{ hello: 'world' }], async function (db, t, done) { let docs = await db.a.find().toArray() diff --git a/test/test-insert-promise.js b/test/test-insert-promise.js index 3edc282..0f88d56 100644 --- a/test/test-insert-promise.js +++ b/test/test-insert-promise.js @@ -2,7 +2,7 @@ var test = require('./tape') var mongojs = require('../index') var db = mongojs('test', ['a', 'b']) -test('insert', async function (t) { +test('insert promise', async function (t) { let docs = await db.a.insert( [{ name: 'Squirtle' }, { name: 'Charmander' }, { name: 'Bulbasaur' }]) t.ok(docs[0]._id) diff --git a/test/test-noop-promise-cleanup.js b/test/test-noop-promise-cleanup.js new file mode 100644 index 0000000..eb03c81 --- /dev/null +++ b/test/test-noop-promise-cleanup.js @@ -0,0 +1,27 @@ +var test = require('./tape') +var mongojs = require('../index') +var db = mongojs('test', ['a', 'b']) + +test('insert', function (t) { + db.a.insert([{name: 'Squirtle'}, {name: 'Charmander'}, {name: 'Bulbasaur'}], function (err, docs) { + t.error(err) + t.ok(docs[0]._id) + t.ok(docs[1]._id) + t.ok(docs[2]._id) + + // Ensure we old calls with "noop" (no callback) still happen as expected. + // And that promises are cleaned up. + var ignoredPromise = db.a.insert([{name: 'Pidgeotto'}]) + setTimeout(function () { + db.a.find({name: 'Pidgeotto'}, function (err, docs) { + t.error(err) + t.equal(docs[0].name, 'Pidgeotto') + t.equal(docs.length, 1) + t.ok(ignoredPromise._done) + db.a.remove(function () { + db.close(t.end.bind(t)) + }) + }) + }, 200) + }) +}) diff --git a/test/test-save-promise.js b/test/test-save-promise.js index 82dbd60..4e2b86a 100644 --- a/test/test-save-promise.js +++ b/test/test-save-promise.js @@ -2,7 +2,7 @@ var test = require('./tape') var mongojs = require('../index') var db = mongojs('test', ['a', 'b']) -test('save', async function (t) { +test('save promise', async function (t) { let doc = await db.a.save({hello: 'world'}) t.equal(doc.hello, 'world') t.ok(doc._id) diff --git a/test/test-update-promise.js b/test/test-update-promise.js index 4383f53..5fd56cc 100644 --- a/test/test-update-promise.js +++ b/test/test-update-promise.js @@ -1,6 +1,6 @@ var insert = require('./insert') -insert('update', [{ +insert('update promise', [{ hello: 'world' }], async function (db, t, done) { let info = await db.a.update({hello: 'world'}, {$set: {hello: 'verden'}}) From 22d340816dece521909d26451e89cb9cf64c1d0e Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Thu, 27 Dec 2018 15:35:28 -0800 Subject: [PATCH 5/7] typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 869366d..8035a69 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ Version > 1.0.x is a major rewrite of mongojs. So expect some things not to work * __Removed__ `mongojs.connect` use `mongojs()` directly instead -# Async/Await Support +## Async/Await Support To have mongojs behave even more like the official MongoDB repl, rather than use callbacks, you can query or update the database using async/await. This works wherever callbacks can be used. Just omit the callback and use the await keyword. Don't forget to use the "async" keyword on your caller function. @@ -264,7 +264,7 @@ To have mongojs behave even more like the official MongoDB repl, rather than use // find a document using a native ObjectId var doc = db.mycollection.findOne({_id: mongojs.ObjectId('523209c4561c640000000001')} - // and so on, so earlier examples. + // and so on, see earlier examples. })() From a189168b50e861f8d32aec8ef1cdc4893f77a559 Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Fri, 28 Dec 2018 15:17:11 -0800 Subject: [PATCH 6/7] comment on promisify function --- lib/promisify.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/promisify.js b/lib/promisify.js index dc3fad2..5e479c4 100644 --- a/lib/promisify.js +++ b/lib/promisify.js @@ -1,3 +1,12 @@ +/** + * promisify(fn, cb) + * + * A progressive promisifier. Wraps an async code block which normally takes a callback, and allows + * instead returning a promise when no callback is passed in. + * + * fn - function which accepts a callback + * cb - a callback to use when the wrapped code finishes, omitting this, promisify returns a Promise. + */ module.exports = function (fn, cb) { if (cb === module.exports.noop || !cb) { var promise = new Promise(function (resolve, reject) { From 6cb72a9cf1ed8c0e1e1d7e529a5257421156db87 Mon Sep 17 00:00:00 2001 From: Clark Van Oyen Date: Sat, 29 Dec 2018 09:22:26 -0800 Subject: [PATCH 7/7] fix docs --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8035a69..bbe0350 100644 --- a/README.md +++ b/README.md @@ -253,14 +253,16 @@ To have mongojs behave even more like the official MongoDB repl, rather than use ```js -(async function(){ // you need an async callee. - - // find everything - var docs = await db.mycollection.find() // docs is an array of all the documents in mycollection - - // find everything, but sort by name - var docs = await db.mycollection.find().sort({name: 1} - // docs is now a sorted array +(async function(){ // you need an async caller. + + // insert a record in mycollection + await db.mycollection.insert({hello: 'world'}) + + // update it + await db.mycollection.update({$set: {hello: 'again'}}) + + // find everything. when not providing a callback, collection.find() returns a cursor. This is similar to how the MongoDB Shell iterates 20 records at a time. To get all results immediately, call toArray() + var docs = await db.mycollection.find().toArray() // docs is an array of all the documents in mycollection // find a document using a native ObjectId var doc = db.mycollection.findOne({_id: mongojs.ObjectId('523209c4561c640000000001')}