MongoDB Node.js native driver silently swallows `bulkWrite` exception - node.js

The script below has a bug in the mongo bulkWrite op syntax: $setOnInsert: { count:0 }, is unnecessary and thus mongo throws an exception: "Cannot update 'count' and 'count' at the same time".
The problem is, the node.js driver doesn't seem to catch it. This script logs "Success!" to the console.
(async () => {
let db = await require('mongodb').MongoClient.connect('mongodb://localhost:27017/myNewDb');
let mongoOps = [{
updateOne: {
filter: { foo: "bar" },
update: {
$setOnInsert: { count:0 },
$inc: { count:1 },
},
upsert: true,
}
}];
try {
await db.collection("myNewCollection").bulkWrite(mongoOps);
console.log("Success!");
} catch(e) {
console.log("Failed:");
console.log(e);
}
})();
Examining db.system.profile.find({}) with db.setProfileLevel(2) we can see the exception:
{
"op" : "update",
"ns" : "myNewDb.myNewCollection",
"query" : {
"foo" : "bar"
},
"updateobj" : {
"$setOnInsert" : {
"count" : 0
},
"$inc" : {
"count" : 1
}
},
"keyUpdates" : 0,
"writeConflicts" : 0,
"numYield" : 0,
"locks" : {
"Global" : {
"acquireCount" : {
"r" : NumberLong(1),
"w" : NumberLong(1)
}
},
"Database" : {
"acquireCount" : {
"w" : NumberLong(1)
}
},
"Collection" : {
"acquireCount" : {
"w" : NumberLong(1)
}
}
},
"exception" : "Cannot update 'count' and 'count' at the same time",
"exceptionCode" : 16836,
"millis" : 0,
"execStats" : {},
"ts" : ISODate("2017-10-12T01:57:03.008Z"),
"client" : "127.0.0.1",
"allUsers" : [],
"user" : ""
}
Why is the driver swallowing errors like this? I definitely seems like a bug, but I figured I'd ask here first just to be sure.

So as commented, "It's a bug". Specifically the bug is right here:
// Return a Promise
return new this.s.promiseLibrary(function(resolve, reject) {
bulkWrite(self, operations, options, function(err, r) {
if(err && r == null) return reject(err);
resolve(r);
});
});
The problem is that the "response" ( or r ) in the callback which is being wrapped in a Promise is not actually null, and therefore despite the error being present the condition is therefore not true and reject(err) is not being called, but rather the resolve(r) is being sent and hence this is not considered an exception.
Correcting would need some triage, but you can either 'work around' as mentioned by inspecting the writeErrors property in the response from the current bulkWrite() implementation or consider one of the other alternatives as:
Using the Bulk API methods directly:
const MongoClient = require('mongodb').MongoClient,
uri = 'mongodb://localhost:27017/myNewDb';
(async () => {
let db;
try {
db = await MongoClient.connect(uri);
let bulk = db.collection('myNewCollection').initializeOrderedBulkOp();
bulk.find({ foo: 'bar' }).upsert().updateOne({
$setOnInsert: { count: 0 },
$inc: { count: 0 }
});
let result = await bulk.execute();
console.log(JSON.stringify(result,undefined,2));
} catch(e) {
console.error(e);
} finally {
db.close();
}
})();
Perfectly fine but of course has the issue of not naturally regressing on server implementations without Bulk API support to using the legacy API methods instead.
Wrapping the Promise Manually
(async () => {
let db = await require('mongodb').MongoClient.connect('mongodb://localhost:27017/myNewDb');
let mongoOps = [{
updateOne: {
filter: { foo: "bar" },
update: {
$setOnInsert: { count:0 },
$inc: { count:1 },
},
upsert: true,
}
}];
try {
let result = await new Promise((resolve,reject) => {
db.collection("myNewCollection").bulkWrite(mongoOps, (err,r) => {
if (err) reject(err);
resolve(r);
});
});
console.log(JSON.stringify(result,undefined,2));
console.log("Success!");
} catch(e) {
console.log("Failed:");
console.log(e);
}
})();
As noted the problem lies within the implementation of how bulkWrite() is returning as a Promise. So instead you can code with the callback() form and do your own Promise wrapping in order to act how you expect it to.
Again as noted, needs a JIRA issue and Triage to which is the correct way to handle the exceptions. But hopefully gets resolved soon. In the meantime, pick an approach from above.

Related

when try to pull an object from array in mongodb acknowledged: true, modifiedCount: 0, upsertedId: null, upsertedCount: 0, matchedCount: 1

I'm trying to remove a product object from user cart when it's count reaches zero.
changeProductCount : (details) => {
return new Promise(async (resolve, reject) => {
try {
if (details.count==-1 && details.quantity==1) {
console.log();
let response = await db.get().collection(CART_COLLECTION)
.updateOne({
$and: [
{ _id: ObjectId(details.cart) },
{ 'products.time': parseInt(details.time) }
]
}, {
$pull : {
products : { item : ObjectId(details.item) }
}
});
if (response) {
console.log(response);
resolve({ removeProduct: true })
}
} else {
let response = await db.get().collection(CART_COLLECTION)
.updateOne({
_id: ObjectId(details.cart),
'products.time': details.time
}, {
$inc : {
'products.$.quantity': parseInt(details.count)
}
});
if (response) {
console.log(response);
resolve({removeProduct:false})
}
}
} catch (error) {
reject(error)
}
})
}
This is my code. I'm trying to pull an object from an array from the userCart when their product count is 0.
Here, if I replace the code as this:
let response = await db.get().collection(CART_COLLECTION)
.updateOne({
_id:ObjectId(details.cart)
}, {
$pull: {
products: {
item : ObjectId(details.item)
}
}
}
);
This code is working, but the problem is, if there are two shirts with same product Id but different sizes, say Medium and Large, when medium is removed, large also gets removed. That's the reason why I added time for each objects when it is first added to cart. But, it is not working. Please help me in this problem.
This is the response that I get:
{
acknowledged: true,
modifiedCount: 0,
upsertedId: null,
upsertedCount: 0,
matchedCount: 1
}
I solved this problem. This is the code that I used.
let response = await db.get().collection(CART_COLLECTION).updateOne({_id:ObjectId(details.cart)},{$pull : {products : {time : parseInt(details.time)}}});
As the time was already a unique value, I used it to match with the array object.

how to update an object of an element in array in mongodb?

This is the structure i have, i want to update the nested array element if an object key matches for example - i want to match grnno :"10431000" and update the other keys of that object like vehicle_no,invoice_no etc.
{
"_id" : ObjectId("5f128b8aeb27bb63057e3887"),
"requirements" : [
{
"grns" : [
{
"invoice_no" : "123",
"vehicle_no" : "345",
"req_id" : "5f128c6deb27bb63057e388a",
"grnno" : "10431000"
},
{
"invoice_no" : "abc",
"vehicle_no" : "def",
"req_id" : "5f128c6deb27bb63057e388a",
"grnno" : "10431001"
}
]
}
]
}
I have tried this code
db.po_grn.update({
"requirements.grns.grnno":"10431001"
}, {
$set: {
"requirements.$.grns": {"invoice_no":"test",vehicle_no:"5455"}
}
})
But this is changing the structure i have like this
"requirements" : [
{
"grns" : {
"invoice_no" : "test",
"vehicle_no":"5455"
},
"req_id" : ObjectId("5f128b8aeb27bb63057e3886")
}
],
grns key should be array, and update should be of the particular object which matches the key "grnno". Please help me out. Thanks.
==Edit==
var grnno = req.body.grnno;
db.po_grn.find({
"requirements.grns.grnno":grnno
}).toArray(function(err, po_grn) {
console.log("po_grn",po_grn);
if (po_grn.length > 0) {
console.log("data.grn.grnno ", grnno);
var query = {
requirements: {
$elemMatch: {
"grns.grnno": grnno
}
}
};
var update = {
$set: {
'requirements.$[].grns.$[inner].invoice_no': data.invoice_no,
'requirements.$[].grns.$[inner].vehicle_no': data.vehicle_no,
}
};
var options = {
arrayFilters: [
{ "inner.grnno" : grnno }
]
};
db.po_grn.update(query, update, options
, function(er, grn) {
console.log("grn",grn,"er",er)
res.send({
status: 1,
message: "Grn updated successfully"
});
}
);
} else {
res.send({
status: 0,
message: "Grn not found "
});
}
})
Use a combination of $[] positional-all operator with array filters to update your inner nested document.
var query = {
requirements: {
$elemMatch: {
"grns.grnno": "10431001"
}
}
};
var update = {
$set: {
'requirements.$[].grns.$[inner].invoice_no': "test",
'requirements.$[].grns.$[inner].vehicle_no': "5455",
}
};
var options = {
arrayFilters: [
{ "inner.grnno" : "10431001" }
]
};
db.collection.update(query, update, options);
Update -
NodeJS native MongoDb driver code attached, which is working fine
const { MongoClient } = require('mongodb');
const url = "mongodb://localhost:27017/";
MongoClient.connect(url, function(err, db) {
if (err) {
throw err;
}
const dbo = db.db("test");
(async() => {
const query = {
requirements: {
$elemMatch: {
"grns.grnno": "10431001"
}
}
};
const update = {
$set: {
'requirements.$[].grns.$[inner].invoice_no': "test",
'requirements.$[].grns.$[inner].vehicle_no': "5455",
}
};
const options = {
arrayFilters: [
{ "inner.grnno" : "10431001" }
],
multi: true
};
try {
const updateResult = await dbo.collection("collection").update(query, update, options);
} catch (err) {
console.error(err);
}
db.close();
})();
});

How to remove object from an Array which is under an object Mongodb

I have a data set like this:
{
"_id" : ObjectId("5bacc98431481e0520856df8"),
"action" : {
"count" : 0,
"shop" : [
{
"uid" : ObjectId("5c0b396a50b8a627c4172a2b"),
},
{
"uid" : ObjectId("5c0b3b471ed11f2124e1e3a8"),
},
{
"uid" : ObjectId("5c0b3b471ed11f2124e1e3a9"),
}
]
}
}
How will I remove the above object whose uid is ObjectId("5c0b3b471ed11f2124e1e3a8") through mongodb Query?
I used with the approach which is not perfect according to me. My approach is like this:
db.CollectionName.find({_id: ObjectId("5bacc98431481e0520856df8")})
.then(data => {
if (data.length > 0) {
let xData = data[0].notifications.shop;
let xuid = ObjectId("5c0b3b471ed11f2124e1e3a8");
let filterData = xData.filter(
x => x.uid!= xuid
);
User.update(
{ _id: ObjectId("5bacc98431481e0520856df8")},
{
$set: {
action: { shop: filterData }
}
}
)
.then(usr => {
console.log("deleted successfully")
})
.catch(er => {
console.log(er)
})
}
})
.catch(error => {
console.log(error)
})
By this approach I remove the uid from an array which itself under an object. If anyone knows this type of task done through MongoDB Query then please let me know.
Any Help/Suggestion is really appreciated. Thanks in advance for the developer who attempted my Query.
The mongo statement to do this would be:
db.getCollection('YOURCOLECTION').update(
{'_id': ObjectId("5bacc98431481e0520856df8")},
{ "$pull":
{ "action.shop": { "uid": ObjectId("5c0b3b471ed11f2124e1e3a8") }}
})
You would be using $pull command in combination with the $update.

mongoDB and sails aggregate dont work with nodejs

I'm using mongodb and sails framework, Production.find({}) is working normally
but Production.aggregate([]) is returning an error
Production.aggregate() is not a function
module.exports = {
list : function(req,res) {
Production.aggregate([{
$project: {
data: { $substr: ["$pt",0,10] },
prodTempo: { $substr: ["$sis",0,10]}
}
}])
.exec(function(err,collection ){
if(err){
res.send(500,{error:"DataBase Error"});
}
res.view('list',{producao:collection});
});
}
};
As of Sails v1.0 the .native() method is deprecated in favor of getDatastore().manager.
https://sailsjs.com/documentation/reference/waterline-orm/models/native
Due to a bug with the current version of sails-mongo (v1.0.1) which doesn't support the new required cursor method I've actually switched to using Mongo View's to manage aggregate queries.
The pattern below is "supposed" to work but currently returns no results because toArray() of an aggregate() function is currently not properly supported. It returns an AggregateCursor which does not support the toArray() method.
WHAT I ENDED UP DOING
const myView = sails.getDatastore().manager.collection("view_name");
myView.find({...match/filter criteria...}).toArray((err, results) => {
if (err) {
// handle error 2
}
// Do something with your results
});
The entire Aggregate query I put into the Mongo DB View and added additional columns to support filter/match capabilities as needed. The only portion of "match" I did not place into Mongo are the dynamic fields which I use above in the find() method. That's why you need the additional fields since find() will only query the columns available in the query and not the underlying model
WHAT SHOULD HAVE WORKED
So the pattern for aggregate would now be as follows:
const aggregateArray = [
{
$project: {
data: { $substr: ['$pt', 0, 10] },
prodTempo: { $substr: ['$sis', 0, 10] }
}
}
];
sails.getDatastore('name of datastore').manager.collection('collection name')
.aggregate(aggregateArray)
.toArray((err, results) => {
if (err) {
// handle error 2
}
// Do something with your results
});
For aggregations you need to call the native function first. Then it looks like this:
const aggregateArray = [
{
$project: {
data: { $substr: ['$pt', 0, 10] },
prodTempo: { $substr: ['$sis', 0, 10] }
}
}
];
Production.native(function(err, prodCollection) {
if (err) {
// handle error 1
} else {
prodCollection
.aggregate(aggregateArray)
.toArray((err, results) => {
if (err) {
// handle error 2
}
// Do something with your results
});
}
});
const regexForFileName = '.*' + fileName + '.*';
var db = model.getDatastore().manager;
var rawMongoCollection = db.collection(model.tableName);
rawMongoCollection.aggregate(
[
{
$project : {
"_id" : 0,
"fileId" : 1,
"fileName" : 1,
"fileSize" : 1,
"createdTime" : 1
}
},
{
$match : {
"fileName" : {
$regex: regexForFileName,
$options: 'i'
}
}
},
{
$sort: {
"createdTime" : -1
}
},
{
$skip: pageNumber * numberOfResultsPerPage
},
{
$limit: numberOfResultsPerPage
}
]
).toArray((err, results) => {
if (err) {
console.log(err);
}
console.log("results: " + JSON.stringify(results));
});

$ projection in mongoDB findOneAndUpdate()

I'm trying to build a simple task queue with express and mongoose. The idea is acquire a single client and return campaign id and client id (which is a subdocument of campaign). Each time someone acquires a client, its status code is set to 1. I've come up with the following query:
router.post('/lease', (err, res) => {
Campaign.findOneAndUpdate({'isEnabled': true, 'clients.contact_status_code': 0}, {
'$set': { 'clients.$.contact_status_code': 1 },
},
{
new: true,
projection: {
'clients.$': true,
},
},
(err, campaign) => {
if (err) {
return res.send(err);
}
res.json(campaign);
}
);
});
But all i'm getting after connecting to this endpoint is this:
{"_id":"591483241a84946a79626aef","clients":[{},{}]}
It seems to me that the problem is with the $ projection, but I have no idea how to fix this.
EDIT: I tried using the following code, utilizing $elemMatch:
router.post('/lease', (err, res) => {
Campaign.findOneAndUpdate({'isEnabled': true, 'clients.contact_status_code': 0}, {
'$set': { 'clients.$.contact_status_code': 1 },
},
{
new: true,
projection: {
clients: {
'$elemMatch': {contact_status_code: 1},
}
},
},
(err, campaign) => {
if (err) {
return res.send(err);
}
res.json(campaign);
}
);
});
Unfortunately, each request yields the first subdocument in the collection, that matched the criteria -- not specifically the one that was updated. Here is an example:
Say, i have the following document in mongo:
{
"_id" : ObjectId("591493d95d48e2738b0d4317"),
"name" : "asd",
"template" : "{{displayname}}",
"isEnabled" : true,
"clients" : [
{
"displayname" : "test",
"_id" : ObjectId("591493d95d48e2738b0d4319"),
"contact_status_code" : 0
},
{
"displayname" : "client",
"_id" : ObjectId("591493d95d48e2738b0d4318"),
"contact_status_code" : 0
}
],
"__v" : 0
}
I run the query for the first time and get the following result:
{"_id":"591493d95d48e2738b0d4317","clients":[{"displayname":"test","_id":"591493d95d48e2738b0d4319","contact_status_code":1}]}
Notice client id "591493d95d48e2738b0d4319" -- this time it runs as expected. But when i run the same query the second time, I get absolutely the same object, although I expect to get one with id "591493d95d48e2738b0d4318".
The issue was with new: true
Here is a working example:
Campaign.findOneAndUpdate({'isEnabled': true, 'clients.contact_status_code': 0}, {
'$set': { 'clients.$.contact_status_code': 1 },
},
{
//new: true <-- this was causing the trouble
projection: {
clients: {
'$elemMatch': {contact_status_code: 0}, // 0 because the old record gets matched
},
},
},
(err, campaign) => {
if (err) {
return res.send(err);
}
res.json(campaign);
}
);
I assume, when the new:true is set, mongo loses the matching context. This approach returns the old record, unfortunately, but that still serves my needs to get the _id.
Seems to me you are getting the campaign id, but you also want clients.$, have you tried clients.$._id?
Update for node MongoDB 3.6 driver
unlike findAndModify the function findOneAndUpdate doesn't have the option new in its options list
But you can use instead returnDocument
returnDocument accept before OR after
When set to after,returns the updated document rather than the original
When set to before,returns the original document rather than the updated.
The default is before.
returnOriginal is Deprecated Use returnDocument instead
Here is a working example:
const filter={'isEnabled': true, 'clients.contact_status_code': 0}
const update={'$set': { 'clients.$.contact_status_code': 1 }}
const options={
returnDocument: 'after' //returns the updated document
projection: {
clients: {
'$elemMatch': {contact_status_code: 0},
// 0 because the old record gets matched
},
},
}
Campaign.findOneAndUpdate(filter,update, options
,(err, campaign) => {
if (err) {
return res.send(err);
}
res.json(campaign);
}
);
findOneAndUpdate Node.js MongoDB Driver API 3.6 documentation

Resources