Skip to content

Commit 60936dc

Browse files
authored
fix: hasAtomicOperator check respects toBSON transformation (#2696)
Certain documents cannot contain atomic operators i.e. keys with a leading dollar sign, documents can contain toBSON transformation functions that would modify such keys the check now respect the transformation NODE-2741
1 parent 7e89e47 commit 60936dc

File tree

3 files changed

+82
-2
lines changed

3 files changed

+82
-2
lines changed

lib/utils.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -809,8 +809,11 @@ function hasAtomicOperators(doc) {
809809
return doc.reduce((err, u) => err || hasAtomicOperators(u), null);
810810
}
811811

812-
const keys = Object.keys(doc);
813-
return keys.length > 0 && keys[0][0] === '$';
812+
return (
813+
Object.keys(typeof doc.toBSON !== 'function' ? doc : doc.toBSON())
814+
.map(k => k[0])
815+
.indexOf('$') >= 0
816+
);
814817
}
815818

816819
module.exports = {

test/functional/bulk.test.js

+63
Original file line numberDiff line numberDiff line change
@@ -1662,4 +1662,67 @@ describe('Bulk', function() {
16621662
);
16631663
});
16641664
});
1665+
1666+
it('should enforce no atomic operators', function() {
1667+
const client = this.configuration.newClient();
1668+
return client
1669+
.connect()
1670+
.then(() => {
1671+
const collection = client.db().collection('noAtomicOp');
1672+
return collection
1673+
.drop()
1674+
.catch(ignoreNsNotFound)
1675+
.then(() => collection);
1676+
})
1677+
.then(collection => {
1678+
return collection.insertMany([{ a: 1 }, { a: 1 }, { a: 1 }]).then(() => collection);
1679+
})
1680+
.then(collection => {
1681+
try {
1682+
return collection.replaceOne({ a: 1 }, { $atomic: 1 });
1683+
} catch (err) {
1684+
expect(err).to.be.instanceOf(
1685+
TypeError,
1686+
'Replacement document must not use atomic operators'
1687+
);
1688+
}
1689+
})
1690+
.finally(() => {
1691+
return client.close();
1692+
});
1693+
});
1694+
1695+
it('should respect toBSON conversion when checking for atomic operators', function() {
1696+
const client = this.configuration.newClient();
1697+
return client
1698+
.connect()
1699+
.then(() => {
1700+
const collection = client.db().collection('noAtomicOp');
1701+
return collection
1702+
.drop()
1703+
.catch(ignoreNsNotFound)
1704+
.then(() => collection);
1705+
})
1706+
.then(collection => {
1707+
return collection.insertMany([{ a: 1 }, { a: 1 }, { a: 1 }]).then(() => collection);
1708+
})
1709+
.then(collection => {
1710+
try {
1711+
return collection.replaceOne(
1712+
{ a: 1 },
1713+
{
1714+
$atomic: 1,
1715+
toBSON() {
1716+
return { atomic: this.$atomic };
1717+
}
1718+
}
1719+
);
1720+
} catch (err) {
1721+
expect.fail(); // shouldn't throw any error
1722+
}
1723+
})
1724+
.finally(() => {
1725+
return client.close();
1726+
});
1727+
});
16651728
});

test/unit/utils.test.js

+14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const eachAsync = require('../../lib/core/utils').eachAsync;
33
const makeInterruptableAsyncInterval = require('../../lib/utils').makeInterruptableAsyncInterval;
44
const now = require('../../lib/utils').now;
5+
const hasAtomicOperators = require('../../lib/utils').hasAtomicOperators;
56
const expect = require('chai').expect;
67
const sinon = require('sinon');
78

@@ -163,4 +164,17 @@ describe('utils', function() {
163164
this.clock.tick(250);
164165
});
165166
});
167+
168+
it('should assert hasAtomicOperators and respect toBSON conversion', function() {
169+
expect(hasAtomicOperators({ $key: 2.3 })).to.be.true;
170+
expect(hasAtomicOperators({ nonAtomic: 1, $key: 2.3 })).to.be.true;
171+
expect(
172+
hasAtomicOperators({
173+
$key: 2.3,
174+
toBSON() {
175+
return { key: this.$key };
176+
}
177+
})
178+
).to.be.false;
179+
});
166180
});

0 commit comments

Comments
 (0)