Mongoose.js: Atomic update of nested properties? - node.js

Using Mongoose version 3.6.4
Say I have a MongoDB document like so:
{
"_id" : "5187b74e66ee9af96c39d3d6",
"profile" : {
"name" : {
"first" : "Joe",
"last" : "Pesci",
"middle" : "Frank"
}
}
}
And I have the following schema for Users:
var UserSchema = new mongoose.Schema({
_id: { type: String },
email: { type: String, required: true, index: { unique: true }},
active: { type: Boolean, required: true, 'default': false },
profile: {
name: {
first: { type: String, required: true },
last: { type: String, required: true },
middle: { type: String }
}
}
created: { type: Date, required: true, 'default': Date.now},
updated: { type: Date, required: true, 'default': Date.now}
);
And I submit a form passing a field named: profile[name][first] with a value of Joseph
and thus I want to update just the user's first name, but leave his last and middle alone, I thought I would just do:
User.update({email: "joe#foo.com"}, req.body, function(err, result){});
But when I do that, it "deletes" the profile.name.last and profile.name.middle properties and I end up with a doc that looks like:
{
"_id" : "5187b74e66ee9af96c39d3d6",
"profile" : {
"name" : {
"first" : "Joseph"
}
}
}
So it's basically overwriting all of profile with req.body.profile, which I guess makes sense. Is there any way around it without having to be more explicit by specifying my fields in the update query instead of req.body?

You are correct, Mongoose converts updates to $set for you. But this doesn't solve your issue. Try it out in the mongodb shell and you'll see the same behavior.
Instead, to update a single deeply nested property you need to specify the full path to the deep property in the $set.
User.update({ email: 'joe#foo.com' }, { 'profile.name.first': 'Joseph' }, callback)

One very easy way to solve this with Moongose 4.1 and the flat package:
var flat = require('flat'),
Schema = mongoose.Schema,
schema = new Schema(
{
name: {
first: {
type: String,
trim: true
},
last: {
type: String,
trim: true
}
}
}
);
schema.pre('findOneAndUpdate', function () {
this._update = flat(this._update);
});
mongoose.model('User', schema);
req.body (for example) can now be:
{
name: {
first: 'updatedFirstName'
}
}
The object will be flattened before the actual query is executed, thus $set will update only the expected properties instead of the entire name object.

I think you are looking for $set
http://docs.mongodb.org/manual/reference/operator/set/
User.update({email: "joe#foo.com"}, { $set : req.body}, function(err, result){});
Try that

Maybe it's a good solution - add option to Model.update, that replace nested objects like:
{field1: 1, fields2: {a: 1, b:2 }} => {'field1': 1, 'field2.a': 1, 'field2.b': 2}
nestedToDotNotation: function(obj, keyPrefix) {
var result;
if (keyPrefix == null) {
keyPrefix = '';
}
result = {};
_.each(obj, function(value, key) {
var nestedObj, result_key;
result_key = keyPrefix + key;
if (!_.isArray(value) && _.isObject(value)) {
result_key += '.';
nestedObj = module.exports.nestedToDotNotation(value, result_key);
return _.extend(result, nestedObj);
} else {
return result[result_key] = value;
}
});
return result;
}
});
need improvements circular reference handling, but this is really useful when working with nested objects
I'm using underscore.js here, but these functions easily can be replaced with other analogs

Related

mongoDB returning with $numberDecimal in the response of a query

Below is my mongoose schema
const mongoose = require('mongoose');
const AccountingCostsSchema = mongoose.Schema(
{
// other properties goes here
accountCosts: {
type: mongoose.Decimal128,
set: (v) =>
mongoose.Types.Decimal128.fromString(parseFloat(v).toFixed(4)),
required: true
}
},
{
collection: 'accountingCosts'
}
);
export = mongoose.model('AccountingCosts', AccountingCostsSchema);
Data in MongoDB
accountingCosts collection
Data in Text mode view
{
"_id" : ObjectId("4e1eb38c2bdea2fb81f5e771"),
"accountId" : ObjectId("4e8c1180d85de1704ce12110"),
"accountCosts" : NumberDecimal("200.00"),
}
Data in Text mode view
My query
db.getCollection('accountingCosts').find({'accountId': '4e8c1180d85de1704ce12110'})
Result from query
"accountCosts": {
"$numberDecimal": "123.00"
}
I tried writing a getter function on schema like i have a setter function. But it is not working
get: function(value) {
return value.toString();
}
My expected output is just a plain property with name and value like below
"accountCosts": "123.00"
I am sure you found a solution, but i will also write it here so who will land here would find a solution.
Your idea to using a getter was right, but maybe you forgot to enable it in the schema, so let' s see how using your code.
You have this schema:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0
}
});
So when you retrieve it you will get:
{
"_id" : "12345",
"accountId" : "123456",
"accountCosts" : {
"$numberDecimal": "123.00"
}
}
Taking in example that you would get accountCosts as number, so something like that:
{
"_id" : "12345",
"accountId" : "123456",
"accountCosts" : 123
}
We would need a getter as you said, so we will need to add a function of this getter in our model file, let' s say after your schema. This could be your getter function:
function getCosts(value) {
if (typeof value !== 'undefined') {
return parseFloat(value.toString());
}
return value;
};
Now, let' s declare this getter in our schema that will become:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
}
});
Now mongoose will uderstand how you want that value is returned, but we have to specify that we want it uses the getter. So we have to enable it when we want to retieve the value in json format. We can simply add it as this:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
}
}, {toJSON: {getters: true}});
And so, when you will retrieve it, you will get this:
{
"_id" : "12345",
"accountId" : "123456",
"accountCosts" : 123,
"id" : "12345"
}
But, whoa (this is not my batman glass!) what is that "id" : "12345"?
According to Mongoose documents:
Mongoose assigns each of your schemas an id virtual getter by default which returns the document's _id field cast to a string, or in the case of ObjectIds, its hexString. If you don't want an id getter added to your schema, you may disable it by passing this option at schema construction time.
How we can avoid it? We can just add id: false in our schema that now will be:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
},
id: false
}, {toJSON: {getters: true}});
And now you will not see it anymore.
Just a little last tip (no, i have not finished):
What happens if you have an array of objects in your schema like this?
var AccountingCostsSchema = new Schema({
accountId: String,
usersAndCosts: [{
username: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
}
],
id: false
}, {toJSON: {getters: true}});
Bad news now: you can not keep on using this schema. Good news now: you can fix it easly. You need to create a new schema for usersAndCosts and then reference it in the parent schema. Let' s see it:
var usersAndCostsSchema = new Schema({
username: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
},
id: false
}, {toJSON: {getters: true}});
var AccountingCostsSchema = new Schema({
accountId: String,
usersAndCosts: [usersAndCostsSchema],
id: false
}, {toJSON: {getters: true}});
The result will be the same, your accountCosts will be shown as number, no double ids.
Remember that of course this is to enable getters for toJSON but you could need to enable them for toObject too, as you could need to do the same for setters. But we here talked about getters for JSON.

Unable insert more than 1 entry in MongoDB

Here is my schema. user_id and other_id are supposed to be unique (composite).
var mongoose = require("mongoose");
var uniqueValidator = require('mongoose-unique-validator');
var Schema = mongoose.Schema;
var FriendshipSchema = new Schema({
user_id: {
type: String,
default: "",
trim: true,
unique:true,
},
other_id: {
type: String,
default: "",
unique:true,
},
status: {
type: String,
index: true,
default: "none",
},
});
FriendshipSchema.plugin(uniqueValidator);
module.exports = mongoose.model('Friendship', FriendshipSchema)
and here is my server-side code. pretty straightforward insert using Mongoose.
app.post('/api/user/friendrequest', function(req, res){
var friendship = new Friendship(req.body);
console.log(req.body);
Friendship.find({}, function (err, docs) {
if (docs.length){
console.log('abac');
}else{
friendship.save(function(err){
if (err)
{
console.log(err)
}
});
}
});
});
I get this response in console but no more than 1 entry is saved in MongoDB. I've deleted indexes as well and its still not working. btw 'user_id' is unique in another Collection. I also don't get any error when i log console.log(err).
{ user_id: 'google-oauth2|117175967810648931400',
status: 'pending',
other_id: 'facebook|10209430751350509' }
abac
Here are the indexes for the friendships collection.
db.friendships.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "kola.friendships"
}
]
What you want is the "combination" of fields to be "unique" here rather than be treated individually as such.
That means your schema should instead be defined like this:
var FriendshipSchema = new Schema({
user_id: {
type: String,
default: "",
trim: true,
},
other_id: {
type: String,
default: "",
},
status: {
type: String,
index: true,
default: "none",
},
});
// Instead define the schema level index here
FriendshipShema.index({ "user_id": 1, "other_id": 1 },{ "unique": true });
module.exports = mongoose.model('Friendship', FriendshipSchema);
The best part of this is that you don't need any plugin to support what you want to do.
Please do make sure you run a .dropIndexes() on the collection to get rid of any individual "unique" indexes that will interfere with the correct operation.
Also see .createindex() and "Unique Indexes" in the core documentation for more information.

Mongoose Populate Returns Some Empty Objects

I have 1 main collection and 1 collection with a ref to the main one. Code looks like :
// Ref schema
const onlineSchema = mongoose.Schema({
_id: {
type: Number,
ref: 'Player',
unique: true
}
}, {
timestamps: true
});
//main schema
const playerSchema = mongoose.Schema({
_id: { // User ID
type: Number,
required: true,
unique: true,
default: 0
},
firstname: {
type: String
},
name: {
type: String,
required: true
},
lastname: {
type: String
},
barfoo: {
type: Boolean
}
...
})
I populate it with this code :
var baz = bar;
...
Online.find().populate({
path: '_id',
match: {
[ baz + 'foo']: true
}
}).exec(function(err, online) {
if (err) {
winston.error(err);
} else {
winston.error(util.inspect(online, {
showHidden: false,
depth: null
}));
}
});
If there are 10 elements in online and only 7 match [ baz + 'foo']: true I get 7 proper arrays and 3 empty arrays that look like this:
{ updatedAt: 2016-12-23T18:00:32.725Z,
createdAt: 2016-12-23T18:00:32.725Z,
_id: null,
__v: 0 },
Why is this happening and how to I filter the final result so it only shows the matching elements?
I can use filter to remove the null arrays after I get the result but I'd like to know how to prevent the the query from passing null arrays in the first place.
Why is this happening ?
This is happening because you get all the documents with Online.find() but the player will be populated only for records that match your condition. Your match is for the populate, not for the find() query.
How do I filter the final result so it only shows the matching
elements ?
You cant find a nested elements of a referenced collections since there is no join in MongoDB. But you can :
keep your schema and use aggregation with $lookup :
Online.aggregate(
[{
$lookup: {
from: "players",
localField: "_id",
foreignField: "_id",
as: "players"
}
}, {
$unwind: "$players"
}, {
$match: {
'players.barfoo': true
}
}],
function(err, result) {
console.log(result);
});
change your schema to include Player as a subdocument :
const playerSchema = new mongoose.Schema({
//...
});
const onlineSchema = new mongoose.Schema({
player: playerSchema
}, {
timestamps: true
});
var Online = mongoose.model('Online', onlineSchema);
Online.find({'player.barfoo':true}).exec(function(err, online) {
console.log(online);
});
Dont make _id the reference of another schema, instead make another field name player and give reference through that.
const onlineSchema = mongoose.Schema({
player: {
type: Number,
ref: 'Player',
unique: true
}
}, {
timestamps: true
});
Population:
Online.find().populate({
path: 'player',
match: {
[ baz + 'foo']: true
}
}).exec(...);
dont use _id to ref field.. because its default filed in mongoDB to create index unique.. change you're field name

NodeJs MongoDb Mongoose - How to Delete item from Multi Dimensional Array?

//MONGOOSE SCHEMA OBJECT
var userSchema = new Schema( {
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
tags:[ {name : String, color : String } ],
bookmarks:[{link : String, tags:[ {name : String, color : String } ]}]
} );
module.exports = userSchema; //Export the userSchema
var UserModel = mongoose.model('UserModel', userSchema ); //Create Model
module.exports = UserModel; //Export the Model
//I CAN DELETE AN ITEM FROM BOOKMARKS ARRAY NO PROBLEM USING
UserModel.findByIdAndUpdate(userId ,{$push : {bookmarks: {link : req.body.link, tags : req.body.tags}}}, function(err, user_data) {
PROBLEM!!
How do I delete a tag from the tags Array, within the bookmarks array given users _id, bookmarks _id and the tag _id or name?
//I have tried variations of the following without success
var update = {bookmarks:[{ _id : bookmarkId},
$pull: {tags:[_id : tagid ] }] };
UserModel.findByIdAndUpdate(userId ,update, function(err, user_data) {
AND
UserModel.findOne( {_id : userId}).select({ bookmarks:[ { $elemMatch: {_id : req.params.bookmarkId}}] }).exec(function(err, user_data)
Initially I was using different Models and subdocuments.
var bookmarkSchema = new Schema( {
link : String,
tags:[tagSchema]
});
var tagSchema = new Schema( {
name : String,
color : String
});
var userSchema = new Schema( {
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
tags:[ {name : String, color : String } ],
bookmarks: [bookmarkSchema]
} );
However, I was unable to delete items from the subdocuments using the $pull command like I used above. So I reverted to just using one schema/model.
This is a very important task to be able to complete and I would be greatful for help.
Thanks Muhammad but I could not get either of the 2 methods to work:
1) Nothing happens to DB and the values of the callbacks are:
*numberAffected: 0
*raw: {"updatedExisting":false,"n":0,"connectionId":322,"err":null,"ok":1}
UserModel.update({bookmarks:req.params.bookmarkId},
{ $pull: {"bookmarks.tags" :{_id:req.body._id, name :req.body.name ,color:req.body.color}}}, function(err, numberAffected, raw) {
2) I had to use the lean() function to convert Mongoose Document data format to normal JSON. Otherwise bookmarks.push({link:user.bookmarks[i].link,_id:user.bookmarks[i]._id,tags:tags})
would not combine properly with:
bookmarks.push(user.bookmarks[i]);
on bookmarks[]
However using the lean() functions means I would not be able to save the data to the DB with .save
UserModel.findById(userId).lean(true).exec(function(err, user) {
delete from sub-Document of JSON doc,
UserModel.update({bookmarks:bookmarkId},{$pull: {"bookmarks.tags" :{_id:tagsId,name :name ,color:color}}}, function(err, user_data) {
or
UserModel.findOnd(userId,function(er,user){
var bookmarks= [];
var tags = [];
for (var i = 0;i<user.bookmarks.length;i++){
if (user.bookmarks[i]._id==bookmarkId)
{
for (var j = 0;j<user.bookmarks[i].tags.length;j++){
if (user.bookmarks[i].tags[j]._id==tagid )
{
}else {
tags.push(user.bookmarks[i].tags[j])
}
}
bookmarks.push({link:user.bookmarks[i].link,_id:user.bookmarks[i]._id,tags:tags})
}
else {
bookmarks.push(user.bookmarks[i]);
}
}
})

How can I auto-case key names when saving to Mongoose?

I have an object:
{ SKU: 'TR1234',
Description: 'Item 1',
UoM: 'each',
client_id: '531382e3005fe0c926bd3957',
Meta: { Test: 'test1', Image: 'http://www.aol.com' } }
I'm trying to save it given my schema:
var ItemSchema = new Schema({
sku: {
type: String,
trim: true,
},
description: {
type: String,
trim: true,
},
company_id: {
type: Schema.ObjectId,
ref: 'Client',
},
createdOn: {
type: Date,
default: Date.now
},
updatedOn: {
type: Date,
default: Date.now
}
}, {versionKey: false});
But it doesn't save and I assume it's because of the capitalized key names. However, those are dynamically generated from a CSV which is parsed with https://github.com/Keyang/node-csvtojson
Ideas?
You can also just use a setter in your mongoose schema, like that:
function toLower (v) {
return v.toLowerCase();
}
var UserSchema = new Schema({
email: { type: String, set: toLower }
});
Just apply it to your fields.
There is also one more approach, just:
email : { type: String, lowercase: true }
Update for keys:
If you would like to change keys, you should the approach likes 'ecdeveloper' mentioned below. My answer was for values, so it makes sense to give this reputation to 'ecdeveloper'. Sorry for confusing.
Here is one more approach without creating a new object:
Object.prototype.keysToUpper = function () {
var k;
for (k in this) {
if (this.hasOwnProperty(k))
this[k.toLowerCase()] = this[k];
delete this[k];
}
return this;
};
What about calling toLowerCase() on each key from your object, and build a new object with lower case keys?
// Assumy your object name is obj
var newObj = {};
Object.keys(obj).forEach(function(key) {
newObj[key.toLowerCase()] = obj[key];
});
// Here you can save your newObj

Resources