Mongoose update sub document if exists - node.js

I have following model :
var VoteSchema = new Schema({
up : Boolean
, createdBy:{type:ObjectId, ref:'users'}
, createdOn : {type:Date, default:Date.now}
});
var QuestionSchema = newSchema({
title:String
, description:String
, votes : [VoteSchema]
, createdBy:{type:ObjectId, ref:'users'}
, createdOn : {type:Date, default:Date.now}
});
var Question = mongoose.model('questions',QuestionSchema);
Suppose user1 is logged in user and question1 is current / viewing question.The user can upvote({up:true}) or downvote({up:false}) a question at any time. How can I add a new vote if a user1 have not casted a vote for question1 else update the vote.
I have been able to write the following lines of code:
QuestionSchema.statics.castVote = function(questionId, vote) {
//NOTE : vote.createdBy equalsto loggedInUserID
Q.nbind(Question.findOne, Question)({
$and:[
{_id:questionId},
{'votes.createdBy':vote.createdBy}
]
}).then(function(doc) {
if(doc) {
//I am clue less
//doc.votes is a list of votes for this question
// how can I get the particular vote casted by the user - vote.createdBy
}else {
//Question.votes.push(vote);
}
});
});

So you're part of the way there, but of course when you don't find a doc then you will not have a doc to work with in the callback. MongoDB has native ways of handling these sorts of updates, but of course you do need to test for the match as you are.
What we can do here is just work within the true or false condition of where the document exists.
Considering vote.value to be your true or false for the "upvote"
Where you do find that there is a matching document you can issue an update like this:
Question.update(
{
_id: questionId,
"votes.createdBy" vote.createdBy,
"votes.up": {"$ne": vote.value }
},
{ $set: { "votes.$.up": vote.value } }
);
So that matches and uses a positional $ operator to make sure the correct index of the matching item is updated. What I added there makes sure that you don't even touch the document where the vote.vaule is already of the same value.
And in the false condition you want to $push onto the array with the new item:
Question.update(
{
_id: questionId
},
{
$push: {
"votes": {
"up": vote.value,
"createdBy": vote.createdBy
}
}
}
);
Of course add the callback details where writeConcern is applied, which you probably do.

Related

How to update multiple fields (maxLength, maxBreadth or maxArea) of a document based on multiple conditions sent from req.body?

I want to store maximum length, breadth, area ever encountered from a request and store it in the database. In this case, anything can be maximum and based on that I want to update max values in the database for that particular field only.
const body = {
oID: 123, // Primary key
length: 50,
breadth: 50
};
const { length, breadth } = body;
const currentArea = length * breadth;
await totalArea.updateOne(
{ oID: 123 },
{
$set: { $maxLength: maxLength < length ? length : $maxLength }, // I want to update the docs based on mongo query only since this won't work.
$set: { $maxBreadth: maxBreadth < breadth ? breadth : $maxBreadth }, // I want to update the docs based on mongo query only since this won't work.
$set: { $maxArea: maxArea < currentArea ? currentArea : $maxArea } // I want to update the docs based on mongo query only since this won't work.
},
{ upsert: true }
);
In the above example, I have demonstrated the logic using ternary operator and I want to perform the same operation using mongoDB query. i.e update a particular field while comparing it to existing fields from the database and update it if it satisfies the condition.
await totalArea.updateOne(
{ oID: 123
},
{
$max: {
maxLength: length,
maxBreadth: breadth,
maxArea : currentArea
}
},
{ upsert: true }
);
I found this to be working correctly. Thanks to #Mehari Mamo's comment.
If I understand correctly, you want something like this:
db.collection.update({
oID: 123
},
[{$set: {maxLength: {$max: [length, "$maxLength"]},
maxBreadth: {$max: [breadth, "$maxBreadth"]},
maxArea : {$max: [currentArea, "$maxArea "]}
}
}
],
{
upsert: true
})
You can check it here .
The [] on the second step allows you to access the current values of the document fields, as this is an aggregation pipeline.

Mongoose: create multiple documents if filter finds none

I'm trying to create multiple documents based on a filter: if document not found => create it.
After searching a bit I found that the correct way to do so is by using updateMany and setting upsert: true (docs).
This made somewhat sense from the documentation's example but as I understand it the filter modifier would be used for the newly-created document. As in the example:
try {
db.inspectors.updateMany(
{ "Sector" : { $gt : 4 }, "inspector" : "R. Coltrane" },
{ $set: { "Patrolling" : false } },
{ upsert: true }
);
} catch (e) {
print(e);
}
"Inspector" : "R. Coltrane" would be inserted to the newly-created document.
But what if my setOnInsert modifier contains the same field as the one in the filter?
What my code looks like:
//first find the already-created tags
await tagModel.find({"tagName": tags}).select('tagName -_id').exec()
.then(async (result: Tag[])=>{
//create the new tags
const newTags = tags.map((tag: any)=>new tagModel({tagName: tag}));
//now insert only the new tags, filtering out the already-created tags ("result")
await tagModel.updateMany(
{"tagName": result},
{$setOnInsert: newTags} ,
{upsert: true},
(err:any, res:any)=>{
...
At first, result is an empty Array ([]). What is created in my MongoDB database is a new Tag document, but its tagName is the result object. Meaning, it looks like:
{
"_id": {
"$oid": "61659c92c6267fe11963b236"
},
"tagName": {
"$in": []
}
}
So essentially my question is, what am I suppose to do in this case where my update modifier should replace my filter query? Perhaps it's just something bad in my code that makes the updateMany function to malfunction? Or should I replace it with a different function?

I want to update sub document in array

I now use mongooses to pull and pull subdocuments to the array, and now I want to change the contents of the detail field of that subdocument with the _id of the subdocument.
{
subDocument: [{
_id: ObjectId('123'),
detail: 'I want update this part'
}]
}
I tried to use the $set method as shown below but it did not work as expected.
Model.findByIdAndUpdate(uid, { $Set: {subDocument: {_id: _id}}});
Looking at the for statement as shown below is likely to have a bad effect on performance. So I want to avoid this method.
const data = findById(uid);
for(...) {
if(data.subDocument[i]._id==_id) {
data.subDocument[i].detail = detail
}
}
Can you tell me some mongodb queries that I can implement?
And, Is it not better to use the 'for(;;)' statement shown above than to search using mongodb's query?
This should work:
Model.findOneAndUpdate({"subdocument._id": uid},
{
$set: {
"subdocument.$.detail ": "detail here"
}
},
).exec(function(err, doc) {
//code
});
To find subdocument by id, I am using something like this :
var subDocument = data.subDocument.id(subDocumentId);
if (subDocument) {
// Do some stuff
}
else {
// No subDocument found
}
Hope it helps.

String + autoincrement number on mongoose

I want to create a String + number every time a value is inserted on database (the number must be autoincrement).
Is it possible to do in Schema? Or do I need to do that before the value's inserted on database?
'question' -> String followed by an autoincrement number
var random = [{
question: 'question1'
},{
question: 'question2'
},{
question: 'question3'
},{
question: 'question4'
}];
const RandomSchema = new Schema({
question: {
type: String,
unique: true
}
})
Autoincrement fields in Mongodb, don't work exactly the same way that they do in an RDBMS. There is a lot more overhead involved. Never the less, creating an auto field is a solved problem. There is also a third party mongoose-autoincrement package that make it a lot easier.
Extending, from that. Your problem is a solved problem too. Because
the string part will always be the same
Simply use string concatenation to prepend 'question' to the auto increment field at display time.
Here is what I implemented with one of the approaches #e4c5 pointed out, without the use of a third-party package.
Define add.js as below:
db.counters.insert(
{
_id: "itemid",
seq: 0
}
)
function getNextSequence(id) {
var ret = db.counters.findAndModify(
{
query: { _id: id },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
db.randoms.insert(
{
question: "question"+getNextSequence("itemid"),
}
)
db.randoms.insert(
{
question: "question"+getNextSequence("itemid"),
}
)
After starting a mongod instance, do mongo < add.js.

How to update a field using its previous value in MongoDB/Mongoose

For example, I have some documents that look like this:
{
id: 1
name: "foo"
}
And I want to append another string to the current name field value.
I tried the following using Mongoose, but it didn't work:
Model.findOneAndUpdate({ id: 1 }, { $set: { name: +"bar" } }, ...);
Edit:
From Compatibility Changes in MongoDB 3.6:
MongoDB 3.6.1 deprecates the snapshot query option.
For MMAPv1, use hint() on the { _id: 1} index instead to prevent a cursor from returning a document more than once if an intervening write operation results in a move of the document.
For other storage engines, use hint() with { $natural : 1 } instead.
Original 2017 answer:
You can't refer to the values of the document you want to update, so you will need one query to retrieve the document and another one to update it. It looks like there's a feature request for that in OPEN state since 2016.
If you have a collection with documents that look like:
{ "_id" : ObjectId("590a4aa8ff1809c94801ecd0"), "name" : "bar" }
Using the MongoDB shell, you can do something like this:
db.test.find({ name: "bar" }).snapshot().forEach((doc) => {
doc.name = "foo-" + doc.name;
db.test.save(doc);
});
The document will be updated as expected:
{ "_id" : ObjectId("590a4aa8ff1809c94801ecd0"), "name": "foo-bar" }
Note the .snapshot() call.
This ensures that the query will not return a document multiple times because an intervening write operation moves it due to the growth in document size.
Applying this to your Mongoose example, as explained in this official example:
Cat.findById(1, (err, cat) => {
if (err) return handleError(err);
cat.name = cat.name + "bar";
cat.save((err, updatedCat) => {
if (err) return handleError(err);
...
});
});
It's worth mentioning that there's a $concat operator in the aggregation framework, but unfortunately you can't use that in an update query.
Anyway, depending on what you need to do, you can use that together with the $out operator to save the results of the aggregation to a new collection.
With that same example, you will do:
db.test.aggregate([{
$match: { name: "bar" }
}, {
$project: { name: { $concat: ["foo", "-", "$name"] }}
}, {
$out: "prefixedTest"
}]);
And a new collection prefixedTest will be created with documents that look like:
{ "_id" : ObjectId("XXX"), "name": "foo-bar" }
Just as a reference, there's another interesting question about this same topic with a few answers worth reading: Update MongoDB field using value of another field
If this is still relevant, I have a solution for MongoDB 4.2.
I had the same problem where "projectDeadline" fields of my "project" documents were Array type (["2020","12","1"])
Using Robo3T, I connected to my MongoDB Atlas DB using SRV link. Then executed the following code and it worked for me.
Initial document:
{
_id : 'kjnolqnw.KANSasdasd',
someKey : 'someValue',
projectDeadline : ['2020','12','1']
}
CLI Command:
db
.getCollection('mainData')
.find({projectDeadline: {$not: {$eq: "noDeadline"}}})
.forEach((doc) => {
var deadline = doc.projectDeadline;
var deadlineDate = new Date(deadline);
db
.mainData
.updateOne({
_id: doc._id},
{"$set":
{"projectDeadline": deadlineDate}
}
)}
);
Resulting document:
{
_id : 'kjnolqnw.KANSasdasd',
someKey : 'someValue',
projectDeadline : '2020-12-01 21:00:00.000Z'
}

Resources