Mongoose update or create many values in one query - node.js

I have a document like this:
{
"_id" : ObjectId("565e906bc2209d91c4357b59"),
"userEmail" : "abc#example.com",
"subscription" : {
"project1" : {
"subscribed" : false
},
"project2" : {
"subscribed" : true
},
"project3" : {
"subscribed" : false
},
"project4" : {
"subscribed" : false
}
}
}
I'm using express to for my post web service call like this:
router.post('/subscribe', function(req, res, next) {
MyModel.findOneAndUpdate(
{
userEmail: req.body.userEmail
},
{
// stuck here on update query
},
{
upsert: true
}, function(err, raw) {
if (err) return handleError(err);
res.json({result: raw});
}
)
});
My req contains data like this:
{
userEmail: "abc#example.com",
subscription: ["project1", "project4"]
}
So these are the steps I would like to perform on this call:
Check user exists, otherwise create the user. For instance, if abc#example.com doesn't exist, create a new document with userEmail as abc#example.com.
If user exists, check project1 and project4 exists in subscription object. If not create those.
If project1 and project4 exists in subscription, then update the subscribed to true.
I'm not sure whether I can achieve all the above 3 steps with a single query. Kindly advise.

Vimalraj,
You can accomplish this using $set. The $set operator replaces the value of a field with the specified value. According to the docs:
If the field does not exist, $set will add a new field with the specified value, provided that the new field does not violate a type constraint. If you specify a dotted path for a non-existent field, $set will create the embedded documents as needed to fulfill the dotted path to the field.
Since your req.subscription will be an array you'll have to build your query. to look like this:
{
$set: {
"subscription.project1":{"subscribed" : true},
"subscription.project4":{"subscribed" : true}
}
You can use reduce to create an object from req.subscription = ["project1","project4"] array
var subscription= req.subscription.reduce(function(o,v){
o["subscription." + v] = {subscription:true};
return o;
},{});
Then your code becomes:
router.post('/subscribe', function(req, res, next) {
var subscription= req.subscription.reduce(function(o,v){
o["subscription." + v] = {subscription:true};
return o;
},{});
MyModel.findOneAndUpdate(
{
userEmail: req.body.userEmail
},
{
$set: subscription
},
{
upsert: true
}, function(err, raw) {
if (err) return handleError(err);
res.json({result: raw});
}
)
});

Related

Match one field and ignore others

I have my data like this:
{"_id":"5c0a17013d8ca91bf4ee7885",
"ep1":{
"email":"test#gmail.com",
"password":"123456",
"phoneNo":"123456789",
"endpointId":"ep1"
}
}
I want to match all the object that has key ep1 and value "email":"test#gmail.com" and set ep1:{}
Code:
My req object is like this {"endpointId":"ep1", "email":
"test#gmail.com"}
let query = {[req.body.endpointId]:{email: req.body.email}};
endpointModelData.endpoint.updateMany(query, {[req.body.endpointId]:{}}, {multi: true}, function(err, doc){
if (err) return res.send(500, { error: err });
console.log(doc)
return res.status(201).send("success.");
})
But this code only works if there is only one entry(that is email) inside my ep1 object
like this:
{"_id":"5c0a17013d8ca91bf4ee7885",
"ep1":{
"email":"test#gmail.com"
}
}
So, how can I match email and ignore other fields?
You need to match: {"ep1.email":"test#gmail.com"} so change your query to:
let query = { [`${req.body.endpointId}.email`]: req.body.email };
You can see that filter working here
On the end of the day your update should look like:
updateMany({ "ep1.email" : "test#gmail.com" }, { "ep1": {} }, {multi: true})

Add object to array of objects if object doesn't exist, otherwise update object

Desired Behaviour
Add object to array of objects if it doesn't exist, otherwise update object.
Each user can only add one rating.
Schema
"ratings" : [
{
"user_email" : "john#smith.com",
"criteria_a" : 0,
"criteria_b" : 3,
"criteria_c" : 1,
"criteria_d" : 5,
"criteria_e" : 2
},
{
"user_email" : "bob#smith.com",
"criteria_a" : 1,
"criteria_b" : 3,
"criteria_c" : 5,
"criteria_d" : 2,
"criteria_e" : 1
},
{
"user_email" : "jane#smith.com",
"criteria_a" : 5,
"criteria_b" : 3,
"criteria_c" : 1,
"criteria_d" : 0,
"criteria_e" : 1
}
]
What I've Tried
I've read this:
The $addToSet operator adds a value to an array unless the value is
already present, in which case $addToSet does nothing to that array.
Source: https://docs.mongodb.com/manual/reference/operator/update/addToSet/
But I'm a bit confused because I think I am trying to:
$addToSet if object doesn't exist
$set (ie update the existing object) if it does exist
The following keeps adding rating objects to the array, even if a user has already added a rating.
var collection = mongo_client.db("houses").collection("houses");
var o_id = new ObjectID(id);
var query = { _id: o_id, "ratings.user_email": user_email };
var update = { $addToSet: { ratings: rating } };
var options = { upsert: true };
collection.updateOne(query, update, options, function(err, doc) {
if (err) {
res.send(err);
} else {
res.json({ doc: doc })
}
});
Edit:
The following works if a user has already submitted a rating, but otherwise it throws the error The positional operator did not find the match needed from the query.:
var update = { $set: { "ratings.$": rating } };
You will need to perform this with two distinct actions:
Attempt a $push if no rating for the given user exists yet, i.e. negate the user_email in your match portion of the update query as you're already doing.
Perform a second update once again matching on "ratings.user_email": user_email, but this time perform a $set on "ratings.$": rating.
If no rating for the user exists yet, then (1) will insert it, otherwise no operation occurs. Now that the rating is guaranteed to exist, (2) will ensure that the rating is updated. In the case that (1) inserted a new rating, (2) will simply result in no operation occuring.
The result should look something like this:
collection.updateOne(
{ _id: o_id, ratings: { $not: { $elemMatch: { user_email: user_email } } } },
{ $push: { ratings: rating } }
);
collection.updateOne(
{ _id: o_id, "ratings.user_email": user_email },
{ $set: { "ratings.$": rating } }
);
A more verbose example with comments is below:
// BEGIN new rating query/update variables
var query_new_rating = { _id: o_id, ratings: { $not: { $elemMatch: { user_email: user_email } } } };
var update_new_rating = { $push: { ratings: rating } };
// END new rating query/update variables
// part 01 - attempt to push a new rating
collection.updateOne(query_new_rating, update_new_rating, function(error, result) {
if (error) {
res.send(error);
} else {
// get the number of modified documents
// if 0 - the rating already exists
// if 1 - a new rating was added
var number_modified = result.result.nModified;
// BEGIN if updating an existing rating
if (number_modified === 0) {
console.log("updating existing rating");
// BEGIN existing rating query/update variables
var query_existing_rating = { _id: o_id, "ratings.user_email": user_email };
var update_existing_rating = { $set: { "ratings.$": rating } };
// END existing rating query/update variables
// part 02 - update an existing rating
collection.updateOne(query_existing_rating, update_existing_rating, function(error, result) {
if (error) {
res.send(error);
} else {
console.log("updated existing rating");
res.json({ result: result.result })
}
});
}
// END if updating an existing rating
// BEGIN if adding a new rating
else if (number_modified === 1) {
console.log("added new rating");
res.json({ result: result.result })
}
// END if adding a new rating
}
});

How to change key Name in mongoDB [duplicate]

Assuming I have a collection in MongoDB with 5000 records, each containing something similar to:
{
"occupation":"Doctor",
"name": {
"first":"Jimmy",
"additional":"Smith"
}
Is there an easy way to rename the field "additional" to "last" in all documents? I saw the $rename operator in the documentation but I'm not really clear on how to specify a subfield.
You can use:
db.foo.update({}, {$rename:{"name.additional":"name.last"}}, false, true);
Or to just update the docs which contain the property:
db.foo.update({"name.additional": {$exists: true}}, {$rename:{"name.additional":"name.last"}}, false, true);
The false, true in the method above are: { upsert:false, multi:true }. You need the multi:true to update all your records.
Or you can use the former way:
remap = function (x) {
if (x.additional){
db.foo.update({_id:x._id}, {$set:{"name.last":x.name.additional}, $unset:{"name.additional":1}});
}
}
db.foo.find().forEach(remap);
In MongoDB 3.2 you can also use
db.students.updateMany( {}, { $rename: { "oldname": "newname" } } )
The general syntax of this is
db.collection.updateMany(filter, update, options)
https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/
You can use the $rename field update operator:
db.collection.update(
{},
{ $rename: { 'name.additional': 'name.last' } },
{ multi: true }
)
If ever you need to do the same thing with mongoid:
Model.all.rename(:old_field, :new_field)
UPDATE
There is change in the syntax in monogoid 4.0.0:
Model.all.rename(old_field: :new_field)
Anyone could potentially use this command to rename a field from the collection (By not using any _id):
dbName.collectionName.update({}, {$rename:{"oldFieldName":"newFieldName"}}, false, true);
see FYI
I am using ,Mongo 3.4.0
The $rename operator updates the name of a field and has the following form:
{$rename: { <field1>: <newName1>, <field2>: <newName2>, ... } }
for e.g
db.getCollection('user').update( { _id: 1 }, { $rename: { 'fname': 'FirstName', 'lname': 'LastName' } } )
The new field name must differ from the existing field name. To specify a in an embedded document, use dot notation.
This operation renames the field nmae to name for all documents in the collection:
db.getCollection('user').updateMany( {}, { $rename: { "add": "Address" } } )
db.getCollection('user').update({}, {$rename:{"name.first":"name.FirstName"}}, false, true);
In the method above false, true are: { upsert:false, multi:true }.To update all your records, You need the multi:true.
Rename a Field in an Embedded Document
db.getCollection('user').update( { _id: 1 }, { $rename: { "name.first": "name.fname" } } )
use link : https://docs.mongodb.com/manual/reference/operator/update/rename/
This nodejs code just do that , as #Felix Yan mentioned former way seems to work just fine , i had some issues with other snipets hope this helps.
This will rename column "oldColumnName" to be "newColumnName" of table "documents"
var MongoClient = require('mongodb').MongoClient
, assert = require('assert');
// Connection URL
//var url = 'mongodb://localhost:27017/myproject';
var url = 'mongodb://myuser:mypwd#myserver.cloud.com:portNumber/databasename';
// Use connect method to connect to the server
MongoClient.connect(url, function(err, db) {
assert.equal(null, err);
console.log("Connected successfully to server");
renameDBColumn(db, function() {
db.close();
});
});
//
// This function should be used for renaming a field for all documents
//
var renameDBColumn = function(db, callback) {
// Get the documents collection
console.log("renaming database column of table documents");
//use the former way:
remap = function (x) {
if (x.oldColumnName){
db.collection('documents').update({_id:x._id}, {$set:{"newColumnName":x.oldColumnName}, $unset:{"oldColumnName":1}});
}
}
db.collection('documents').find().forEach(remap);
console.log("db table documents remap successfully!");
}
If you are using MongoMapper, this works:
Access.collection.update( {}, { '$rename' => { 'location' => 'location_info' } }, :multi => true )

Can't find a easy way out of multiple async for each node js (sails)

So here's the deal :
I have an array of objects with a child array of objects
askedAdvices
askedAdvice.replayAdvices
I'm looping trough the parent and foreach looping trough the childs and need to populate() two obejcts (I'm using sails)
The child looks like :
askedAdvices = {
replayAdvices : [{
bookEnd : "<ID>",
user : "<ID>"
}]
}
So my goal is to cycle and populate bookEnd and user with two findOne query, but I'm going mad with the callback hell.
Here's the Models code :
AskedAdvices Model
module.exports = {
schema : false,
attributes: {
bookStart : {
model : 'book'
},
replayAdvices : {
collection: 'replybookend'
},
user : {
model : 'user',
required : true
},
text : {
type : "text"
}
}
};
ReplyBookEnd Model
module.exports = {
schema : false,
attributes: {
bookEnd : {
model : 'book'
},
user : {
model : 'user',
required : true
},
text : {
type : "text"
}
}
};
Here's the Method code :
getAskedAdvices : function(req, res) {
var queryAskedAdvices = AskedAdvices.find()
.populate("replayAdvices")
.populate("user")
.populate("bookStart")
queryAskedAdvices.exec(function callBack(err,askedAdvices){
if (!err) {
askedAdvices.forEach(function(askedAdvice, i){
askedAdvice.replayAdvices.forEach(function(reply, i){
async.parallel([
function(callback) {
var queryBook = Book.findOne(reply.bookEnd);
queryBook.exec(function callBack(err,bookEndFound) {
if (!err) {
reply.bookEnd = bookEndFound;
callback();
}
})
},
function(callback) {
var queryUser = User.findOne(reply.user)
queryUser.exec(function callBack(err,userFound){
if (!err) {
reply.user = userFound;
callback();
}
})
}
], function(err){
if (err) return next(err);
return res.json(200, reply);
})
})
})
} else {
return res.json(401, {err:err})
}
})
}
I can use the async library but need suggestions
Thanks folks!
As pointed out in the comments, Waterline doesn't have deep population yet, but you can use async.auto to get out of callback hell. The trick is to gather up the IDs of all the children you need to find, find them with single queries, and then map them back onto the parents. The code would look something like below.
async.auto({
// Get the askedAdvices
getAskedAdvices: function(cb) {
queryAskedAdvices.exec(cb);
},
// Get the IDs of all child records we need to query.
// Note the dependence on the `getAskedAdvices` task
getChildIds: ['getAskedAdvices', function(cb, results) {
// Set up an object to hold all the child IDs
var childIds = {bookEndIds: [], userIds: []};
// Loop through the retrieved askedAdvice objects
_.each(results.getAskedAdvices, function(askedAdvice) {
// Loop through the associated replayAdvice objects
_.each(askedAdvice.replayAdvices, function(replayAdvice) {
childIds.bookEndIds.push(replayAdvice.bookEnd);
childIds.userIds.push(replayAdvice.user);
});
});
// Get rid of duplicate IDs
childIds.bookEndIds = _.uniq(childIds.bookEndIds);
childIds.userIds = _.uniq(childIds.userIds);
// Return the list of IDs
return cb(null, childIds);
}],
// Get the associated book records. Note that this task
// relies on `getChildIds`, but will run in parallel with
// the `getUsers` task
getBookEnds: ['getChildIds', function(cb, results) {
Book.find({id: results.getChildIds.bookEndIds}).exec(cb);
}],
getUsers: ['getChildIds', function(cb, results) {
User.find({id: results.getChildIds.userIds}).exec(cb);
}]
}, function allTasksDone(err, results) {
if (err) {return res.serverError(err);
// Index the books and users by ID for easier lookups
var books = _.indexBy(results.getBookEnds, 'id');
var users = _.indexBy(results.getUsers, 'id');
// Add the book and user objects back into the `replayAdvices` objects
_.each(results.getAskedAdvices, function(askedAdvice) {
_.each(askedAdvice.replayAdvices, function(replayAdvice) {
replayAdvice.bookEnd = books[replayAdvice.bookEnd];
replayAdvice.user = users[replayAdvice.bookEnd];
});
});
});
Note that this is assuming Sails' built-in Lodash and Async instances; if you're using newer versions of those packages the usage of async.auto has changed slightly (the task function arguments are switched so that results comes before cb), and _.indexBy has been renamed to _.keyBy.

Many-to-Many in MongoDB

I have two MongoDB models "Users" and "Roles". Each user can have multiple roles and each role can be assigned to many users. To keep the relation simple I would like to store the reference between both models only in the "Users" model which is already working as expected. But when I'm loading all roles at once with .find({}), I would also like to know how many users are assigned to these roles (to check if a role can be modified).
I'm using Node.js + ExpressJS and mongoose. This is what I already have:
var userSchema = new Schema({
username: String,
...
roles : [ {
type : Number,
ref : 'Role'
} ]
});
var roleSchema = new Schema({
_id : Number,
name : String
});
--------------------------------------------------
function getRoles(request, response) {
Role.find({}, function(err, roles) {
....
response.send(roles);
});
}
But now I wonder how I would achive the count query per role and still be able to send the result in one response.
I would like to avoid to send a single request per role and try to do the count within the getRoles() function. In a relational database I would do something like this:
select r.*,
(select count(*) from user2role u2r where u2r.role_id = r.role_id)
from role r;
But what's the MongoDB equivalent?
Any help and hints will be appreciated.
Thanks, Gerry
To achieve the user count query per role with the given schema, you could either use the count() method as follows:
function getUsersCount(roleName, req, res) {
Role.findOne({"name": roleName}, function(err, role) {
var roleId = role._id;
// var countQuery = User.where({ "role": roleId }).count();
// User.count({ "roles": roleId }).count(callback)
// User.count({ "roles": roleId }, callback)
User.where({ "roles": roleId }).count(function (err, count) {
if (err) return handleError(err);
console.log("There are %d users with the role %d", count, roleName);
res.send(count);
})
});
}
or you can use the aggregate() method like:
// Find the user count for a given role
function getUsersCount(roleName, req, res) {
Role.findOne({"name": roleName}, function(err, role) {
var roleId = role._id;
var pipeline = [
{
"$match": { "roles": roleId } /* filter the documents that only have the roleId in the role array */
},
{ "$unwind": "$roles" },
{
"$match": { "roles": roleId }
},
{
"$group": {
"_id": null, "count": { "$sum": 1 }
}
},
{
"$project": {
"_id": 0, "count": 1
}
}
];
Users.aggregate(pipeline, function (err, result) {
if (err) return handleError(err);
console.log(result); // [ { count: 55 } ] for example
res.send(result);
});
// Or use the aggregation pipeline builder.
Users.aggregate()
.match({ "roles": roleId })
.unwind("roles")
.match({ "roles": roleId })
.group({ "_id": null, "count": { "$sum": 1 } })
.select("-id count")
.exec(function (err, result) {
if (err) return handleError(err);
console.log(result); // [ { count: 55 } ] for example
res.send(result);
});
});
}
-- EDIT --
One approach that you could take is to use the lean() method to get pure JavaScript objects that you can manipulate to add the extra userCount field for each role. With lean() it's possible because documents returned from queries with the lean option enabled are plain javascript objects', not MongooseDocuments. Thus with the roles returned from the find() cursor, you can convert them into plain JS objects
that you can iterate over using the native JavaScript map() method, get the number of users per role using another query that takes into consideration the count() method explained above and return the modified object.
The following demonstrates how the function is implemented:
function getRoles(request, response) {
Role.find({}).lean().exec(function(err, roles) {
var result = roles.map(function(r){
var obj = {},
countQuery = User.where({ "roles": r._id }).count();
obj["userCount"] = countQuery;
obj["_id"] = r._id;
obj["name"] = r.name;
return obj;
});
response.send(result);
});
}
As per your above explanation about the schema design, i can assume your schema is as below :
Roles collection :
{
role_id :
name :
}
User collection :
{
user :
role_id : [ ]
....
}
you can use aggregation to achieve this :
db.users.aggregate([{$unwind: "$role_id"},
{$group: {_id: "$role_id", count : {"$sum":1}} },
]);

Resources