I have a post route that receives data from a PUT request in an express app that aims to update a mongoose document based on submitted form input. The "Base" model is Profile, and I have two discriminator models Helper and Finder that conditionally add fields to the Profile schema (see below for details).
Thus, req.body.profile will contain different fields depending on the discriminator it's associated with, but will always contain the fields (username, email city, accountType) present in the "base" model, Profile.
Before I send my PUT request, an example of a document in Profile looks like this:
{ jobTitle: '',
lastPosition: '',
email: '',
city: '',
accountType: 'helper',
_id: 5c77883d8db04c921db5f635,
username: 'here2help',
__v: 0 }
This looks good to me, and suggests that the model is being created as I want (with base fields from Profile, and those associated with the Helper model - see below for models).
My POST route then looks like this:
router.put("/profile/:id", middleware.checkProfileOwnership, function(req, res){
console.log(req.body.profile);
Profile.findOneAndUpdate(req.params.id, req.body.profile, function(err, updatedProfile){
if(err){
console.log(err.message);
res.redirect("/profile");
} else {
console.log(updatedProfile);
res.redirect("/profile/" + req.params.id);
}
});
});
The information I receive from the form (console.log(req.body.profile)) is what I expect to see:
{ accountType: 'helper',
username: 'here2help',
email: 'helpingU#me.com',
city: 'New York',
jobTitle: 'CEO',
lastPosition: 'sales rep'}
However, after updating the document with req.body.profile in Profile.findOneAndUpdate(), I do not see my returned document updated:
console.log(updatedProfile)
{ jobTitle: '',
lastPosition: '',
email: 'helpingu#me.com',
city: 'New York',
accountType: 'helper',
_id: 5c77883d8db04c921db5f635,
username: 'here2help',
__v: 0 }
So, the fields that are defined in my 'Base' model (ie those defined in ProfileSchema - see below) are being updated (e.g. city), but those that are in my discriminators are not - see below.
The updated information is clearly present in req, but is not propagated to the Profile model - How can this be?
I've also tried using findByIdAndUpdate but I get the same result.
Here are the Schemas I'm defining:
Profile - my "base" schema:
var mongoose = require("mongoose");
var passportLocalMongoose = require("passport-local-mongoose");
var profileSchema = new mongoose.Schema({
username: String,
complete: { type: Boolean, default: false },
email: { type: String, default: "" },
city: { type: String, default: "" }
}, { discriminatorKey: 'accountType' });
profileSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model("Profile", profileSchema);
Finder
var Profile = require('./profile');
var Finder = Profile.discriminator('finder', new mongoose.Schema({
position: { type: String, default: "" },
skills: Array
}));
module.exports = mongoose.model("Finder");
Helper
var Profile = require('./profile');
var Helper = Profile.discriminator('helper', new mongoose.Schema({
jobTitle: { type: String, default: "" },
lastPosition: { type: String, default: "" }
}));
module.exports = mongoose.model("Helper");
This is my first attempt at using discriminators in mongoose, so it's more than possible that I am setting them up incorrectly, and that this is the root of the problem.
Please let me know if this is unclear, or I need to add more information.
It matters what schema you use to query database
Discriminators build the mongo queries based on the object you use. For instance, If you enable debugging on mongo using mongoose.set('debug', true) and run Profile.findOneAndUpdate() you should see something like:
Mongoose: profiles.findAndModify({
_id: ObjectId("5c78519e61f4b69da677a87a")
}, [], {
'$set': {
email: 'finder#me.com',
city: 'New York',
accountType: 'helper',
username: 'User NAme', __v: 0 } }, { new: true, upsert: false, remove: false, projection: {} })
Notice it uses only the fields defined in Profile schema.
If you use Helper, you would get something like:
profiles.findAndModify({
accountType: 'helper',
_id: ObjectId("5c78519e61f4b69da677a87a")
}, [], {
'$set': {
jobTitle: 'CTO',
email: 'finder#me.com',
city: 'New York',
accountType: 'helper ',
username: 'User Name', __v: 0 } }, { new: true, upsert: false, remove: false, projection: {} })
Notice it adds the discriminator field in the filter criteria, this is documented:
Discriminator models are special; they attach the discriminator key to queries. In other words, find(), count(), aggregate(), etc. are smart enough to account for discriminators.
So what you need to do when updating is to use the discriminator field in order to know which Schema to use when calling update statement:
app.put("/profile/:id", function(req, res){
console.log(req.body);
if(ObjectId.isValid(req.params.id)) {
switch(req.body.accountType) {
case 'helper':
schema = Helper;
break;
case 'finder':
schema = Finder;
break;
default:
schema = Profile;
}
schema.findOneAndUpdate({ _id: req.params.id }, { $set : req.body }, { new: true, upsert: false, remove: {}, fields: {} }, function(err, updatedProfile){
if(err){
console.log(err);
res.json(err);
} else {
console.log(updatedProfile);
res.json(updatedProfile);
}
});
} else {
res.json({ error: "Invalid ObjectId"});
} });
Notice, above is not necessary when creating a new document, in that scenario mongoose is able to determine which discriminator to use.
You cannot update discriminator field
Above behavior has a side effect, you cannot update the discriminator field because it will not find the record. In this scenario, you would need to access the collection directly and update the document, as well as define what would happen with the fields that belong to the other discriminator.
db.profile.findOneAndUpdate({ _id: req.params.id }, { $set : req.body }, { new: true, upsert: false, remove: {}, fields: {} }, function(err, updatedProfile){
if(err) {
res.json(err);
} else {
console.log(updatedProfile);
res.json(updatedProfile);
}
});
Please add option in findOneAndUpdate - { new: true };
In Moongose findOneAndUpdate() Method have four parameters
like
A.findOneAndUpdate(conditions, update, options, callback) // executes
And you need to execute like this
var query = { name: 'borne' };
Model.findOneAndUpdate(query, { name: 'jason bourne' }, options, callback)
or even
// is sent as
Model.findOneAndUpdate(query, { $set: { name: 'jason bourne' }}, options, callback)
This helps prevent accidentally overwriting your document with { name: 'jason bourne' }.
Related
I'm new to Aggregation in MongoDB and I'm trying to understand the concepts of it by making examples.
I'm trying to paginate my subdocuments using aggregation but the returned document is always the overall values of all document's specific field.
I want to paginate my following field which contains an array of Object IDs.
I have this User Schema:
const UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
firstname: String,
lastname: String,
following: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
...
}, { timestamps: true, toJSON: { virtuals: true }, toObject: { getters: true, virtuals: true } });
Without aggregation, I am able to paginate following,
I have this route which gets the user's post by their username
router.get(
'/v1/:username/following',
isAuthenticated,
async (req, res, next) => {
try {
const { username } = req.params;
const { offset: off } = req.query;
let offset = 0;
if (typeof off !== undefined && !isNaN(off)) offset = parseInt(off);
const limit = 2;
const skip = offset * limit;
const user = await User
.findOne({ username })
.populate({
path: 'following',
select: 'profilePicture username fullname',
options: {
skip,
limit,
}
})
res.status(200).send(user.following);
} catch (e) {
console.log(e);
res.status(500).send(e)
}
}
);
And my pagination version using aggregate:
const following = await User.aggregate([
{
$match: { username }
},
{
$lookup: {
'from': User.collection.name,
'let': { 'following': '$following' },
'pipeline': [
{
$project: {
'fullname': 1,
'username': 1,
'profilePicture': 1
}
}
],
'as': 'following'
},
}, {
$project: {
'_id': 0,
'following': {
$slice: ['$following', skip, limit]
}
}
}
]);
Suppose I have this documents:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'gagi',
following: []
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'kuku',
following: []
},
{
_id: '76jghkdfhasjhfsdkf',
username: 'john',
following: ['5fdgffdgfdgdsfsdfsf', '5fgjhkljvlkdsjfsldkf']
},
]
And when I test my route for user john: /john/following, everything is fine but when I test for different user which doesn't have any following: /gagi/following, the returned result is the same as john's following which aggregate doesn't seem to match user by username.
/john/following | following: 2
/kuku/following | following: 0
Aggregate result:
[
{
_id: '5fdgffdgfdgdsfsdfsf',
username: 'kuku',
...
},
{
_id: '5fgjhkljvlkdsjfsldkf',
username: 'gagi',
...
}
]
I expect /kuku/following to return an empty array [] but the result is same as john's. Actually, all username I test return the same result.
I'm thinking that there must be wrong with my implementation since I've only started exploring aggregation.
Mongoose uses a DBRef to be able to populate the field after it has been retrieved.
DBRefs are only handled on the client side, MongoDB aggregation does not have any operators for handling those.
The reason that aggregation pipeline is returning all of the users is the lookup's pipeline does not have a match stage, so all of the documents in the collection are selected and included in the lookup.
The sample document there is showing an array of strings instead of DBRefs, which wouldn't work with populate.
Essentially, you must decide whether you want to use aggregation or populate to handle the join.
For populate, use the ref as shown in that sample schema.
For aggregate, store an array of ObjectId so you can use lookup to link with the _id field.
I'm trying to update one subdocument addresses (works) and then update many subdocuments except the previous one. Basically every time an address change is_preferred to true, it must update the previous address that is_preferred was true to false (i'm trying to update everyone except the address that changed to true).
User document
_id: ObjectId("5b996f0fd5fbf511709f668f");
addresses: [
{
_id: ObjectId("5ba33e0991cd7a3bb85dab7e");
is_preferred:true
},
{
_id: ObjectId("5ba3e9337310c637207b44cb");
is_preferred:false
}
]
Here is my solution:
// model
User = mongoose.model('user', new Schema({
_id: { type: mongoose.Schema.Types.ObjectId, required: true },
addresses: [
{
_id: { type: mongoose.Schema.Types.ObjectId, required: true },
is_preferred: { type: Boolean, required: true }
}
],
}, { collection: 'user' }););
// route
router.put('/updateAddress/:addressId', auth, user.updateAddress);
// user.js
exports.updateAddress = wrap(async(req, res, next) => {
// update one object address `is_preferred` to true and return an array 'addresses' containing it
const user = await User.findOneAndUpdate(
{ addresses: { $elemMatch: { _id: mongoose.Types.ObjectId(req.params.addressId) } } }, { 'addresses.$': req.body },
{ projection: {
addresses: {
$elemMatch: { _id: mongoose.Types.ObjectId(req.params.addressId) }
}
}, new: true }).lean();
if (user) {
// updated object `is_preferred` changed to true, so another objects must be false
if (user.addresses[0].is_preferred) {
// doesnt work
await User.update({ _id: { $ne: mongoose.Types.ObjectId(req.params.addressId) }, is_preferred: true },
{ $set: { addresses: { is_preferred: false } } }, { multi: true });
}
res.status(200).json({success: true, message: 'Saved.', new_object: user.addresses[0]});
} else {
res.status(400).json({success: false, message: 'Error.'});
}
});
I'm able to update the user subdocument addresses is_preferred to true. However updating another addresses is_preferred to false isn't working. What Am I doing wrong?
I would recommend for a scenario like yours to utilize the mongoose middleware pre or post schema hooks. The idea is that instead of dealing with this in your controller you would take care of it in your schema via that middleware.
The only inconvenience is that the pre and post hooks do not fire on findOneAndUpdate and you would need to do first find then update.
So you would do something like this for the post hook:
User.post('save', doc => {
// You can update all the rest of the addresses here.
});
Also for your update to work you need to do something like this:
User.update(
{ "addresses._id": { $ne: mongoose.Types.ObjectId(req.params.addressId) }},
{ $set: { 'addresses.0.is_preferred': false }},
{ multi: true }
)
I have a Mongoose schema called "Users" which has a "Roles" subdocument as one of its variables like so:
var UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
roles: [ { type: Number, ref: 'Role' } ]
});
var RoleSchema = new mongoose.Schema({
_id: Number,
name: { type: String, required: true, unique: true },
description: { type: String, required: true }
});
I want to create a Mongoose query that will find all users with roles.names of "admin" or "owner". I've tried using this query, which I thought would work, but I don't get any users when I use the where...in part.
var roles = ["owner", "admin"];
User.find()
.populate('roles')
.where('roles.name').in(roles)
.sort({'_id': 1})
.exec(function (err, users) {
res.send(users);
});
Can someone tell me where my logic is wrong?
It's not possible to query more than one collection with a single query. If Role was an embedded subdocument in User then you could do a query on roles.name but presently that's not supported because it's a separate collection that is referenced in User.
However, a workaround is to add another filter step after the query returns that manually filters out documents which don't have any roles that matched the populate criteria but first you need to use a match query in populate instead of where method:
var roles = ["owner", "admin"];
User.find()
.populate('roles', null, { name: { $in: roles } } )
.sort({'_id': 1})
.exec(function (err, users) {
users = users.filter(function(user){
return user.roles.length;
});
res.send(users);
});
This is built into Mongoose populate() here . You can simply structure a query like this:
var roles = ["owner", "admin"];
User.find()
.populate({
path: 'roles',
match: { name: { $in: roles }},
select: 'name'
})
.sort({'_id': 1})
.exec(function (err, users) {
res.send(users);
});
I got 3 database models in mongoose that looks like this:
//profile.js
var ProfileSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true },
matches: [{ type: Schema.Types.ObjectId, ref: 'Match' }]
});
//match.js
var MatchSchema = new Schema({
scores: [{ type: Schema.Types.ObjectId, ref: 'Score', required: true }],
});
//score.js
var ScoreSchema = new Schema({
score: {type: Number, required: true},
achivement: [{type: String, required: true}],
});
And I try to populate a profile with
Profile.findOne({ _id: mongoose.Types.ObjectId(profile_id) })
.populate('matches')
.populate('matches.scores')
.exec(function(err, profile) {
if (err) {...}
if (profile) {
console.log(profile);
}
});
The matches get populated but I dont get the scores in matches to populate. Is this not supported in mongoose or do I do something wrong? Populate gives me this:
{
user_token: "539b07397c045fc00efc8b84"
username: "username002"
sex: 0
country: "SE"
friends: []
-matches: [
-{
__v: 1
_id: "539eddf9eac17bb8185b950c"
-scores: [
"539ee1c876f274701e17c068"
"539ee1c876f274701e17c069"
"539ee1c876f274701e17c06a"
]
}
]
}
But I want to populate the score array in the match array. Can I do this?
Yes, you are right. I tried using Chaining of populate I got same output.
For your query please use async.js and then populate by the method mentioned below.
For more details please have a look at this code snippet. It is a working, tested, sample code according to your query. Please go through the commented code for better understanding in the code below and the link of the snippet provided.
//Find the profile and populate all matches related to that profile
Profile.findOne({
_id: mongoose.Types.ObjectId(profile_id)
})
.populate('matches')
.exec(function(err, profile) {
if (err) throw err;
//We got the profile and populated matches in Array Form
if (profile) {
// Now there are multiple Matches
// We want to fetch score of each Match
// for that particular profile
// For each match related to that profile
async.forEach(profile.matches, function(match) {
console.log(match, 'match')
// For each match related to that profile
// Populate score achieved by that person
Match.find({
_id:match.id
})
.populate('scores', 'score')
.exec(function (err, score) {
if (err) throw err;
// here is score of all the matches
// played by the person whose profile id
// is passed
console.log(score);
})
})
}
});
Profile.findOne({ _id: mongoose.Types.ObjectId(profile_id) })
.populate('matches.scores')
.exec(function(err, profile) {
if (err) {...}
if (profile) {
console.log(profile);
}
});
I have a problem saving a json object in mongodb(Mongoose) so when i make the insert everything is ok, but when i make the request of the same object, Mongoose return a modified json. Its like Mongoose autocomplete the twitter field and i dont know why.
Here is my code:
UserSchema = mongoose.Schema({
firstName: String,
lastName: String,
email: String,
salt: String,
hash: String,
twitter:{
id: String,
email: String,
name: String
},
facebook:{
id: String,
email: String,
name: String,
username: String,
photo: String,
gender: String
}
});
I save json in my database:
User.create({
email : profile.emails[0].value,
facebook : {
id: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
username: profile.username,
photo: profile.photos[0].value,
gender: profile.gender
}
}, function(err, user){
if(err) throw err;
// if (err) return done(err);
done(null, user);
});
But i when mongoose return a json.
Mongoose generated a field in the json. twitter:{} <----
I dont know why, can anyone lend me a hand?
If you look at the document saved to the MongoDB, you'll see that the twitter object isn't actually present unless the properties of the sub object are set. The object is created so that you can conveniently set properties without needing to worry about creating the sub/nested object. So you can do this:
var user = new User();
user.twitter.id = 'wiredprairie';
user.save();
Results:
{ "_id" : ObjectId("522259394eb9ed0c30000002"),
"twitter" : { "id" : "wiredprairie" }, "__v" : 0 }
And if you went one step further and wanted a more "pure" view of the data, you could use toJSON on the Mongoose model instance:
console.log(user.toJSON());
results:
{ _id: 522259ad3621834411000001, twitter: { id: 'wiredprairie' } }