How to prevent pushing in the document with same attribute in Mongodb - node.js

I have the following structure. I would like to prevent pushing in the document with the same attribute.
E.g. Basically, i find the user object first. If i have another vid (with is already inside), it will not get pushed in. Try using $addToSet, but failed.
I am using Mongoose.
This is my Model Structure:
var User = mongoose.model('User', {
oauthID: Number,
name: String,
username: String,
email: String,
location: String,
birthday: String,
joindate: Date,
pvideos: Array
});
This is my code for pushing into Mongo
exports.pinkvideo = function(req, res) {
var vid = req.body.vid;
var oauthid = req.body.oauthid;
var User = require('../models/user.js');
var user = User.findOne({
oauthID: oauthid
}, function(err, obj) {
if (!err && obj != null) {
obj.pvideos.push({
vid: vid
});
obj.save(function(err) {
res.json({
status: 'success'
});
});
}
});
};

You want the .update() method rather than retrieving the document and using .save() after making your changes.
This not only gives you access to the $addToSet operator that was mentioned, and it's intent is to avoid duplicates in arrays it is a lot more efficient as you are only sending your changes to the database rather than the whole document back and forth:
User.update(
{ oauthID: oauthid },
{ "$addToSet": { "pVideos": vid } },
function( err, numAffected ) {
// check error
res.json({ status: "success" })
}
)
The only possible problem there is it does depend on what you are actually pushing onto the array and expecting it to be unique. So if your array already looked like this:
[ { "name": "A", "value": 1 } ]
And you sent and update with an array element like this:
{ "name": "A", "value": 2 }
Then that document would not be considered to exist purely on the value of "A" in "name" and would add an additional document rather than just replace the existing document.
So you need to be careful about what your intent is, and if this is the sort of logic you are looking for then you would need to find the document and test the existing array entries for the conditions that you want.
But for basic scenarios where you simply don't want to add a clear duplicate then $addToSet as shown is what you want.

Related

MongoDB upsert creates another instance

I currently have an upsert function in my project which works but my main problem is that it creates another instance of the record, and updates the new instance instead. This is the code:
router.route('/carousel/update/:_id').put(function(req, res) {
var id;
if(req.params._id == 'undefined'){
id = crypto.randomBytes(12).toString('hex');
}
else {
id = ObjectId(req.params._id)
}
db.collection('home').updateOne({"_id": id},
{$set: req.body}, {upsert: true}, (err, results) => {
if (err) throw err;
res.send(results)
console.log(req.body)
});
});
The problem:
1. It mystifies me that mongoDB takes my crypto generated _id and takes it as the new _id for the upserted document. Why is that? When {upsert: true}, isn't mongoDB supposed to generate a new _id?
2. Because of the nature of problem 1, whenever I try to update the original document, it updates the upserted document instead since they have the same _id values even though their _ids are positioned at different document levels.
In conclusion, when given a 'home' document, how do I upsert correctly without adding a new record with the same values and _ids?
Thanks for your help!
EDIT
This is the JSON body content of the document with custom generated _id using crypto:
{
"_id": "1262d480eea83567181b3206",
"header": "hello",
"subheader": "hello"
}
Whereas, this is the body content of the upserted document.
{
"_id": {
"$oid": "1262d480eea83567181b3206"
},
"header": "helloasad",
"subheader": "helloasda"
}
As observed, after upserting, it takes the same _id value of the original document but on another document level.
A possible solution/explanation based on #Ashwanth Madhav information:
In your code 'id' was being sent to the update as a String type, but the id in MongoDB is an ObjectId type:
Code will be something like that:
var id;
if(req.params._id == 'undefined'){
// 'id' NEED TO BE AN ObjectId...
// 'id' WAS BEING SENT AS A 'String'
id = ObjectId(crypto.randomBytes(12).toString('hex'));
}
else {
id = ObjectId(req.params._id)
}

Why the mongodb is changing the _id when I use its findOneAndUpdate method?

I came from relational database whereas the primary key (this case, _id) is the same along its life, and so I was surprised when I saw this behavior in mongodb.
I'm using the mongoose's findOneAndUpdate plugin method in the below way:
User.findOneAndUpdate(
{ "products._id": _id, "_id": req.payload._id },
{
"$set": {
"products.$": {name: "New name"}
}
},
{
new: true ,
runValidators: true
},
function (err, doc) {
if (err != null) {
res.status(500).json({ message: "Error on updating. Please, try again later." });
} else if (doc == null) {
res.status(404).json({ message: "Product not found." });
} else {
res.status(200).json(doc.products)
}
}
);
Before start:
{_id: 58b5e637f9f904a800721abf, name: "Old name"}
After (_id changed):
{_id: 58b5e35a7f4ff38c433a5bc9, name: "New name"}
I just want to keep the same _id after an update, 'cause I think I could to face troubles when I implement a simultaneous updates for example.
I searched and I found out this mongoose method is called straightforward to the mongo's drivers without middlewares. Thus, I guess this question can be solved by experts in mongodb without knowledge in mongoose.
_id is attached to a document revision, not a document entity.
By passing new: true you're asking Mongo to return the id of the latest revision, which will have a different id than the original document (Upsert).
For document based storages it's recommended to implement your own UUID schema.
Either go deterministic with uuid:
var UUID = require('uuid-1345');
UUID.v3({
namespace: UUID.namespace.oid,
name: "abc" // Some formula to calculate you uuid, could be based on the document's legacy id or some other unique identifier.
}, function (err, id) {
console.log("Generated a name-based UUID using MD5:\n\t%s\n", id);
});
Or random with plain random HEX:
var crypto = require('crypto');
var id = crypto.randomBytes(20).toString('hex');
Include this in your document's body... and don't forget to index it!

Saving unique array elements MongoDB

I am trying to save a unique array of String elements using MongoDB but for some reason it allows me to save duplicates.
I am using mongoose. My code:
schema = mongoose.Schema({
"searchingId": { "type": String,
"unique": true,
"index": true },
"sharedTo" : {
type: [String],
unique: true,
"trim":true
}
}, {collection: 'myCollection'});
Basically the point is to keep a list of email addresses where the user had sent emails and to prevent the user from spamming them. But this schema will allow me to push any string to a sharedTo array and to .save() it no matter whether the duplicates exist. How to prevent this from happening?
EDIT:
Lahar's answer does help with my question but not entierly. I would like to prevent user from adding emails if there is at least one duplicate. So basically $addToSet will help with uniqueness but not with my question.
You can use $addToSet instead of $push to add email in "sharedTo" array.
That won't add duplicate element(email in your case).
By providing unique:true to whole array field won't check uniqueness of array element.
Please check $addToSet documentation.
you can implement your own static function that will run from the schema class to do this for every email entered
UserSchema.statics.findUseremail = function(useremail, suffix,
callback) {
var _this = this;
var possibleuseremail = useremail + (suffix || '');
_this.findOne({
useremail: possibleuseremail
}, function(err, userem) {
if (!err) {
if (!userem) {
callback(findUseremail);
} else {
return _this.findUseremail(useremail, (suffix || 0) +
1, callback);
}
} else {
callback(null);
}
});
};
So looks like I had found the solution myself but thanks to the Lahar Shah answer for pointing me in the right direction. Instead of using
Model.update(conditions, doc, [options], [callback])
I did
fetching the object
Added each of emails to my sharedTo attribute while checking for duplicates.
saving the object if no duplicates
Code:
var length = emails.length;
for( var i = 0; i < length; i++ ) {
var saved = doc.sharedTo.addToSet(emails[i]).length;
if (saved != 1) {
//status 409 - You have already sent email to user emails[i]
return;
}
}
doc[0].save(function(fail, success) {
if(fail) {
//error
} else {
//success return 200
}
});

mongoose is removing empty object out of embedded documents in array

notice the following code, that shows a schema with 2 arrays, one is configured to be from Type:
[
mongoose.Schema.Types.Mixed
]
and one is configured to be from type:
[
{
value: mongoose.Schema.Types.Mixed
}
]
Here is the code:
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
var schema = new mongoose.Schema({
withSchema: [{
value:mongoose.Schema.Types.Mixed}
],
withoutSchema: [mongoose.Schema.Types.Mixed],
} , {minimize: false});
var Tweak = mongoose.model('tweak', schema );
I update the document using the same data:
var data = {
"withSchema" : [ { "value": { a:"221", b:{} } } ],
"withoutSchema" : [ { "value": { a:"221", b:{} } } ]
}
Tweak.findByIdAndUpdate("545680170960023a185ea77e", data, function(err, doc){
console.log(doc);
//{
// "withSchema" : [ { "value": { a:"221" } } ],
// "withoutSchema" : [ { "value": { a:"221", b:{} } } ]
//}
});
How do I prevent this b:{} removal?
EDIT:
It turns out this happens only when there is an embeddedDocument inside an Array.
Removal of empty objects from arrays - is cause by the minimize option of schema - which defaults to 'true'. Erken answered this in a comment in the down voted answer above - putting it as a separate answer so people can find it.
Can be overridden to 'false' in the schema - then it will save empty objects in arrays
var schema = new Schema({ name: String, inventory: {} }, { minimize: false });
from http://mongoosejs.com/docs/guide.html#minimize
This approach involves first retrieving the document from Mongo, then issuing an update command (triggered by calling save). Take a look at the following code.
var id = "54619b5ef610b70b14a46e79";
Tweak.findById(id, function(err, result) {
if (err) {
console.log(err);
}
console.log(result.withSchema[0].value);
result.withSchema[0].value = data.withSchema[0].value;
result.withoutSchema.value = data.withoutSchema.value;
result.save(function(err) {
if (err) {
console.log(err);
}
console.log('updated');
});
});
After saving the document run the code snippet with appropriate 'id' value.
Objects which evaluate to null (as your b does) are skipped by mongoose. As an empty or non existant object evaluates to null in all MongoDB drivers and even shell queries, too, it actually does not make a difference wether an empty b is saved or not.
For example, if you query if b exists, since b is empty, it evaluates to nulland hence the query would fail for that document, regardless if bhas an empty value or simply isn't there.
Since you already use the minimize option, I assume that the empty result, since it evaluates to null, simply isn't displayed. When checking with the shell, the key should be there with an empty value.
Bottom line: for all practical purposes, it does not make a difference wether b holds an empty value or simply isn't there.

Trying to remove a subdocument in Mongoose gives me an internal mongoose error

I have a schema as follows (simplified):
var Permission = new Schema({
_id: String, // email address
role: String // "admin" or "member"
});
var Org = new Schema({
name: {type: String, index: {unique: true, dropDups: true}, trim: true},
permissions: [Permission]
});
An example document would look like this:
{
"name": "My Org",
"permissions" : [
{"_id" : "joe#gmail.com", "role" : "admin"},
{"_id" : "mary#gmail.com", "role" : "member"}
]
}
I am trying to delete one of the permissions rows, using the command org.permissions.remove(req.params.email), as shown in context below:
exports.removePermissions = function(req, res) {
var name = req.params.name;
return Org
.findOne({name: name})
.select()
.exec(function(err, org) {
if (err) return Org.handleError(res, err);
if (!org) return Org.handleError(res, new Error("#notfound " + name));
org.permissions.remove(req.params.email);
org.save(function(err, org) {
if (err) return Org.handleError(res, err);
else return res.send(org);
});
});
};
When I do this, I get the following error:
TypeError: Cannot use 'in' operator to search for '_id' in joe#gmail.com
at EmbeddedDocument.Document._buildDoc (/../node_modules/mongoose/lib/document.js:162:27)
at EmbeddedDocument.Document (/../node_modules/mongoose/lib/document.js:67:20)
at EmbeddedDocument (/../node_modules/mongoose/lib/types/embedded.js:27:12)
at new EmbeddedDocument (/../node_modules/mongoose/lib/schema/documentarray.js:26:17)
at MongooseDocumentArray._cast (/../node_modules/mongoose/lib/types/documentarray.js:62:10)
at Object.map (native)
at MongooseDocumentArray.MongooseArray.remove (/../node_modules/mongoose/lib/types/array.js:360:21)
at model.Org.methods.removePermissions (/../models/org.js:159:20)
The only thing I can think of is that Mongoose does not support _id fields that are not ObjectID's? This is strange, because I use these elsewhere in my code and it works fine (e.g. org.permissions.id("joe#gmail.com") works).
Any suggestions much appreciated!
I'm not sure why using remove there isn't working, but you can do this atomically with findOneAndUpdate and the $pull operator:
exports.removePermissions = function(req, res) {
var name = req.params.name;
return Org.findOneAndUpdate(
{name: name},
{$pull: {permissions: {_id: req.params.email}}},
function(err, org) {
// org contains the updated doc
...
});
};
As per this answer, you need to call remove() on the subdocument you want to remove, rather than on the entire subdocument array.
So, change:
org.permissions.remove(req.params.email);
to:
org.permissions.id(req.params.email).remove();
This two-step method has the added advantage over the answer supplied by #JohnnyHK in that you can validate whether the subdocument actually exists before removing it. This can be useful if you'd like to send a 404 response indicating that the subdocument doesn't exist - as far as I am aware, this isn't possible using the $pull atomic operator.
Note that this also will only work if your subdocument array has a schema, as illustrated in the question. If it doesn't, or it has a schema type of Mixed, the collection returned from the database will be a plain array rather than a Mongoose-enhanced array. This means that there is no .id() function. In this case, I would use lodash#remove instead:
_.remove(org.permissions, (function(permission) {
return permission._id.toString() === req.params.email;
}));

Resources