FindAndUpdate How to check if document was really updated - javascript
Imagine the following model:
var Office =
{
id: 1,
name: "My Office",
branches:
[
{
adddress: "Some street, that avenue",
isPrincipal: true,
},
{
adddress: "Another address",
isPrincipal: false,
},
]
}
I'd like to remove a branch, but we can't let the user remove the principal branch from an office. So here's my function:
remove: function(body)
{
return new Promise(function(resolve, reject)
{
return Office.findByIdAndUpdate(1, { $pull: {'branches': {_id: body.branch.id}}}, { new: true })
.then(function(updatedOffice){
resolve(updatedOffice)
})
.catch(function(error){
reject(error);
});
})
}
I have some doubts here:
As you can see I haven't included the another WHERE on the isPrincipal property, this is because I don't know how can I determine whether the office object got actually changed. Because the object will alway be retrieved but... How can I know this for sure?
Is FindByIdAndUpdate the best approach considering we can't let a user delete the principal branch, and if he's trying to do so, we have to show a warning.
The only real reliable way to see if an update was applied for something like a $pull is to basically check the returned document and see if the data you intended to $pull is still in there or not.
That's for any of the "findAndUpdate" variety of actions, and there is a valid reason for that as well as it also being the case that a plain .update() will actually "reliably" tell you if the modification was in fact made.
To walk through the cases:
Check the Returned Content
This basically involves looking at the array in the returned document in order to see if what we asked to remove is actually there:
var pullId = "5961de06ea264532c684611a";
Office.findByIdAndUpdate(1,
{ "$pull": { "branches": { "_id": pullId } } },
{ "new": true }
).then(office => {
// Check if the supplied value is still in the array
console.log(
"Still there?: %s",
(office.branches.find( b => b._id.toHexString() === pullId))
? true : false
);
}).catch(err => console.error(err))
We use .toHexString() in order to compare the actual value from an ObjectId since JavaScript just does not do "equality" with "Objects". You would check on both "left" and "right" if supplying something that was already "cast" to an ObjectId value, but in this case we know the other input is a "string".
Just use .update(), "It's reliable"
The other case here to consider brings into question if you "really need" the returned modified data anyway. Because the .update() method, will reliably return a result telling you if anything was actually modified:
Office.update(
{ "_id": 1 },
{ "$pull": { "branches": { "_id": pullId } } },
).then(result => {
log(result);
}).catch(err => console.error(err))
Where result here will look like:
{
"n": 1,
"nModified": 1, // <--- This always tells the truth, and cannot lie!
"opTime": {
"ts": "6440673063762657282",
"t": 4
},
"electionId": "7fffffff0000000000000004",
"ok": 1
}
And in which the nModified is a "true" indicator of whether something "actually updated". Therefore if it's 1 then the $pull actually had an effect, but when 0 nothing was actually removed from the array and nothing was modified.
This is because the method actually uses the updated API, which does have reliable results indicating actual modifications. The same would apply to something like a $set which did not actually change the value because the the value supplied was equal to what already existed in the document.
findAndModify Lies!
The other case here you might think of when looking closely at the documentation is to actually inspect the "raw result" and see if the document was modified or not. There is actually an indicator in the specification for this.
The problem is ( as well as requiring more work with Promises ) that the result is not actually truthful:
var bogusId = "5961de06ea264532c684611a"; // We know this is not there!
Promise((resolve,reject) => {
Office.findByIdAndUpdate(1,
{ "$pull": { "branches": { "_id": bogusId } } },
{ "new": true, "passRawResult" },
(err,result,raw) => { // We cannot pass multiple results to a Promise
if (err) reject(err);
resolve({ result, raw }); // So we wrap it!
}
)
})
.then(response => log(response.raw))
.catch(err => console.error(err));
The problem here is that even when we "know" this should not modify, the response says otherwise:
{
"lastErrorObject": {
"updatedExisting": true,
"n": 1 // <--- LIES! IT'S ALL LIES!!!
},
"value": {
"_id": 1,
"name": "My Office",
"branches": [
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961de06ea264532c6846118"
}
],
"__v": 0
},
"ok": 1,
"_kareemIgnore": true
}
So even after all that work to get the "third" argument out of the callback response, we still did not get told the correct information about the update.
Concluding
So if you want to "reliably" do this with a single request ( and you cannot reliably do that with multiple requests since the document could change in between! ) then your two options are:
Check the returned document to see if the data you wanted to remove is still there.
Forget returning a document and trust that .update() always tells you the "truth" ;)
Which one of these you use depends on the application usage pattern, but those are the two different ways of returning an "reliable" result.
Bit of a Listing
So just to be sure, here's a listing that goes through all the examples and demonstrates what they actually return:
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/test');
const branchesSchema = new Schema({
address: String,
isPrincipal: Boolean
});
const officeSchema = new Schema({
_id: Number,
name: String,
branches: [branchesSchema]
},{ _id: false });
const Office = mongoose.model('Office', officeSchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
const testId = "5961a56d3ffd3d5e19c61610";
async.series(
[
// Clean data
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Insert some data and pull
(callback) =>
async.waterfall(
[
// Create and demonstrate
(callback) =>
Office.create({
_id: 1,
name: "My Office",
branches: [
{
address: "Some street, that avenue",
isPrincipal: true
},
{
address: "Another address",
isPrincipal: false
},
{
address: "Third address",
isPrincipal: false
}
]
},callback),
// Demo Alternates
(office,callback) =>
async.mapSeries(
[true,false].map((t,i) => ({ t, branch: office.branches[i] })),
(test,callback) =>
(test.t)
? Office.findByIdAndUpdate(office._id,
{ "$pull": { "branches": { "_id": test.branch._id } } },
{ "new": true , "passRawResult": true },
(err,result,raw) => {
if (err) callback(err);
log(result);
log(raw);
callback();
})
: Office.findByIdAndUpdate(office._id,
{ "$pull": { "branches": { "_id": test.branch._id } } },
{ "new": true } // false here
).then(result => {
log(result);
console.log(
"Present %s",
(result.branches.find( b =>
b._id.toHexString() === test.branch._id.toHexString() ))
? true : false
);
callback();
}).catch(err => callback(err)),
callback
)
],
callback
),
// Find and demonstate fails
(callback) =>
async.waterfall(
[
(callback) => Office.findOne({},callback),
(office,callback) =>
async.eachSeries([true,false],(item,callback) =>
(item)
? Office.findByIdAndUpdate(office._id,
{ "$pull": { "branches": { "_id": testId } } },
{ "new": true, "passRawResult": true },
(err,result,raw) => {
if (err) callback(err);
log(result);
log(raw);
callback();
}
)
: Office.findByIdAndUpdate(office._id,
{ "$pull": { "branches": { "_id": testId } } },
{ "new": true }
).then(result => {
console.log(result);
console.log(
"Present %s",
(result.branches.find( b =>
b._id.toHexString() === office.branches[0]._id.toHexString()))
? true : false
);
callback();
})
.catch(err => callback(err)),
callback)
],
callback
),
// Demonstrate update() modified shows 0
(callback) =>
Office.update(
{},
{ "$pull": { "branches": { "_id": testId } } }
).then(result => {
log(result);
callback();
})
.catch(err => callback(err)),
// Demonstrate wrapped promise
(callback) =>
Office.findOne()
.then(office => {
return new Promise((resolve,reject) => {
Office.findByIdAndUpdate(office._id,
{ "$pull": { "branches": { "_id": testId } } },
{ "new": true, "passRawResult": true },
(err,result,raw) => {
if (err) reject(err);
resolve(raw)
}
);
})
})
.then(office => {
log(office);
callback();
})
.catch(err => callback(err))
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
);
And the output it produces:
Mongoose: offices.remove({}, {})
Mongoose: offices.insert({ _id: 1, name: 'My Office', branches: [ { address: 'Some street, that avenue', isPrincipal: true, _id: ObjectId("5961e5211a73e8331b44d74b") }, { address: 'Another address', isPrincipal: false, _id: ObjectId("5961e5211a73e8331b44d74a") }, { address: 'Third address', isPrincipal: false, _id: ObjectId("5961e5211a73e8331b44d749") } ], __v: 0 })
Mongoose: offices.findAndModify({ _id: 1 }, [], { '$pull': { branches: { _id: ObjectId("5961e5211a73e8331b44d74b") } } }, { new: true, passRawResult: true, upsert: false, remove: false, fields: {} })
{
"_id": 1,
"name": "My Office",
"__v": 0,
"branches": [
{
"address": "Another address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d74a"
},
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d749"
}
]
}
{
"lastErrorObject": {
"updatedExisting": true,
"n": 1
},
"value": {
"_id": 1,
"name": "My Office",
"branches": [
{
"address": "Another address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d74a"
},
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d749"
}
],
"__v": 0
},
"ok": 1,
"_kareemIgnore": true
}
Mongoose: offices.findAndModify({ _id: 1 }, [], { '$pull': { branches: { _id: ObjectId("5961e5211a73e8331b44d74a") } } }, { new: true, upsert: false, remove: false, fields: {} })
{
"_id": 1,
"name": "My Office",
"__v": 0,
"branches": [
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d749"
}
]
}
Present false
Mongoose: offices.findOne({}, { fields: {} })
Mongoose: offices.findAndModify({ _id: 1 }, [], { '$pull': { branches: { _id: ObjectId("5961a56d3ffd3d5e19c61610") } } }, { new: true, passRawResult: true, upsert: false, remove: false, fields: {} })
{
"_id": 1,
"name": "My Office",
"__v": 0,
"branches": [
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d749"
}
]
}
{
"lastErrorObject": {
"updatedExisting": true,
"n": 1
},
"value": {
"_id": 1,
"name": "My Office",
"branches": [
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d749"
}
],
"__v": 0
},
"ok": 1,
"_kareemIgnore": true
}
Mongoose: offices.findAndModify({ _id: 1 }, [], { '$pull': { branches: { _id: ObjectId("5961a56d3ffd3d5e19c61610") } } }, { new: true, upsert: false, remove: false, fields: {} })
{ _id: 1,
name: 'My Office',
__v: 0,
branches:
[ { address: 'Third address',
isPrincipal: false,
_id: 5961e5211a73e8331b44d749 } ] }
Present true
Mongoose: offices.update({}, { '$pull': { branches: { _id: ObjectId("5961a56d3ffd3d5e19c61610") } } }, {})
{
"n": 1,
"nModified": 0,
"opTime": {
"ts": "6440680872013201413",
"t": 4
},
"electionId": "7fffffff0000000000000004",
"ok": 1
}
Mongoose: offices.findOne({}, { fields: {} })
Mongoose: offices.findAndModify({ _id: 1 }, [], { '$pull': { branches: { _id: ObjectId("5961a56d3ffd3d5e19c61610") } } }, { new: true, passRawResult: true, upsert: false, remove: false, fields: {} })
{
"lastErrorObject": {
"updatedExisting": true,
"n": 1
},
"value": {
"_id": 1,
"name": "My Office",
"branches": [
{
"address": "Third address",
"isPrincipal": false,
"_id": "5961e5211a73e8331b44d749"
}
],
"__v": 0
},
"ok": 1,
"_kareemIgnore": true
}
Finding and updating in two steps would better in this case, as you just said you'll have the option of warning the user.
A note on find. You have an array of objects branches. To match more than one field in find $elemMatch is needed. The query will look something like:
Office.findOne({_id: 1, "branches" : {$elemMatch: {"_id": body.branch.id, "isPrincipal": false}}})
Which will either return the office document or not. If it does, you proceed with findByIdAndUpdate (which is better than modifying and saving the already found document). If it does not, return a forbidden message to the user.
Related
Update a document in an array and return the document in mongoDB
So I have an only array in a collection, which has a name of "posts". I am trying to update a document in that array and return the updated document. I tried this: Posts.updateOne( {}, { $set : { 'posts.$[id].image' : cloudinaryUrl, 'posts.$[id].post' : req.body.updatedPost } }, { arrayFilters: [ { 'id._id': new ObjectId(req.body.postID) }, ], }, ); I'm getting this: { "data": { "acknowledged": true, "modifiedCount": 1, "upsertedId": null, "upsertedCount": 0, "matchedCount": 1 } } And also this: Posts.findOneAndUpdate( { 'posts': { $elemMatch: { creatorId: req.userData.userId, _id: new ObjectId(req.body.postID), } } }, { $set : { 'posts.$.image' : cloudinaryUrl, 'posts.$.post' : req.body.updatedPost } }, ) And I'm getting the whole collection (I'm just showing you 1 post): { "data": { "_id": "63ddd8059b4324f25f69469e", "__v": 0, "posts": [ { "post": "h123jjkkl", "image": "", "comments": [], "likes": 0, "creatorId": "63cdb85f5f2fb46f75781f7e", "date": "2023-02-04T04:36:31.982Z", "_id": "63dde0cf749dde1d574c29cf" }, ] But I can't get the updated document. Can someone help me do it?
const updatedData = await User.findOneAndUpdate( { _id: userId }, { $set: { 'posts.$[id].image' : cloudinaryUrl, 'posts.$[id].post' : req.body.updatedPost } }, { returnDocument: "after" } ); You will get the updated document, you can use a async function or can get the data using .then and avoid the await keyword
unable to load nested object data in mongoose database properly
The issue I am facing is that even though I am able to load the data in the database but the desired result is different from what is being returned. The issue is that whenever I am running for loop the qn variable is inserted twice but need it only once. Please if someone can help me resolve this issue. userProject.js router.post('/notification/check/upload/survey' , async (req,res) => { //const taginsert = []; const mcq = []; //const newData = {}; //const o = {}; console.log(req.body); for(var i=0; i<=req.body.questions.length-1;i++) { // newData = { // qn: req.body.questions[i].qn // } for(var j=0; j<=req.body.questions[i].options.length-1;j++){ const o = { qn: req.body.questions[i].qn, //{ options: [{ option: req.body.questions[i].options[j].option, totalValues: req.body.questions[i].options[j].totalValues }] } // newData.options = o; // newData.push(o); mcq.push(o); } } //mcq = newData; const check = new userCampaignSurvey({ userId: req.body.userId, status: 'Pending', notification_type:req.body.notification_type, questions: mcq }) check.save((err,p) => { if(err) { res.status(500).send(err); } res.status(201).send(p); }) }) model.js userId: { type: Number, required: true, trim: true }, notification_id: { type: Number, }, notification_type: { type: Number, required: true, }, status: { type: String, required: true }, questions: [ { qn: { type: String, required: true }, options: [ { option: { type: String }, totalViews: { type:Number } } ] }, ], created_at: { type: Date, required: true, default: Date.now(), }, }); result received { "userId": 1, "notification_type": 1, "status": "Pending", "questions": [ { "qn": "Which brand do you like better for buying shoes?", "options": [ { "option": "Adidas", "_id": "639064c9ec215f2cd1b2b15d" } ], "_id": "639064c9ec215f2cd1b2b15c" }, { "qn": "Which brand do you like better for buying shoes?", "options": [ { "option": "Reebok", "_id": "639064c9ec215f2cd1b2b15f" } ], "_id": "639064c9ec215f2cd1b2b15e" } ], "created_at": "2022-12-07T10:02:48.442Z", "_id": "639064c9ec215f2cd1b2b15b", "__v": 0 } expected result { "userId": 1, "status": "Pending", "notification_type": 1, "questions": [ { "qn": "Which brand do you like better for buying shoes?", "options": [ { "option": "Adidas", "totalViews": 20 }, { "option": "Reebok", "totalViews": 10 } ] } ] }
Script that returns _id in MongoDB with JavaScript
I need a javascript code that saves in a variable the _id I'm having trouble getting that _id because it’s inside an array. I have looked everywhere but I can't find a solution for this. { "_id": { "$oid": "626bacea1847f675e47b2bd8" }, "tipo": "conta", "data_abertura": { "$date": "2013-02-19T00:00:00Z" }, "observacoes": "Sem observações", "iban": "PT50000506515926456299903", "saldo": 1456.23, "bic": "BBPIPTPL001", "tipo_conta": "Conta Ordenado", "cartoes": [ { "_id": { "$oid": "626bacea1847f675e47b2bd7" }, "num_cartao": 4908509005925727, "nome_cartao": "João Correia", "validade": { "$date": "2025-10-03T00:00:00Z" }, "pin": 5609, "cvc": 975, "estado": "Ativo", "tipo_cartao": "Crédito" } ], "permissoes": { "levantamentos": true, "depositos": true, "pagamentos": true, "transferencias": true, "creditos": true, "acoes": true }, "titulares": [ { "$oid": "626bacea1847f675e47b2bd6" } ] } I want the cartoes _id "cartoes": [ { "_id": { "$oid": "626bacea1847f675e47b2bd7" }, I want something like this: let conta = db.banco.findOne({"tipo": "conta"}); idConta = conta._id; console.log("id: " + idConta);//id: 626bacea1847f675e47b2bd8
Edit: According to the comments, I understand you want to input num_cartao value and to get an output of the _id of this cartao. You can use an aggregation for this: const cartao_id = await db.collection.aggregate([ { $match: {cartoes: {$elemMatch: {"num_cartao": 4908509005925727}}} }, { $project: { cartoes: { $filter: { input: "$cartoes", as: "item", cond: {$eq: ["$$item.num_cartao", 4908509005925727]} } } } }, { $project: { data: {"$arrayElemAt": ["$cartoes", 0]} } }, { $project: { _id: {$toString: "$data._id"} } } ]) console.log(`cartao_id: ${cartao_id}`);//id: 626bacea1847f675e47b2bd7 That will provide you what you want, as you can see on this playground You first $match the documents with the right num_cartao, then $filter the right carto from the cartoes array, and then you format it to string.
Loopback findOrCreate creates new records even if there is an existing record
As per my understanding of the loopback documentation Persistedmodel.findOrCreate should find a model according to the query and return it, or create a new entry in the database if the model does not exist. What I have noticed in my case is that it creates a new entry irrespective of whether there is an existing entry. Not sure what I am missing. Here is my code: teams-joined.json { "name": "teamsJoined", "base": "PersistedModel", "idInjection": true, "options": { "validateUpsert": true }, "properties": { "teamID": { "type": "string", "required": true }, "playerID":{ "type":"string", "required":true } }, "validations": [], "relations": {}, "acls": [], "methods": {} } teams-joined.js let queryThatWorks = {where:{ and: [ {teamID: teamID} ] } }; let query = {where:{ and: [ {teamID: teamID}, {playerID: req.currentUser.id}, ], } }; let joinTeamsData = { teamID: teamID, playerID: req.currentUser.id, }; console.log(query.where,'query'); teamsJoined.findOrCreate(query, joinTeamsData, function(err, data, created) { console.log(data,created); }); When I cal the API multiple times, this is what I get { and: [ { teamID: 'bf36e0-93a5-11e8-a8f4-9d86f4dd79ee' }, { playerID: '5b20887bb6563419505c4590' } ] } 'query' { teamID: 'bf36e0-93a5-11e8-a8f4-9d86f4dd79ee', playerID: '5b20887bb6563419505c4590', id: 5b61798534fa410d2b1d900a } 'data' true 'created' { and: [ { teamID: 'bf36e0-93a5-11e8-a8f4-9d86f4dd79ee' }, { playerID: '5b20887bb6563419505c4590' } ] } 'query' { teamID: 'bf36e0-93a5-11e8-a8f4-9d86f4dd79ee', playerID: '5b20887bb6563419505c4590', id: 5b61798634fa410d2b1d900b } 'data' true 'created' I expected it to return false for created and just return the existing data. This works fine when I use the 'queryThatWorks' query from my code sample.
You don't need to use and operator in your where clause. See findOrCreate. { where: { teamID: teamID, playerID: req.currentUser.id } } You can include your data into your filter like that: { where: joinTeamsData } To return specific fields in your return statement, you can use the fields option in your query. Finally, try this: let data = { teamID: teamID, playerID: req.currentUser.id, }; let filter = { where: data }; teamsJoined.findOrCreate(filter, data, function(err, instance, created) { console.log(instance, created); } );
Mongo query search updateOne $elemmatch
I'm having trouble updating my database. I have a surveys collection and each document is as follows: { "_id": { "$oid": "5aaf4f7984521736d88db4bb" }, "title": "4242 ", "body": "4242 ", "subject": "4242 ", "recipients": [ { "email": "a#gmail.com", "responded": false, "_id": { "$oid": "5ab04084be3c1529bcbdcd6e" } }, { "email": " b#gmail.com", "responded": false, "_id": { "$oid": "5ab04084be3c1529bcbdcd6d" } }, { "email": " c#gmail.com", "responded": false, "_id": { "$oid": "5ab04084be3c1529bcbdcd6c" } }, { "email": " d#gmail.com", "responded": false, "_id": { "$oid": "5ab04084be3c1529bcbdcd6b" } }, { "email": " e#gmail.com", "responded": false, "_id": { "$oid": "5ab04084be3c1529bcbdcd6a" } } ], "_user": { "$oid": "5aa5edbf8887a21af8a8db4c" }, "dateSent": { "$date": "2018-03-20T00:11:55.943Z" }, "yes": 0, "no": 0, "__v": 0 } I'm calling this on my back-end using mongoose to try and update my database whenever a user responded to a survey. const Survey = mongoose.model('surveys'); Survey.updateOne( { _id: surveyId, recipients: { $elemMatch: { email: email, responded: false } } }, { $inc: { [choice]: 1 }, $set: { 'recipients.$.responded': true } } ).exec(); But the update is only successful when the query matches the first object in the recipients array. For example, this would work. The query successfully updates the survey document and the recipient subdocument. Survey.updateOne( { _id: surveyId, recipients: { $elemMatch: { email: "a#gmail.com", responded: false } } }, { $inc: { [choice]: 1 }, $set: { 'recipients.$.responded': true } } ).exec(); But this doesn't work Survey.updateOne( { _id: surveyId, recipients: { $elemMatch: { email: "b#gmail.com", responded: false } } }, { $inc: { [choice]: 1 }, $set: { 'recipients.$.responded': true } } ).exec(); These are my schemas const surveySchema = new Schema({ title: String, body: String, subject: String, recipients: [RecipientSchema], yes: { type: Number, default: 0 }, no: { type: Number, default: 0 }, _user: { type: Schema.Types.ObjectId, ref: 'User' }, dateSent: Date, lastResponded: Date }); const recipientSchema = new Schema({ email: String, responded: { type: Boolean, default: false } }); When I use node to try to query the database manually, it also only returns the survey when the query matches the first recipient subdocument. This successfully finds the survey and returns it. Survey.find( { _id: "5aaf4f7984521736d88db4bb", recipients: { $elemMatch: { email: "a#gmail.com", responded: false } } }).then(console.log) These don't Survey.find( { _id: "5aaf4f7984521736d88db4bb", recipients: { $elemMatch: { email: "b#gmail.com", responded: false } } }).then(console.log) Survey.find( { _id: "5aaf4f7984521736d88db4bb", recipients: { $elemMatch: { email: "c#gmail.com", responded: false } } }).then(console.log) I have been trying to look up how $elemMatch works and someone told me that I can't query the properties of objects inside the recipients array, and that I can only query its IDs.
The problem is that you're storing the subsequent emails without trimming them, as you can see only the first email is properly trimmed, but the rest have a blank space at the beginning , making the query return nothing.