From 8ed95c604d00939f80b58e36a2e287039bc5a579 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 25 Jun 2024 14:38:07 -0400 Subject: [PATCH 01/10] BREAKING CHANGE: remove count and findOneAndRemove, add findOneAndDelete, countDocuments, estimatedDocumentCount --- lib/collection/collection.js | 3 +- lib/collection/node.js | 13 +++- lib/mquery.js | 90 +++++++++++++++++--------- lib/permissions.js | 12 ++-- package.json | 2 +- test/index.js | 118 +++++++++++++++++------------------ 6 files changed, 138 insertions(+), 100 deletions(-) diff --git a/lib/collection/collection.js b/lib/collection/collection.js index b473f89..3bea9a4 100644 --- a/lib/collection/collection.js +++ b/lib/collection/collection.js @@ -10,7 +10,8 @@ const methods = [ 'updateMany', 'updateOne', 'replaceOne', - 'count', + 'countDocuments', + 'estimatedDocumentCount', 'distinct', 'findOneAndDelete', 'findOneAndUpdate', diff --git a/lib/collection/node.js b/lib/collection/node.js index 6580f51..7421a9f 100644 --- a/lib/collection/node.js +++ b/lib/collection/node.js @@ -31,10 +31,17 @@ class NodeCollection extends Collection { } /** - * count(match, options) + * countDocuments(match, options) */ - async count(match, options) { - return this.collection.count(match, options); + async countDocuments(match, options) { + return this.collection.countDocuments(match, options); + } + + /** + * estimatedDocumentCount(match, options) + */ + async estimatedDocumentCount(match, options) { + return this.collection.estimatedDocumentCount(match, options); } /** diff --git a/lib/mquery.js b/lib/mquery.js index dc874ad..a38ff5e 100644 --- a/lib/mquery.js +++ b/lib/mquery.js @@ -1943,47 +1943,80 @@ Query.prototype._findOne = async function _findOne() { }; /** - * Exectues the query as a count() operation. + * Executes the query as a countDocuments() operation. * * #### Example: * - * query.count().where('color', 'black').exec(); + * query.countDocuments().where('color', 'black').exec(); * - * query.count({ color: 'black' }) + * query.countDocuments({ color: 'black' }) * - * await query.count({ color: 'black' }); + * await query.countDocuments({ color: 'black' }); * - * const doc = await query.where('color', 'black').count(); + * const count = await query.where('color', 'black').countDocuments(); * console.log('there are %d kittens', count); * - * @param {Object} [criteria] mongodb selector + * @param {Object} [filter] mongodb selector * @return {Query} this - * @see mongodb http://www.mongodb.org/display/DOCS/Aggregation#Aggregation-Count * @api public */ -Query.prototype.count = function(criteria) { - this.op = 'count'; +Query.prototype.countDocuments = function(filter) { + this.op = 'countDocuments'; this._validate(); - if (Query.canMerge(criteria)) { - this.merge(criteria); + if (Query.canMerge(filter)) { + this.merge(filter); } return this; }; +/** + * Executes a `countDocuments` Query + * @returns the results + */ +Query.prototype._countDocuments = async function _countDocuments() { + const conds = this._conditions, + options = this._optionsForExec(); + + debug('countDocuments', this._collection.collectionName, conds, options); + + return this._collection.countDocuments(conds, options); +}; + +/** + * Executes the query as a estimatedDocumentCount() operation. + * + * #### Example: + * + * query.estimatedDocumentCount(); + * + * const count = await query.estimatedDocumentCount(); + * console.log('there are %d kittens', count); + * + * @return {Query} this + * @api public + */ + +Query.prototype.estimatedDocumentCount = function() { + this.op = 'estimatedDocumentCount'; + this._validate(); + + return this; +}; + /** * Executes a `count` Query * @returns the results */ -Query.prototype._count = async function _count() { +Query.prototype._estimatedDocumentCount = async function _estimatedDocumentCount() { const conds = this._conditions, options = this._optionsForExec(); - debug('count', this._collection.collectionName, conds, options); + debug('estimatedDocumentCount', this._collection.collectionName, conds, options); - return this._collection.count(conds, options); + return this._collection.estimatedDocumentCount(conds, options); }; /** @@ -2329,7 +2362,7 @@ Query.prototype._findOneAndUpdate = async function() { }; /** - * Issues a mongodb [findAndModify](http://www.mongodb.org/display/DOCS/findAndModify+Command) remove command. + * Issues a mongodb findOneAndDelete. * * Finds a matching document, removes it, returning the found document (if any). * @@ -2339,28 +2372,25 @@ Query.prototype._findOneAndUpdate = async function() { * * #### Examples: * - * await A.where().findOneAndRemove(conditions, options) // executes - * A.where().findOneAndRemove(conditions, options) // return Query - * await A.where().findOneAndRemove(conditions) // executes - * A.where().findOneAndRemove(conditions) // returns Query - * await A.where().findOneAndRemove() // executes - * A.where().findOneAndRemove() // returns Query - * A.where().findOneAndDelete() // alias of .findOneAndRemove() + * await A.where().findOneAndDelete(conditions, options) // executes + * A.where().findOneAndDelete(conditions, options) // return Query + * await A.where().findOneAndDelete(conditions) // executes + * A.where().findOneAndDelete(conditions) // returns Query + * await A.where().findOneAndDelete() // executes + * A.where().findOneAndDelete() // returns Query * - * @param {Object} [conditions] + * @param {Object} [filter] * @param {Object} [options] * @return {Query} this - * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command * @api public */ -Query.prototype.findOneAndRemove = Query.prototype.findOneAndDelete = function(conditions, options) { - this.op = 'findOneAndRemove'; +Query.prototype.findOneAndDelete = function(filter, options) { + this.op = 'findOneAndDelete'; this._validate(); - // apply conditions - if (Query.canMerge(conditions)) { - this.merge(conditions); + if (Query.canMerge(filter)) { + this.merge(filter); } // apply options @@ -2373,7 +2403,7 @@ Query.prototype.findOneAndRemove = Query.prototype.findOneAndDelete = function(c * Executes a `findOneAndRemove` Query * @returns the results */ -Query.prototype._findOneAndRemove = async function() { +Query.prototype._findOneAndDelete = async function() { const options = this._optionsForExec(); const conds = this._conditions; diff --git a/lib/permissions.js b/lib/permissions.js index 97d2c73..1b6b9d6 100644 --- a/lib/permissions.js +++ b/lib/permissions.js @@ -34,7 +34,7 @@ denied.distinct.tailable = true; denied.findOneAndUpdate = -denied.findOneAndRemove = function(self) { +denied.findOneAndDelete = function(self) { const keys = Object.keys(denied.findOneAndUpdate); let err; @@ -54,12 +54,12 @@ denied.findOneAndUpdate.batchSize = denied.findOneAndUpdate.tailable = true; -denied.count = function(self) { +denied.countDocuments = function(self) { if (self._fields && Object.keys(self._fields).length > 0) { return 'field selection and slice'; } - const keys = Object.keys(denied.count); + const keys = Object.keys(denied.countDocuments); let err; keys.every(function(option) { @@ -73,6 +73,6 @@ denied.count = function(self) { return err; }; -denied.count.slice = -denied.count.batchSize = -denied.count.tailable = true; +denied.countDocuments.slice = +denied.countDocuments.batchSize = +denied.countDocuments.tailable = true; diff --git a/package.json b/package.json index 9016083..b8a4bd9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "eslint": "8.x", "eslint-plugin-mocha-no-only": "1.1.1", "mocha": "9.x", - "mongodb": "5.x" + "mongodb": "6.x" }, "bugs": { "url": "https://github.com/aheckmann/mquery/issues/new" diff --git a/test/index.js b/test/index.js index 2da006b..daec1c9 100644 --- a/test/index.js +++ b/test/index.js @@ -1105,7 +1105,7 @@ describe('mquery', function() { }); noDistinct('slice'); - no('count', 'slice'); + no('countDocuments', 'slice'); }); // options @@ -1202,7 +1202,7 @@ describe('mquery', function() { }); if (!options.distinct) noDistinct(type); - if (!options.count) no('count', type); + if (!options.count) no('countDocuments', type); }); } @@ -1333,7 +1333,7 @@ describe('mquery', function() { assert.equal(m, m.tailable()); }); noDistinct('tailable'); - no('count', 'tailable'); + no('countDocuments', 'tailable'); }); describe('writeConcern', function() { @@ -1557,32 +1557,32 @@ describe('mquery', function() { }); }); - describe('count', function() { + describe('countDocuments', function() { describe('with no exec', function() { it('does not execute', function() { const m = mquery(); assert.doesNotThrow(function() { - m.count(); + m.countDocuments(); }); assert.doesNotThrow(function() { - m.count({ x: 1 }); + m.countDocuments({ x: 1 }); }); }); }); it('is chainable', function() { const m = mquery(); - const n = m.count({ x: 1 }).count().count({ y: 2 }); + const n = m.countDocuments({ x: 1 }).countDocuments().countDocuments({ y: 2 }); assert.equal(m, n); assert.deepEqual(m._conditions, { x: 1, y: 2 }); - assert.equal('count', m.op); + assert.equal(m.op, 'countDocuments'); }); it('merges other queries', function() { const m = mquery({ name: 'mquery' }); m.read('nearest'); m.select('_id'); - const a = mquery().count(m); + const a = mquery().countDocuments(m); assert.deepEqual(a._conditions, m._conditions); assert.deepEqual(a.options, m.options); assert.deepEqual(a._fields, m._fields); @@ -1598,18 +1598,18 @@ describe('mquery', function() { }); it('when criteria is passed with a exec', async() => { - const count = await mquery().collection(col).count({ name: 'mquery count' }); + const count = await mquery().collection(col).countDocuments({ name: 'mquery count' }); assert.ok(count); assert.ok(1 === count); }); it('when Query is passed with a exec', async() => { const m = mquery({ name: 'mquery count' }); - const count = await mquery().collection(col).count(m); + const count = await mquery().collection(col).countDocuments(m); assert.ok(count); assert.ok(1 === count); }); it('when just nothing is passed but executed', async() => { - const count = await mquery({ name: 'mquery count' }).collection(col).count(); + const count = await mquery({ name: 'mquery count' }).collection(col).countDocuments(); assert.ok(1 === count); }); }); @@ -1617,49 +1617,49 @@ describe('mquery', function() { describe('validates its option', function() { it('sort', function(done) { assert.doesNotThrow(function() { - mquery().sort('x').count(); + mquery().sort('x').countDocuments(); }); done(); }); it('select', function(done) { assert.throws(function() { - mquery().select('x').count(); + mquery().select('x').countDocuments(); }, /field selection and slice cannot be used with count/); done(); }); it('slice', function(done) { assert.throws(function() { - mquery().where('x').slice(-3).count(); + mquery().where('x').slice(-3).countDocuments(); }, /field selection and slice cannot be used with count/); done(); }); it('limit', function(done) { assert.doesNotThrow(function() { - mquery().limit(3).count(); + mquery().limit(3).countDocuments(); }); done(); }); it('skip', function(done) { assert.doesNotThrow(function() { - mquery().skip(3).count(); + mquery().skip(3).countDocuments(); }); done(); }); it('batchSize', function(done) { assert.throws(function() { - mquery({}, { batchSize: 3 }).count(); + mquery({}, { batchSize: 3 }).countDocuments(); }, /batchSize cannot be used with count/); done(); }); it('tailable', function(done) { assert.throws(function() { - mquery().tailable().count(); + mquery().tailable().countDocuments(); }, /tailable cannot be used with count/); done(); }); @@ -2016,8 +2016,8 @@ describe('mquery', function() { name = '1 arg'; const n = m.updateOne({ $set: { name: name } }).setOptions({ returnDocument: 'after' }); const res = await n.findOneAndUpdate(); - assert.ok(res.value); - assert.equal(res.value.name, name); + assert.ok(res); + assert.equal(res.name, name); }); }); describe('with 2 args', function() { @@ -2037,7 +2037,7 @@ describe('mquery', function() { it('update + exec', async() => { const m = mquery().collection(col).where({ name: name }); const res = await m.findOneAndUpdate({}, { $inc: { age: 10 } }, { returnDocument: 'after' }); - assert.equal(10, res.value.age); + assert.equal(res.age, 10); }); }); describe('with 3 args', function() { @@ -2051,31 +2051,31 @@ describe('mquery', function() { it('conditions + update + exec', async() => { const m = mquery().collection(col); const res = await m.findOneAndUpdate({ name: name }, { works: true }, { returnDocument: 'after' }); - assert.ok(res.value); - assert.equal(name, res.value.name); - assert.ok(true === res.value.works); + assert.ok(res); + assert.equal(res.name, name); + assert.ok(res.works); }); }); describe('with 4 args', function() { it('conditions + update + options + exec', async() => { const m = mquery().collection(col); const res = await m.findOneAndUpdate({ name: name }, { works: false }, {}); - assert.ok(res.value); - assert.equal(name, res.value.name); - assert.ok(true === res.value.works); + assert.ok(res); + assert.equal(res.name, name); + assert.ok(res.works); }); }); }); - describe('findOneAndRemove', function() { - let name = 'findOneAndRemove'; + describe('findOneAndDelete', function() { + let name = 'findOneAndDelete'; - validateFindAndModifyOptions('findOneAndRemove'); + validateFindAndModifyOptions('findOneAndDelete'); describe('with 0 args', function() { it('makes no changes', function() { const m = mquery(); - const n = m.findOneAndRemove(); + const n = m.findOneAndDelete(); assert.deepEqual(m, n); }); }); @@ -2083,61 +2083,61 @@ describe('mquery', function() { describe('that is an object', function() { it('updates the doc', function() { const m = mquery(); - const n = m.findOneAndRemove({ name: '1 arg' }); + const n = m.findOneAndDelete({ name: '1 arg' }); assert.deepEqual(n._conditions, { name: '1 arg' }); }); }); describe('that is a query', function() { it('updates the doc', function() { const m = mquery({ name: name }); - const n = m.findOneAndRemove(m); + const n = m.findOneAndDelete(m); assert.deepEqual(n._conditions, { name: name }); }); }); it('that is a function', async() => { await col.insertOne({ name: name }); const m = mquery({ name: name }).collection(col); - const res = await m.findOneAndRemove(); - assert.ok(res.value); - assert.equal(name, res.value.name); + const res = await m.findOneAndDelete(); + assert.ok(res); + assert.equal(res.name, name); }); }); describe('with 2 args', function() { it('conditions + options', function() { const m = mquery().collection(col); - m.findOneAndRemove({ name: name }, { returnDocument: 'after' }); + m.findOneAndDelete({ name: name }, { returnDocument: 'after' }); assert.deepEqual({ name: name }, m._conditions); assert.deepEqual({ returnDocument: 'after' }, m.options); }); it('query + options', function() { const n = mquery({ name: name }); const m = mquery().collection(col); - m.findOneAndRemove(n, { sort: { x: 1 } }); + m.findOneAndDelete(n, { sort: { x: 1 } }); assert.deepEqual({ name: name }, m._conditions); assert.deepEqual({ sort: { x: 1 } }, m.options); }); it('conditions + exec', async() => { await col.insertOne({ name: name }); const m = mquery().collection(col); - const res = await m.findOneAndRemove({ name: name }); - assert.equal(name, res.value.name); + const res = await m.findOneAndDelete({ name: name }); + assert.equal(res.name, name); }); it('query + exec', async() => { await col.insertOne({ name: name }); const n = mquery({ name: name }); const m = mquery().collection(col); - const res = await m.findOneAndRemove(n); - assert.equal(name, res.value.name); + const res = await m.findOneAndDelete(n); + assert.equal(res.name, name); }); }); describe('with 3 args', function() { it('conditions + options + exec', async() => { - name = 'findOneAndRemove + conds + options + cb'; + name = 'findOneAndDelete + conds + options + cb'; await col.insertMany([{ name: name }, { name: 'a' }]); const m = mquery().collection(col); - const res = await m.findOneAndRemove({ name: name }, { sort: { name: 1 } }); - assert.ok(res.value); - assert.equal(name, res.value.name); + const res = await m.findOneAndDelete({ name: name }, { sort: { name: 1 } }); + assert.ok(res); + assert.equal(res.name, name); }); }); }); @@ -2210,7 +2210,7 @@ describe('mquery', function() { }); it('count', async() => { - const m = mquery().collection(col).count({ name: 'exec' }); + const m = mquery().collection(col).countDocuments({ name: 'exec' }); const count = await m.exec(); assert.equal(2, count); }); @@ -2231,14 +2231,14 @@ describe('mquery', function() { it('works', async() => { await mquery().collection(col).updateMany({ name: 'exec' }, { name: 'test' }). exec(); - const res = await mquery().collection(col).count({ name: 'test' }).exec(); + const res = await mquery().collection(col).countDocuments({ name: 'test' }).exec(); assert.equal(res, 2); }); it('works with write concern', async() => { await mquery().collection(col).updateMany({ name: 'exec' }, { name: 'test' }) .w(1).j(true).wtimeout(1000) .exec(); - const res = await mquery().collection(col).count({ name: 'test' }).exec(); + const res = await mquery().collection(col).countDocuments({ name: 'test' }).exec(); assert.equal(res, 2); }); }); @@ -2247,7 +2247,7 @@ describe('mquery', function() { it('works', async() => { await mquery().collection(col).updateOne({ name: 'exec' }, { name: 'test' }). exec(); - const res = await mquery().collection(col).count({ name: 'test' }).exec(); + const res = await mquery().collection(col).countDocuments({ name: 'test' }).exec(); assert.equal(res, 1); }); }); @@ -2293,19 +2293,19 @@ describe('mquery', function() { const m = mquery().collection(col); m.findOneAndUpdate({ name: 'exec', age: 1 }, { $set: { name: 'findOneAndUpdate' } }, { returnDocument: 'after' }); const res = await m.exec(); - assert.equal('findOneAndUpdate', res.value.name); + assert.equal(res.name, 'findOneAndUpdate'); }); }); - describe('findOneAndRemove', function() { + describe('findOneAndDelete', function() { it('with exec', async() => { const m = mquery().collection(col); - m.findOneAndRemove({ name: 'exec', age: 2 }); + m.findOneAndDelete({ name: 'exec', age: 2 }); const res = await m.exec(); - assert.equal('exec', res.value.name); - assert.equal(2, res.value.age); - const num = await mquery().collection(col).count({ name: 'exec' }); - assert.equal(1, num); + assert.equal(res.name, 'exec'); + assert.equal(res.age, 2); + const num = await mquery().collection(col).countDocuments({ name: 'exec' }); + assert.equal(num, 1); }); }); }); @@ -2375,7 +2375,7 @@ describe('mquery', function() { }); it('creates a promise that is resolved on success', function(done) { - const promise = mquery().collection(col).count({ name: 'then' }).then(); + const promise = mquery().collection(col).countDocuments({ name: 'then' }).then(); promise.then(function(count) { assert.equal(2, count); done(); From 4716c31d53173053815c74bc29c9433d548d01ce Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 25 Jun 2024 14:40:52 -0400 Subject: [PATCH 02/10] run tests on more recent node versions --- .github/workflows/test.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b74300e..2315522 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - node: [14, 16, 18] + node: [16, 18, 20] mongo: [4.2, 5.0] services: mongodb: diff --git a/package.json b/package.json index b8a4bd9..717f282 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "url": "git://github.com/aheckmann/mquery.git" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "dependencies": { "debug": "4.x" From d22970e323ec717317eb0ee525201a1382aa914b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 10:01:43 -0400 Subject: [PATCH 03/10] remove debug dependency, bump devDependencies, fix tests for mongodb driver v6 --- lib/mquery.js | 18 ------------------ package.json | 7 ++----- test/index.js | 36 ++++++++++++++++-------------------- 3 files changed, 18 insertions(+), 43 deletions(-) diff --git a/lib/mquery.js b/lib/mquery.js index dc874ad..6a8211e 100644 --- a/lib/mquery.js +++ b/lib/mquery.js @@ -7,7 +7,6 @@ const assert = require('assert'); const util = require('util'); const utils = require('./utils'); -const debug = require('debug')('mquery'); /** * Query constructor used for building queries. @@ -1857,8 +1856,6 @@ Query.prototype._find = async function _find() { options.fields = this._fieldsForExec(); } - debug('_find', this._collection.collectionName, conds, options); - return this._collection.find(conds, options); }; @@ -1893,7 +1890,6 @@ Query.prototype.cursor = function cursor(criteria) { options.fields = this._fieldsForExec(); } - debug('findCursor', this._collection.collectionName, conds, options); return this._collection.findCursor(conds, options); }; @@ -1937,8 +1933,6 @@ Query.prototype._findOne = async function _findOne() { options.fields = this._fieldsForExec(); } - debug('findOne', this._collection.collectionName, conds, options); - return this._collection.findOne(conds, options); }; @@ -1981,8 +1975,6 @@ Query.prototype._count = async function _count() { const conds = this._conditions, options = this._optionsForExec(); - debug('count', this._collection.collectionName, conds, options); - return this._collection.count(conds, options); }; @@ -2037,8 +2029,6 @@ Query.prototype._distinct = async function _distinct() { const conds = this._conditions, options = this._optionsForExec(); - debug('distinct', this._collection.collectionName, conds, options); - return this._collection.distinct(this._distinctDoc, conds, options); }; @@ -2183,8 +2173,6 @@ async function _updateExec(query, op) { const criteria = query._conditions; const doc = query._updateForExec(); - debug('update', query._collection.collectionName, criteria, doc, options); - return query._collection[op](criteria, doc, options); } @@ -2220,8 +2208,6 @@ Query.prototype._deleteOne = async function() { const conds = this._conditions; - debug('deleteOne', this._collection.collectionName, conds, options); - return this._collection.deleteOne(conds, options); }; @@ -2258,8 +2244,6 @@ Query.prototype._deleteMany = async function() { const conds = this._conditions; - debug('deleteOne', this._collection.collectionName, conds, options); - return this._collection.deleteMany(conds, options); }; @@ -2469,8 +2453,6 @@ Query.prototype.cursor = function() { options.fields = this._fieldsForExec(); } - debug('cursor', this._collection.collectionName, conds, options); - return this._collection.findCursor(conds, options); }; diff --git a/package.json b/package.json index 9016083..70971a8 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,11 @@ "engines": { "node": ">=14.0.0" }, - "dependencies": { - "debug": "4.x" - }, "devDependencies": { "eslint": "8.x", "eslint-plugin-mocha-no-only": "1.1.1", - "mocha": "9.x", - "mongodb": "5.x" + "mocha": "11.x", + "mongodb": "6.x" }, "bugs": { "url": "https://github.com/aheckmann/mquery/issues/new" diff --git a/test/index.js b/test/index.js index 2da006b..5623e1c 100644 --- a/test/index.js +++ b/test/index.js @@ -2014,7 +2014,7 @@ describe('mquery', function() { await col.insertOne({ name: name }); const m = mquery({ name: name }).collection(col); name = '1 arg'; - const n = m.updateOne({ $set: { name: name } }).setOptions({ returnDocument: 'after' }); + const n = m.updateOne({ $set: { name: name } }).setOptions({ returnDocument: 'after', includeResultMetadata: true }); const res = await n.findOneAndUpdate(); assert.ok(res.value); assert.equal(res.value.name, name); @@ -2036,7 +2036,7 @@ describe('mquery', function() { }); it('update + exec', async() => { const m = mquery().collection(col).where({ name: name }); - const res = await m.findOneAndUpdate({}, { $inc: { age: 10 } }, { returnDocument: 'after' }); + const res = await m.findOneAndUpdate({}, { $inc: { age: 10 } }, { returnDocument: 'after', includeResultMetadata: true }); assert.equal(10, res.value.age); }); }); @@ -2050,19 +2050,17 @@ describe('mquery', function() { }); it('conditions + update + exec', async() => { const m = mquery().collection(col); - const res = await m.findOneAndUpdate({ name: name }, { works: true }, { returnDocument: 'after' }); + const res = await m.findOneAndUpdate({ name: name }, { works: true }, { returnDocument: 'after', includeResultMetadata: true }); assert.ok(res.value); assert.equal(name, res.value.name); assert.ok(true === res.value.works); }); - }); - describe('with 4 args', function() { - it('conditions + update + options + exec', async() => { + it('empty options', async() => { const m = mquery().collection(col); const res = await m.findOneAndUpdate({ name: name }, { works: false }, {}); - assert.ok(res.value); - assert.equal(name, res.value.name); - assert.ok(true === res.value.works); + assert.ok(res); + assert.equal(name, res.name); + assert.ok(true === res.works); }); }); }); @@ -2094,12 +2092,12 @@ describe('mquery', function() { assert.deepEqual(n._conditions, { name: name }); }); }); - it('that is a function', async() => { + it('executes', async() => { await col.insertOne({ name: name }); const m = mquery({ name: name }).collection(col); const res = await m.findOneAndRemove(); - assert.ok(res.value); - assert.equal(name, res.value.name); + assert.ok(res); + assert.equal(name, res.name); }); }); describe('with 2 args', function() { @@ -2120,22 +2118,20 @@ describe('mquery', function() { await col.insertOne({ name: name }); const m = mquery().collection(col); const res = await m.findOneAndRemove({ name: name }); - assert.equal(name, res.value.name); + assert.equal(name, res.name); }); it('query + exec', async() => { await col.insertOne({ name: name }); const n = mquery({ name: name }); const m = mquery().collection(col); const res = await m.findOneAndRemove(n); - assert.equal(name, res.value.name); + assert.equal(name, res.name); }); - }); - describe('with 3 args', function() { it('conditions + options + exec', async() => { name = 'findOneAndRemove + conds + options + cb'; await col.insertMany([{ name: name }, { name: 'a' }]); const m = mquery().collection(col); - const res = await m.findOneAndRemove({ name: name }, { sort: { name: 1 } }); + const res = await m.findOneAndRemove({ name: name }, { sort: { name: 1 }, includeResultMetadata: true }); assert.ok(res.value); assert.equal(name, res.value.name); }); @@ -2291,7 +2287,7 @@ describe('mquery', function() { describe('findOneAndUpdate', function() { it('with exec', async() => { const m = mquery().collection(col); - m.findOneAndUpdate({ name: 'exec', age: 1 }, { $set: { name: 'findOneAndUpdate' } }, { returnDocument: 'after' }); + m.findOneAndUpdate({ name: 'exec', age: 1 }, { $set: { name: 'findOneAndUpdate' } }, { returnDocument: 'after', includeResultMetadata: true }); const res = await m.exec(); assert.equal('findOneAndUpdate', res.value.name); }); @@ -2302,8 +2298,8 @@ describe('mquery', function() { const m = mquery().collection(col); m.findOneAndRemove({ name: 'exec', age: 2 }); const res = await m.exec(); - assert.equal('exec', res.value.name); - assert.equal(2, res.value.age); + assert.equal('exec', res.name); + assert.equal(2, res.age); const num = await mquery().collection(col).count({ name: 'exec' }); assert.equal(1, num); }); From e2dd39a3384cce20386741f90b87368814cc288a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 10:12:40 -0400 Subject: [PATCH 04/10] bump ubuntu, node, mongodb for tests --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2315522..fb95f20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,12 +8,12 @@ permissions: jobs: test: # os is not in the matrix, because otherwise there would be way too many testing instances - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - node: [16, 18, 20] - mongo: [4.2, 5.0] + node: [20, 22] + mongo: [7.0, 8.0] services: mongodb: image: mongo:${{ matrix.mongo }} From 7deaea69ced0ade2b99175063b53ee67651eb6a2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 10:18:10 -0400 Subject: [PATCH 05/10] fix tests --- lib/mquery.js | 6 ++---- test/index.js | 14 +++++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/mquery.js b/lib/mquery.js index 3ded24d..51995a4 100644 --- a/lib/mquery.js +++ b/lib/mquery.js @@ -1971,10 +1971,8 @@ Query.prototype.countDocuments = function(filter) { * @returns the results */ Query.prototype._countDocuments = async function _countDocuments() { - const conds = this._conditions, - options = this._optionsForExec(); - - debug('countDocuments', this._collection.collectionName, conds, options); + const conds = this._conditions; + const options = this._optionsForExec(); return this._collection.countDocuments(conds, options); }; diff --git a/test/index.js b/test/index.js index 85e13d1..215fba3 100644 --- a/test/index.js +++ b/test/index.js @@ -2017,7 +2017,7 @@ describe('mquery', function() { const n = m.updateOne({ $set: { name: name } }).setOptions({ returnDocument: 'after', includeResultMetadata: true }); const res = await n.findOneAndUpdate(); assert.ok(res); - assert.equal(res.name, name); + assert.equal(res.value.name, name); }); }); describe('with 2 args', function() { @@ -2095,7 +2095,7 @@ describe('mquery', function() { it('executes', async() => { await col.insertOne({ name: name }); const m = mquery({ name: name }).collection(col); - const res = await m.findOneAndRemove(); + const res = await m.findOneAndDelete(); assert.ok(res); assert.equal(name, res.name); }); @@ -2117,21 +2117,21 @@ describe('mquery', function() { it('conditions + exec', async() => { await col.insertOne({ name: name }); const m = mquery().collection(col); - const res = await m.findOneAndRemove({ name: name }); + const res = await m.findOneAndDelete({ name: name }); assert.equal(name, res.name); }); it('query + exec', async() => { await col.insertOne({ name: name }); const n = mquery({ name: name }); const m = mquery().collection(col); - const res = await m.findOneAndRemove(n); + const res = await m.findOneAndDelete(n); assert.equal(name, res.name); }); it('conditions + options + exec', async() => { name = 'findOneAndDelete + conds + options + cb'; await col.insertMany([{ name: name }, { name: 'a' }]); const m = mquery().collection(col); - const res = await m.findOneAndRemove({ name: name }, { sort: { name: 1 }, includeResultMetadata: true }); + const res = await m.findOneAndDelete({ name: name }, { sort: { name: 1 }, includeResultMetadata: true }); assert.ok(res.value); assert.equal(name, res.value.name); }); @@ -2289,7 +2289,7 @@ describe('mquery', function() { const m = mquery().collection(col); m.findOneAndUpdate({ name: 'exec', age: 1 }, { $set: { name: 'findOneAndUpdate' } }, { returnDocument: 'after', includeResultMetadata: true }); const res = await m.exec(); - assert.equal(res.name, 'findOneAndUpdate'); + assert.equal(res.value.name, 'findOneAndUpdate'); }); }); @@ -2300,7 +2300,7 @@ describe('mquery', function() { const res = await m.exec(); assert.equal('exec', res.name); assert.equal(2, res.age); - const num = await mquery().collection(col).count({ name: 'exec' }); + const num = await mquery().collection(col).countDocuments({ name: 'exec' }); assert.equal(1, num); }); }); From 4a77a21c3649ad2955b165fb59fd660d9ee62924 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 10:28:32 -0400 Subject: [PATCH 06/10] BREAKING CHANGE: remove use$geoWithin, always use $geoWithin over $within --- lib/mquery.js | 31 ++++--------------------------- test/index.js | 12 ++++++------ 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/lib/mquery.js b/lib/mquery.js index 51995a4..0d5c8ac 100644 --- a/lib/mquery.js +++ b/lib/mquery.js @@ -58,29 +58,6 @@ function Query(criteria, options) { } } -/** - * This is a parameter that the user can set which determines if mquery - * uses $within or $geoWithin for queries. It defaults to true which - * means $geoWithin will be used. If using MongoDB < 2.4 you should - * set this to false. - * - * @api public - * @property use$geoWithin - */ - -let $withinCmd = '$geoWithin'; -Object.defineProperty(Query, 'use$geoWithin', { - get: function() { return $withinCmd == '$geoWithin'; }, - set: function(v) { - if (true === v) { - // mongodb >= 2.4 - $withinCmd = '$geoWithin'; - } else { - $withinCmd = '$within'; - } - } -}); - /** * Converts this query to a constructor function with all arguments and options retained. * @@ -732,7 +709,7 @@ Query.prototype.elemMatch = function() { Query.prototype.within = function within() { // opinionated, must be used after where this._ensurePath('within'); - this._geoComparison = $withinCmd; + this._geoComparison = '$geoWithin'; if (0 === arguments.length) { return this; @@ -800,7 +777,7 @@ Query.prototype.box = function() { } const conds = this._conditions[path] || (this._conditions[path] = {}); - conds[this._geoComparison || $withinCmd] = { $box: box }; + conds[this._geoComparison || '$geoWithin'] = { $box: box }; return this; }; @@ -834,7 +811,7 @@ Query.prototype.polygon = function() { } const conds = this._conditions[path] || (this._conditions[path] = {}); - conds[this._geoComparison || $withinCmd] = { $polygon: val }; + conds[this._geoComparison || '$geoWithin'] = { $polygon: val }; return this; }; @@ -882,7 +859,7 @@ Query.prototype.circle = function() { ? '$centerSphere' : '$center'; - const wKey = this._geoComparison || $withinCmd; + const wKey = this._geoComparison || '$geoWithin'; conds[wKey] = {}; conds[wKey][type] = [val.center, val.radius]; diff --git a/test/index.js b/test/index.js index 215fba3..4ffb250 100644 --- a/test/index.js +++ b/test/index.js @@ -546,33 +546,33 @@ describe('mquery', function() { describe('of length 1', function() { it('delegates to circle when center exists', function() { const m = mquery().where('loc').within({ center: [10, 10], radius: 3 }); - assert.deepEqual({ $within: { $center: [[10, 10], 3] } }, m._conditions.loc); + assert.deepEqual({ $geoWithin: { $center: [[10, 10], 3] } }, m._conditions.loc); }); it('delegates to box when exists', function() { const m = mquery().where('loc').within({ box: [[10, 10], [11, 14]] }); - assert.deepEqual({ $within: { $box: [[10, 10], [11, 14]] } }, m._conditions.loc); + assert.deepEqual({ $geoWithin: { $box: [[10, 10], [11, 14]] } }, m._conditions.loc); }); it('delegates to polygon when exists', function() { const m = mquery().where('loc').within({ polygon: [[10, 10], [11, 14], [10, 9]] }); - assert.deepEqual({ $within: { $polygon: [[10, 10], [11, 14], [10, 9]] } }, m._conditions.loc); + assert.deepEqual({ $geoWithin: { $polygon: [[10, 10], [11, 14], [10, 9]] } }, m._conditions.loc); }); it('delegates to geometry when exists', function() { const m = mquery().where('loc').within({ type: 'Polygon', coordinates: [[10, 10], [11, 14], [10, 9]] }); - assert.deepEqual({ $within: { $geometry: { type: 'Polygon', coordinates: [[10, 10], [11, 14], [10, 9]] } } }, m._conditions.loc); + assert.deepEqual({ $geoWithin: { $geometry: { type: 'Polygon', coordinates: [[10, 10], [11, 14], [10, 9]] } } }, m._conditions.loc); }); }); describe('of length 2', function() { it('delegates to box()', function() { const m = mquery().where('loc').within([1, 2], [2, 5]); - assert.deepEqual(m._conditions.loc, { $within: { $box: [[1, 2], [2, 5]] } }); + assert.deepEqual(m._conditions.loc, { $geoWithin: { $box: [[1, 2], [2, 5]] } }); }); }); describe('of length > 2', function() { it('delegates to polygon()', function() { const m = mquery().where('loc').within([1, 2], [2, 5], [2, 4], [1, 3]); - assert.deepEqual(m._conditions.loc, { $within: { $polygon: [[1, 2], [2, 5], [2, 4], [1, 3]] } }); + assert.deepEqual(m._conditions.loc, { $geoWithin: { $polygon: [[1, 2], [2, 5], [2, 4], [1, 3]] } }); }); }); }); From f36edc1b6425097bea6bbb8964d37b4f13dd5a0a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 11:44:01 -0400 Subject: [PATCH 07/10] BREAKING CHANGE: make 1-arg syntax for updateOne, updateMany, findOneAndX set conditions not update, add findOneAndReplace Fix #104 Re: Automattic/mongoose#15363 --- lib/collection/collection.js | 1 + lib/collection/node.js | 7 ++ lib/mquery.js | 86 ++++++++++++++++------ lib/permissions.js | 15 ++++ test/index.js | 138 +++++++++++++++++++++++++++++------ 5 files changed, 201 insertions(+), 46 deletions(-) diff --git a/lib/collection/collection.js b/lib/collection/collection.js index 3bea9a4..10366ca 100644 --- a/lib/collection/collection.js +++ b/lib/collection/collection.js @@ -14,6 +14,7 @@ const methods = [ 'estimatedDocumentCount', 'distinct', 'findOneAndDelete', + 'findOneAndReplace', 'findOneAndUpdate', 'aggregate', 'findCursor', diff --git a/lib/collection/node.js b/lib/collection/node.js index 7421a9f..d12983c 100644 --- a/lib/collection/node.js +++ b/lib/collection/node.js @@ -100,6 +100,13 @@ class NodeCollection extends Collection { return this.collection.findOneAndUpdate(match, update, options); } + /** + * findOneAndReplace(match, update, options) + */ + async findOneAndReplace(match, update, options) { + return this.collection.findOneAndReplace(match, update, options); + } + /** * var cursor = findCursor(match, options) */ diff --git a/lib/mquery.js b/lib/mquery.js index 0d5c8ac..13aa678 100644 --- a/lib/mquery.js +++ b/lib/mquery.js @@ -2058,11 +2058,6 @@ Query.prototype._distinct = async function _distinct() { */ Query.prototype.updateMany = function updateMany(criteria, doc, options) { - if (arguments.length === 1) { - doc = criteria; - criteria = options = undefined; - } - return _update(this, 'updateMany', criteria, doc, options); }; @@ -2093,11 +2088,6 @@ Query.prototype._updateMany = async function() { */ Query.prototype.updateOne = function updateOne(criteria, doc, options) { - if (arguments.length === 1) { - doc = criteria; - criteria = options = undefined; - } - return _update(this, 'updateOne', criteria, doc, options); }; @@ -2128,11 +2118,6 @@ Query.prototype._updateOne = async function() { */ Query.prototype.replaceOne = function replaceOne(criteria, doc, options) { - if (arguments.length === 1) { - doc = criteria; - criteria = options = undefined; - } - this.setOptions({ overwrite: true }); return _update(this, 'replaceOne', criteria, doc, options); }; @@ -2255,7 +2240,7 @@ Query.prototype._deleteMany = async function() { }; /** - * Issues a mongodb [findAndModify](http://www.mongodb.org/display/DOCS/findAndModify+Command) update command. + * Issues a mongodb findOneAndUpdate command. * * Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any). * @@ -2288,11 +2273,6 @@ Query.prototype.findOneAndUpdate = function(criteria, doc, options) { this.op = 'findOneAndUpdate'; this._validate(); - if (arguments.length === 1) { - doc = criteria; - criteria = options = undefined; - } - if (Query.canMerge(criteria)) { this.merge(criteria); } @@ -2319,6 +2299,68 @@ Query.prototype._findOneAndUpdate = async function() { return this._collection.findOneAndUpdate(conds, update, options); }; +/** + * Issues a mongodb findOneAndReplace command. + * + * Finds a matching document, replaces it according to the `replacement` arg, passing any `options`, and returns the found document (if any). + * + * #### Available options + * + * - `new`: bool - true to return the modified document rather than the original. defaults to true + * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. + * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update + * + * #### Examples: + * + * await query.findOneAndReplace(conditions, replacement, options) // executes + * query.findOneAndReplace(conditions, replacement, options) // returns Query + * await query.findOneAndReplace(conditions, replacement) // executes + * query.findOneAndReplace(conditions, replacement) // returns Query + * await query.findOneAndReplace(replacement) // returns Query + * query.findOneAndReplace(replacement) // returns Query + * await query.findOneAndReplace() // executes + * query.findOneAndReplace() // returns Query + * + * @param {Object|Query} [query] + * @param {Object} [replacement] + * @param {Object} [options] + * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command + * @return {Query} this + * @api public + */ + +Query.prototype.findOneAndReplace = function(criteria, replacement, options) { + this.op = 'findOneAndReplace'; + this._validate(); + + if (Query.canMerge(criteria)) { + this.merge(criteria); + } + + // apply replacement + if (replacement) { + this._updateDoc = replacement; + this.options = this.options || {}; + this.options.overwrite = true; + } + + options && this.setOptions(options); + + return this; +}; + +/** + * Executes a `findOneAndReplace` Query + * @returns the results + */ +Query.prototype._findOneAndReplace = async function() { + const conds = this._conditions; + const replacement = this._updateForExec(); + const options = this._optionsForExec(); + + return this._collection.findOneAndReplace(conds, replacement, options); +}; + /** * Issues a mongodb findOneAndDelete. * @@ -2573,7 +2615,7 @@ Query.prototype._fieldsForExec = function() { */ Query.prototype._updateForExec = function() { - const update = utils.clone(this._updateDoc); + const update = this._updateDoc == null ? {} : utils.clone(this._updateDoc); const ops = utils.keys(update); const ret = {}; diff --git a/lib/permissions.js b/lib/permissions.js index 1b6b9d6..b7ea0f3 100644 --- a/lib/permissions.js +++ b/lib/permissions.js @@ -53,6 +53,21 @@ denied.findOneAndUpdate.skip = denied.findOneAndUpdate.batchSize = denied.findOneAndUpdate.tailable = true; +denied.findOneAndReplace = function(self) { + const keys = Object.keys(denied.findOneAndUpdate); + let err; + + keys.every(function(option) { + if (self.options[option]) { + err = option; + return false; + } + return true; + }); + + return err; +}; + denied.countDocuments = function(self) { if (self._fields && Object.keys(self._fields).length > 0) { diff --git a/test/index.js b/test/index.js index 4ffb250..c49c7d0 100644 --- a/test/index.js +++ b/test/index.js @@ -59,7 +59,7 @@ describe('mquery', function() { const q = mquery().setOptions(opts); q.where(match); q.select(select); - q.updateOne(update); + q.updateOne({}, update); q.where(path); q.find(); @@ -1408,7 +1408,7 @@ describe('mquery', function() { const original = { $set: { iTerm: true } }; const m = mquery().updateOne(original); const n = mquery().merge(m); - m.updateOne({ $set: { x: 2 } }); + m.updateOne({}, { $set: { x: 2 } }); assert.notDeepEqual(m._updateDoc, n._updateDoc); done(); }); @@ -1429,7 +1429,7 @@ describe('mquery', function() { const original = { $set: { iTerm: true } }; const m = mquery().updateOne(original); const n = mquery().merge(original); - m.updateOne({ $set: { x: 2 } }); + m.updateOne({}, { $set: { x: 2 } }); assert.notDeepEqual(m._updateDoc, n._updateDoc); done(); }); @@ -1845,7 +1845,7 @@ describe('mquery', function() { }); it('is chainable', function() { - const m = mquery({ x: 1 }).updateOne({ y: 2 }); + const m = mquery({ x: 1 }).updateOne(null, { y: 2 }); const n = m.where({ y: 2 }); assert.equal(m, n); assert.deepEqual(n._conditions, { x: 1, y: 2 }); @@ -1855,8 +1855,8 @@ describe('mquery', function() { it('merges update doc arg', function() { const a = [1, 2]; - const m = mquery().where({ name: 'mquery' }).updateOne({ x: 'stuff', a: a }); - m.updateOne({ z: 'stuff' }); + const m = mquery().where({ name: 'mquery' }).updateOne(null, { x: 'stuff', a: a }); + m.updateOne(null, { z: 'stuff' }); assert.deepEqual(m._updateDoc, { z: 'stuff', x: 'stuff', a: a }); assert.deepEqual(m._conditions, { name: 'mquery' }); assert.ok(!m.options.overwrite); @@ -1904,7 +1904,7 @@ describe('mquery', function() { it('works', async() => { const m = mquery().collection(col); - const num = await m.where({ _id: id }).updateOne({ name: 'changed' }); + const num = await m.where({ _id: id }).updateOne(null, { name: 'changed' }); assert.ok(1, num); const doc = await m.findOne(); assert.equal(doc.name, 'changed'); @@ -1914,7 +1914,7 @@ describe('mquery', function() { describe('when just exec passed', function() { it('works', async() => { const m = mquery().collection(col).where({ _id: id }); - m.updateOne({ name: 'Frankenweenie' }); + m.updateOne(null, { name: 'Frankenweenie' }); const res = await m.updateOne(); assert.equal(res.modifiedCount, 1); const doc = await m.findOne(); @@ -1988,6 +1988,14 @@ describe('mquery', function() { validateFindAndModifyOptions('findOneAndUpdate'); + beforeEach(function() { + return mquery().collection(col).updateOne({ name }, { name }, { upsert: true }); + }); + + afterEach(function () { + return mquery().collection(col).deleteMany(); + }); + describe('with 0 args', function() { it('makes no changes', function() { const m = mquery(); @@ -1997,28 +2005,20 @@ describe('mquery', function() { }); describe('with 1 arg', function() { describe('that is an object', function() { - it('updates the doc', function() { + it('sets conditions', function() { const m = mquery(); - const n = m.findOneAndUpdate({ $set: { name: '1 arg' } }); - assert.deepEqual(n._updateDoc, { $set: { name: '1 arg' } }); + const n = m.findOneAndUpdate({ name: '1 arg' }); + assert.deepEqual(n._conditions, { name: '1 arg' }); + assert.strictEqual(n._updateDoc, undefined); }); }); describe('that is a query', function() { it('updates the doc', function() { - const m = mquery({ name: name }).updateOne({ x: 1 }); + const m = mquery({ name: name }).updateOne(null, { x: 1 }); const n = mquery().findOneAndUpdate(m); assert.deepEqual(n._updateDoc, { x: 1 }); }); }); - it('that is a function', async() => { - await col.insertOne({ name: name }); - const m = mquery({ name: name }).collection(col); - name = '1 arg'; - const n = m.updateOne({ $set: { name: name } }).setOptions({ returnDocument: 'after', includeResultMetadata: true }); - const res = await n.findOneAndUpdate(); - assert.ok(res); - assert.equal(res.value.name, name); - }); }); describe('with 2 args', function() { it('conditions + update', function() { @@ -2036,7 +2036,7 @@ describe('mquery', function() { }); it('update + exec', async() => { const m = mquery().collection(col).where({ name: name }); - const res = await m.findOneAndUpdate({}, { $inc: { age: 10 } }, { returnDocument: 'after', includeResultMetadata: true }); + const res = await m.findOneAndUpdate({}, { $set: { age: 10 } }, { returnDocument: 'after', includeResultMetadata: true }); assert.equal(10, res.value.age); }); }); @@ -2057,10 +2057,100 @@ describe('mquery', function() { }); it('empty options', async() => { const m = mquery().collection(col); - const res = await m.findOneAndUpdate({ name: name }, { works: false }, {}); + const res = await m.findOneAndUpdate({ name: name }, { works: false }, { returnDocument: 'after' }); assert.ok(res); assert.equal(name, res.name); - assert.ok(true === res.works); + assert.strictEqual(res.works, false); + }); + }); + }); + + describe('findOneAndReplace', function() { + let name = 'findOneAndReplace + fn'; + + validateFindAndModifyOptions('findOneAndReplace'); + + beforeEach(function() { + return mquery().collection(col).updateOne({ name }, { name }, { upsert: true }); + }); + + afterEach(function () { + return mquery().collection(col).deleteMany(); + }); + + describe('with 0 args', function() { + it('makes no changes', function() { + const m = mquery(); + const n = m.findOneAndReplace(); + assert.deepEqual(m, n); + }); + }); + + describe('with 1 arg', function() { + describe('that is an object', function() { + it('replaces the doc', function() { + const m = mquery(); + const n = m.findOneAndReplace({ name: '1 arg', age: 10 }); + assert.deepStrictEqual(n._conditions, { name: '1 arg', age: 10 }); + assert.strictEqual(n._updateDoc, undefined); + }); + }); + describe('that is a query', function() { + it('replaces the doc', function() { + const m = mquery({ name: name }).updateOne(null, { x: 1 }); + const n = mquery().findOneAndReplace(m); + assert.deepEqual(n._updateDoc, { x: 1 }); + }); + }); + }); + + describe('with 2 args', function() { + it('conditions + replacement', function() { + const m = mquery().collection(col); + m.findOneAndReplace({ name: name }, { name: 'replaced', age: 100 }); + assert.deepEqual({ name: name }, m._conditions); + assert.deepEqual({ name: 'replaced', age: 100 }, m._updateDoc); + }); + it('query + replacement', function() { + const n = mquery({ name: name }); + const m = mquery().collection(col); + m.findOneAndReplace(n, { name: 'replaced', age: 100 }); + assert.deepEqual({ name: name }, m._conditions); + assert.deepEqual({ name: 'replaced', age: 100 }, m._updateDoc); + }); + it('replacement + exec', async() => { + await col.insertOne({ name: name }); + const m = mquery().collection(col).where({ name: name }); + const res = await m.findOneAndReplace({}, { name: 'replaced', age: 101 }, { returnDocument: 'after', includeResultMetadata: true }); + assert.ok(res.value); + assert.equal(res.value.name, 'replaced'); + assert.equal(res.value.age, 101); + }); + }); + + describe('with 3 args', function() { + it('conditions + replacement + options', function() { + const m = mquery().collection(col); + const n = m.findOneAndReplace({ name: name }, { name: 'replaced', works: true }, { returnDocument: 'before' }); + assert.deepEqual({ name: name }, n._conditions); + assert.deepEqual({ name: 'replaced', works: true }, n._updateDoc); + assert.deepEqual({ returnDocument: 'before', overwrite: true }, n.options); + }); + it('conditions + replacement + exec', async() => { + await col.insertOne({ name: name }); + const m = mquery().collection(col); + const res = await m.findOneAndReplace({ name: name }, { name: 'replaced', works: true }, { returnDocument: 'after', includeResultMetadata: true }); + assert.ok(res.value); + assert.equal(res.value.name, 'replaced'); + assert.ok(true === res.value.works); + }); + it('empty options', async() => { + await col.insertOne({ name: name }); + const m = mquery().collection(col); + const res = await m.findOneAndReplace({ name: name }, { name: 'replaced', works: false }, { returnDocument: 'after' }); + assert.ok(res); + assert.equal(res.name, 'replaced'); + assert.ok(false === res.works); }); }); }); From 7200d8540b765b382ca15325a801306ed4a72c56 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 15:11:31 -0400 Subject: [PATCH 08/10] Update .github/workflows/test.yml Co-authored-by: Aaron Heckmann --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb95f20..0040ff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - node: [20, 22] + node-version: ['lts/*', 'lts/-1', 'latest'] mongo: [7.0, 8.0] services: mongodb: From 4b9b3aff7b98b2a95a5852154975efa993ee6ad5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 9 Sep 2025 15:17:09 -0400 Subject: [PATCH 09/10] Update .github/workflows/test.yml Co-authored-by: Aaron Heckmann --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0040ff7..ed3e347 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: ['lts/*', 'lts/-1', 'latest'] + node: ['lts/*', 'lts/-1', 'latest'] mongo: [7.0, 8.0] services: mongodb: From fe47b50e4d2f884e254297f516fba1f5bd4809d9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Nov 2025 10:30:31 -0500 Subject: [PATCH 10/10] fix workflows, use node debuglog instead of debug --- .github/workflows/test.yml | 2 +- lib/mquery.js | 15 +++++++++++++++ package.json | 2 +- test/index.js | 8 ++++---- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed3e347..e913ab9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - node: ['lts/*', 'lts/-1', 'latest'] + node: ['20', '22', '24'] mongo: [7.0, 8.0] services: mongodb: diff --git a/lib/mquery.js b/lib/mquery.js index 13aa678..3aae9f4 100644 --- a/lib/mquery.js +++ b/lib/mquery.js @@ -8,6 +8,8 @@ const assert = require('assert'); const util = require('util'); const utils = require('./utils'); +const debug = util.debuglog('mquery'); + /** * Query constructor used for building queries. * @@ -1833,6 +1835,8 @@ Query.prototype._find = async function _find() { options.fields = this._fieldsForExec(); } + debug('find', this._collection.collectionName, conds, options); + return this._collection.find(conds, options); }; @@ -1867,6 +1871,8 @@ Query.prototype.cursor = function cursor(criteria) { options.fields = this._fieldsForExec(); } + debug('findCursor', this._collection.collectionName, conds, options); + return this._collection.findCursor(conds, options); }; @@ -1910,6 +1916,8 @@ Query.prototype._findOne = async function _findOne() { options.fields = this._fieldsForExec(); } + debug('findOne', this._collection.collectionName, conds, options); + return this._collection.findOne(conds, options); }; @@ -1951,6 +1959,8 @@ Query.prototype._countDocuments = async function _countDocuments() { const conds = this._conditions; const options = this._optionsForExec(); + debug('countDocuments', this._collection.collectionName, conds, options); + return this._collection.countDocuments(conds, options); }; @@ -1982,6 +1992,7 @@ Query.prototype.estimatedDocumentCount = function() { Query.prototype._estimatedDocumentCount = async function _estimatedDocumentCount() { const conds = this._conditions; const options = this._optionsForExec(); + debug('estimatedDocumentCount', this._collection.collectionName, conds, options); return this._collection.estimatedDocumentCount(conds, options); }; @@ -2165,6 +2176,7 @@ async function _updateExec(query, op) { const criteria = query._conditions; const doc = query._updateForExec(); + debug(op, query._collection.collectionName, criteria, doc, options); return query._collection[op](criteria, doc, options); } @@ -2200,6 +2212,7 @@ Query.prototype._deleteOne = async function() { const conds = this._conditions; + debug('deleteOne', this._collection.collectionName, conds, options); return this._collection.deleteOne(conds, options); }; @@ -2358,6 +2371,7 @@ Query.prototype._findOneAndReplace = async function() { const replacement = this._updateForExec(); const options = this._optionsForExec(); + debug('findOneAndReplace', this._collection.collectionName, conds, replacement, options); return this._collection.findOneAndReplace(conds, replacement, options); }; @@ -2407,6 +2421,7 @@ Query.prototype._findOneAndDelete = async function() { const options = this._optionsForExec(); const conds = this._conditions; + debug('findOneAndDelete', this._collection.collectionName, conds, options); return this._collection.findOneAndDelete(conds, options); }; diff --git a/package.json b/package.json index ea4d744..121e1ed 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "url": "git://github.com/aheckmann/mquery.git" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.19.0" }, "devDependencies": { "eslint": "8.x", diff --git a/test/index.js b/test/index.js index c49c7d0..f394128 100644 --- a/test/index.js +++ b/test/index.js @@ -1984,7 +1984,7 @@ describe('mquery', function() { } describe('findOneAndUpdate', function() { - let name = 'findOneAndUpdate + fn'; + const name = 'findOneAndUpdate + fn'; validateFindAndModifyOptions('findOneAndUpdate'); @@ -1992,7 +1992,7 @@ describe('mquery', function() { return mquery().collection(col).updateOne({ name }, { name }, { upsert: true }); }); - afterEach(function () { + afterEach(function() { return mquery().collection(col).deleteMany(); }); @@ -2066,7 +2066,7 @@ describe('mquery', function() { }); describe('findOneAndReplace', function() { - let name = 'findOneAndReplace + fn'; + const name = 'findOneAndReplace + fn'; validateFindAndModifyOptions('findOneAndReplace'); @@ -2074,7 +2074,7 @@ describe('mquery', function() { return mquery().collection(col).updateOne({ name }, { name }, { upsert: true }); }); - afterEach(function () { + afterEach(function() { return mquery().collection(col).deleteMany(); });