Update query that handles duplicates and increment operator - javascript

Background:
I have an update query on collection called alerts that runs each
time a "flow" is received.
I have an array of objects in my alert's document called
blacklistedCommunication.
What should happen:
When a new flow arrives, then the alerts doc is updated only if the flow's client_addr and server_addr are not already present in blacklistedCommuication. While at the same time, if we do find duplicates, it should only increment the flowCount.
The current query:
The below update query works to push new objects to blacklistedCommunication object if it's not present already.
However, if it is indeed present, it will not update the flowCount
How can I incorporate this logic into the query? Do I need to write a separate update query in case of duplicates?
alerts.update({
alertLevel: "orgLevelAlert",
alertCategory: "blacklistedServersViolationAlert",
alertState: "open",
'blacklistedCommunication.client': {
$ne: flow.netflow.client_addr
},
// 'blacklistedCommunication.server': {
// $ne: flow.netflow.server_addr
// }
}, {
$set: {
"misc.updatedTime": new Date()
},
$inc: {
flowCount: 1
},
$push: {
blacklistedCommunication: {
client: flow.netflow.client_addr,
server: flow.netflow.server_addr
}
}
});

You can use $addToSet instead of $push. It will ensure unique {client:*,server:*} object within blacklistedCommunication and will always update flowCount:
alerts.update({
alertLevel: "orgLevelAlert",
alertCategory: "blacklistedServersViolationAlert",
alertState: "open"
}, {
$set: {
"misc.updatedTime": new Date()
},
$inc: {
flowCount: 1
},
$addToSet: {
blacklistedCommunication: {
client: flow.netflow.client_addr,
server: flow.netflow.server_addr
}
}
});

Related

Mongo / Mongoose - Push object into document property array on condition

I have a document stored on mongo that, amongst others, contains a property type of an array with a bunch of objects inside. I am trying to push an object to that array. However each object that gets pushed to into the array contains a date object. How I need it to work is if an object with that date already exists then dont push the new object into the array.
I'm been playing with it for a while, trying to run update and elemMatch queries etc, but I can't figure it out. I have a long draw out version below, as you can see I'm making 2 separate requests to the database, can someone help me reduce this to something simpler?
UserModel.findById(req.user._id, (err, doc) => {
if (err) {
return res.sendStatus(401);
}
const dateExists = doc.entries.find((entry) => entry.date === date);
if (dateExists) {
res.sendStatus(401);
} else {
UserModel.findByIdAndUpdate(
req.user._id,
{
$push: {
entries: {
date,
memo,
},
},
},
{ safe: true, upsert: true, new: true },
(err, doc) => (err ? res.sendStatus(401) : res.sendStatus(200))
);
}
});
Thanks.
db.users.updateOne({
_id: req.user._id,
"entries.date": date,
},
{
$push: {
entries: {date,memo},
}
})
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/index.html

Can't update array inside of document - Mongoose [duplicate]

This question already has an answer here:
Update array inside of Mongo document doesn't work
(1 answer)
Closed 2 years ago.
I have a Users collection, each document has an "push_subscriptions" property. The problem is that I can't add a new element inside of it. Mongo updates "updatedAt" date but my array stays the same. I tried several approaches, but nothing worked out. My last try was with $push operator.
Here's what I tried:
const { user_id } = req.params; // Getting id normally
const subscription = req.body; // Getting subscription object normally
const updated_user = await UserModel.findByIdAndUpdate(user_id,
{
$push: {
push_subscriptions: subscription // Should add the subscription to array
}
},
{ new: true }).exec();
console.log(updated_user); // But console shows that the array is still empty
Here is the User's schema's push_subscription part:
push_subscriptions: {
type: Array,
},
Element I want to add to that array:
{"endpoint":"https://fcm.googleapis.com/fcm/send/dpH5lCsTSSM:APA91bHqjZxM0VImWWqDRN7U0a3AycjUf4O-byuxb_wJsKRaKvV_iKw56s16ekq6FUqoCF7k2nICUpd8fHPxVTgqLunFeVeB9lLCQZyohyAztTH8ZQL9WCxKpA6dvTG_TUIhQUFq_n",
"keys": {
"p256dh":"BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
"auth":"4vQK-SvRAN5eo-8ASlrwA=="
}
}
can you try it with this piece of code?
UserModel.updateOne({_id: user_id},{ $addToSet: { push_subscriptions: subscription } },async(err,raw)=>{})
and update your model as
push_subscriptions:[
{
_id: false ,
endpoint:{
type: String, required: false
},
keys:{
type: String, required: false
}
}
],
and use the command for update .
i hope it helps

MongoDB - check if $addToSet was a duplicate or not

I'm creating a like functionality for my app using MongoDB. I recently stumbled upon a problem.
My code for the liking:
async function like(articleId, userId) {
const db = await makeDb();
return new Promise((resolve, reject) => {
const result = db.collection(collectionName).findOneAndUpdate(
{ articleId: ObjectID(articleId) },
{
$setOnInsert: {
articleId: ObjectID(articleId),
creationDate: new Date()
},
$addToSet: { likes: ObjectID(userId) }
},
{ upsert: true }
);
resolve(result);
});
What it does:
If there is no document in the collection create one and fill in all the fields
If a document exists update only the likes array with a new userID
This works as expected and there are no problems, thanks to $addToSet I don't have any duplicates etc.
However after the above code gets executed and the result is returned I need to do some additional stuff and for it to work I need to know if Mongos $addToSet did change anything.
So in short:
if a new userId has been added to the likes array I want to do something
if the userId is already in the likes array and the $addToSet didnt change anything I don't want to take any action.
Is there a way to distinguish if the $addToSet did something?
The current console.log() of the result looks like this:
{
lastErrorObject: { n: 1, updatedExisting: true },
value: { _id: 5e2c47c57bb5183d80dce14f,
articleId: 5da4d1365217baf52fbcd76a,
creationDate: 2020-01-25T13:51:01.928Z,
likes: [ 5d750b677d8edfc08af8a527 ]
},
ok: 1
}
The one thing that comes to my mind (but is very bad performance-wise) is to compare the likes array before the update if it contains the userID with javascript includes method.
Any better ideas?
I found the solution!
99% of the credit goes to Neil Lunn for this answer:
https://stackoverflow.com/a/44994382/2175228
It brought me on the right track. I ditched the findOneAndUpdate method in favor of the updateOne method. The query parameters etc. everything stayed the same, only the method name changed and obviously the result type of this method.
The problem with the updateOne method is that it does not return the object that was updated (or the old one). But it does return the upsertedId which is the only thing I need in my situation.
So in the end what You get is a very long CommandResult object which starts with:
CommandResult {
result: { n: 1, nModified: 0, ok: 1 },
(...)
}
but when I looked very closely I noticed that the object has exactly those fields that I needed the whole time:
modifiedCount: 0,
upsertedId: null, // if object was inserted, this field will contain it's ID
upsertedCount: 0,
matchedCount: 1
So what You need to do is just take the parts that You need from the CommandResult object and just proceed ;)
My code after the changes:
async function like(articleId, userId) {
const db = await makeDb();
return new Promise((resolve, reject) => {
db.collection(collectionName)
.updateOne(
{ articleId: ObjectID(articleId) },
{
$setOnInsert: {
articleId: ObjectID(articleId),
creationDate: new Date()
},
$addToSet: { likes: ObjectID(userId) }
},
{ upsert: true }
)
.then(data => {
// wrap the content that You need
const result = {
upsertedId: data.upsertedId,
upsertedCount: data.upsertedCount,
modifiedCount: data.modifiedCount
};
resolve(result);
});
});
}
Hopefully this answer will help someone with a similar problem in the future :)

Mongoose: Add more items to existing object

Using Mongoose, How can I add more items to an object without replacing existing ones?
User.findOneAndUpdate(
{ userId: 0 },
{ userObjects: { newItem: value } }
);
The problem with above code is that it clears whatever was there before and replaces it with newItem when I wanted it just to add another item to userObjects(Like push function for javascript arrays).
Use dot notation to specify the field to update/add particular fields in an embedded document.
User.findOneAndUpdate(
{ userId: 0 },
{ "userObjects.newerItem": newervalue } }
);
or
User.findOneAndUpdate(
{ userId: 0 },
{ "$set":{"userObjects.newerItem": newervalue } }
);
or Use $mergeObjects aggregation operator to update the existing obj by passing new objects
User.findOneAndUpdate(
{"userId":0},
[{"$set":{
"userObjects":{
"$mergeObjects":[
"$userObjects",
{"newerItem":"newervalue","newestItem":"newestvalue"}
]
}
}}]
)
According to your question, i am guessing userObjects is an array.
You can try $push to insert items into the array.
User.findOneAndUpdate(
{ userId: 0 },
{ $push : {"userObjects": { newItem: value } }},
{safe :true , upsert : true},function(err,model)
{
...
});
For more info, read MongoDB $push reference.
Hope it helps you. If you had provided the schema, i could have helped better.
Just create new collection called UserObjects and do something like this.
UserObject.Insert({ userId: 0, newItem: value }, function(err,newObject){
});
Whenever you want to get these user objects from a user then you can do it using monogoose's query population to populate parent objects with related data in other collections. If not, then your best bet is to just make the userObjects an array.

Mongoose — Create new property with $sum using aggregation

I'm trying to add a new field in all documents that contain the sum of an array of numbers.
Here is the Schema (removed irrelevant fields for brevity):
var PollSchema = new Schema(
{
votes: [Number]
}
);
I establish the model:
PollModel = mongoose.model('Poll', PollSchema);
And I use aggregation to create a new field that contains the sum of the votes array.
PollModel.aggregate([
{
$project: {
totalVotes: { $sum: "$votes"}
}
}
]);
When I startup my server, I get no errors; however, the totalVotes field hasn't been created. I used this documentation to help me. It similarly uses the $sum operator and I did it exactly like the documentation illustrates, but no results.
MongoDb aggregation doesn't save its result into database. You just get the result of aggregation inline within a callback.
So after aggregation you would need to do multi update to your database:
PollModel.aggregate([
{
$project: { totalVotes: { $sum: "$votes"} }
}]).exec( function(err, docs){
// bulk is used for updating all records within a single query
var bulk = PollModel.collection.initializeUnorderedBulkOp();
// add all update operations to bulk
docs.forEach(function(doc){
bulk.find({_id: doc._id}).update({$set: {totalVotes: doc.totalVotes}});
});
// execute all bulk operations
bulk.execute(function(err) {
});
})
});
Unfortunately this does not work as you think it does because "votes" is actually an array of values to start with, and then secondly because $sum is an accumulator operator for usage in the $group pipeline stage only.
So in order for you to get the total of the array as another property, first you must $unwind the array and then $group together on the document key to $sum the total of the elements:
PostModel.aggregate(
[
{ "$unwind": "$votes" },
{ "$group": {
"_id": "$_id",
"anotherField": { "$first": "$anotherField" },
"totalVotes": { "$sum": "$votes" }
}}
],
function(err,results) {
}
);
Also noting here another accumulator in $first would be necessary for each additional field you want in results as $group and $project only return the fields you ask for.
Generally though this is better to keep as a property within each document for performance reasons, as it's faster than using aggregate. So to do this just increment a total each time you $push to an array by also using $inc:
PostModel.update(
{ "_id": id },
{
"$push": { "votes": 5 },
"$inc": { "totalVotes": 5 }
},
function(err,numAffected) {
}
);
In that way the "totalVotes" field is always ready to use without the overhead of needing to deconstruct the array and sum the values for each document.
You don't have totalVotes in your schema. Just try the below code.
var PollSchema = new Schema(
{
votes: [Number],
totalVotes: Number
}
);
PollModel.aggregate([
{
$project: {
totalVotes: { $sum: "$votes"}
}
}
]);
or
resultData.toJSON();
#Blakes Seven and #Volodymyr Synytskyi helped me arrive to my solution! I also found this documentation particularly helpful.
PollModel.aggregate(
[
{ '$unwind': '$votes' },
{ '$group': {
'_id': '$_id',
'totalVotes': { '$sum': '$votes' }
}}
],
function(err,results) {
// console.log(results);
results.forEach(function(result){
var conditions = { _id: result._id },
update = { totalVotes: result.totalVotes },
options = { multi: true };
PollModel.update(conditions, update, options, callback);
function callback (err, numAffected) {
if(err) {
console.error(err);
return;
} else {
// console.log(numAffected);
}
}
});
}
);

Categories

Resources