Default value not set while using Update with Upsert as true - node.js

I have the following model for users:
var UserSchema = new mongoose.Schema({
name: String,
dob: Date,
sex: String,
photo: String,
email: {type: String, index: {unique: true, required: true}},
created: {type: Date, default: Date.now}
});
var User = mongoose.model('Users', UserSchema);
As you can see the 'created' field takes a default value of the current date so that it is automatically set when a new user is created.
I use the following query when user details are posted:
User.findOneAndUpdate({email: user.email}, user, {upsert: true}, function (err, user) {
if (err) {
return callback (err);
} else {
callback(null, user);
}
});
The purpose of using findOneAndUpdate with upsert: true is to either return an existing profile, or create a new one. It also updates any fields based on the data posted.
However, the created field gets updated with the current date each time, even though the created field is not posted. How can I make sure that this field is set only once?
EDIT
An example object from the database:
{
"_id" : ObjectId("54620b38b431d48bce7cab81"),
"email" : "someone#google.com",
"__v" : 0,
"name" : "somone",
"sex" : "male"
}
It turns out that the created field is not being set even while creating a new object using upsert. Mongoose just returns the current date based on the schema even though it does not exist in the document.
So, the question now becomes: How do I make sure that using upsert creates the default value for a field not supplied in the arguments?

For adding defaults to your document if it was created with findOneAndUpdate (it didn't exist before the query) and you did not provide the field in the update you should use setDefaultsOnInsert.
When upsert and setDefaultsOnInsert are both true, the defaults will be set if the record is not found and a new one is created.
This skips the workflow of having to check if the record exists and if not then creating a new one with 'save' just to make sure defaults are set.
I have had the same issue (record created with findOneAndUpdate with upsert: true) and the default value for a field was not added to the record, even though it was in the schema.
This is only in regards to adding defaults when using findOneAndUpdate to create documents, not for skipping the update of the 'created' field.
e.g.
User.findOneAndUpdate({email: user.email}, user, {upsert: true, setDefaultsOnInsert:true}, ...)

findOneAndUpdate simply sends a MongoDB findAndModify request (see findOneAndUpdate). What this means is that it skips all the mongoose magic involved with the schema setters, getters, defaults, etc. Validation is only run on create/save so the way around this is to do a .findOne(), check existence/create a new one, and then .save().
see this issue for more discussion
EDIT:
In regards to the first question about changing the date each time, you could change the schema a bit. Get rid of the default value, and instead add this after declaring the schema:
UserSchema.pre("save", function (next) {
if (!this.created) {
this.created = new Date();
}
next();
});
That will only create a date if the created: value is not present, and should prevent it from changing the creation date each time (when using .save()).
see Mongoose middleware

Related

Does Mongoose upsert operation update/renew default schema values?

Mongoose Schema:
new Schema({
...
createDate: { type: Date, default: Date.now },
updateDate: { type: Date, default: Date.now }
});
Upsert operation:
const upsertDoc = {
...
}
Model.update({ key: 123 }, upsertDoc, { upsert: true })
when I upsert with update or findOneAndUpdate the default schema values createDate and updateDate are always renewed no matter document is inserted or updated. It's same when I use $set (in which of course I don't pass dates).
I don't seem to find anything to tell if it's an expected behavior. I expect dates to be added only on insert and not update, unless explicitly set.
If you are looking for "proof" of the expected behavior, then look no further than the source code itself. Particularly within the schema.js main definition:
updates.$setOnInsert = {};
updates.$setOnInsert[createdAt] = now;
}
return updates;
};
this.methods.initializeTimestamps = function() {
if (createdAt && !this.get(createdAt)) {
this.set(createdAt, new Date());
}
if (updatedAt && !this.get(updatedAt)) {
this.set(updatedAt, new Date());
}
return this;
};
this.pre('findOneAndUpdate', _setTimestampsOnUpdate);
this.pre('update', _setTimestampsOnUpdate);
this.pre('updateOne', _setTimestampsOnUpdate);
this.pre('updateMany', _setTimestampsOnUpdate);
}
function _setTimestampsOnUpdate(next) {
var overwrite = this.options.overwrite;
this.update({}, genUpdates(this.getUpdate(), overwrite), {
overwrite: overwrite
});
applyTimestampsToChildren(this);
next();
}
So there you can see all the 'pre' middleware handlers being registered for each of the "update" method variants and to the same functional code. These all essentially modify the $set operator in any "update" you issue to include the updatedAt field, or whatever name you mapped to that key in the schema options.
The actual statement sent with "upsert" actions uses $setOnInsert for the createdAt field or mapped option name ( see the top of the listing ). This action only applies when an "upsert" actually occurs, so documents that exist and are merely matches for any of the "update" methods are never actually touched by this value.
Those operators are part of how MongoDB works and not really to do with mongoose, but the code shown here shows how mongoose "adjusts" your "update" actions in order to include these additional operations.
For reference the whole main function in schema.js which works out what to apply currently begins at Line #798 for the genUpdates() function as called in the bottom part of the listing shown here yet the top part is the last few lines of that function where the keys of $setOnInsert get defined.
So in summary, YES every "update" action is intentional that the updatedAt mapped field has the current Date value assigned, and also that the "updates" are modified to include the $setOnInsert action which only applies when a new document is created as the result of an "upsert" action for the createdAt mapped field.
Well, I'd always recommend to use the provided and recommended way to manage createdAt and updatedAt by mongoose. Simply by passing timeStamp: true as schema options.
This is always a best practice and lets you not to be worried about such behaviors.
I use it and I never see a problem with timestamps using update or findOneAndUpdate.
Here is how you use it
new Schema({
... //Your schema
},{ timestamps: true})

Mongoose returns default value for field even when the field removed from the document

I have a school schema like this:
var SchoolSchema= new mongoose.Schema({
name: {type:String, required: true}
status: { type: String, default: "active" }
});
Mongoose default feature works on two levels:
1) Set field value to default while saving, if the field is not present in the input.
2) While fetching, set field value to default value, if the field is not present in the saved document.
What I wish is for it to set it to default value only while saving, and when fetching a document it should return null value for status if the status property is not present in the record. Currently it returns 'active' when I remove status property from the record.
Is there a way I could do this Mongoose?
What you can do is use the pre-save middleware.
It would look something like this:
schema.pre('save', function(next) {
// Change your fields
next()
})
This code will be activated when you save your mongo document.
Mongoose also adds a isNew field to your object so you could extend this code to only change your fields if this is the first time the document is being save to the DB
schema.pre('save', function(next) {
if (this.isNew) {
// Change your fields
}
next()
})

Mongoose find with default value

I have a mongoose model: (With a field that has a default)
var MySchema= new mongoose.Schema({
name: {
type: String,
required: true
},
isClever: {
type: Boolean,
default: false
}
});
I can save a model of this type by just saving a name and in mongoDB, only name can be seen in the document (and not isClever field). That's fine because defaults happen at the mongoose level. (?)
The problem I am having then is, when trying to retrieve only people called john and isClever = false:
MySchema.find({
'name' : 'john',
'isClever': false
}).exec( function(err, person) {
// person is always null
});
It always returns null. Is this something related to how defaults work with mongoose? We can't match on a defaulted value?
According to Mongoose docs, default values are applied when the document skeleton is constructed.
When you execute a find query, it is passed to Mongo when no document is constructed yet. Mongo is not aware about defaults, so since there are no documents where isClever is explicitly true, that results in empty output.
To get your example working, it should be:
MySchema.find({
'name' : 'john',
'isClever': {
$ne: true
}
})

Purpose of 'isUnique' field in Mongoose

I don't understand the purpose of fields like unique and required in Mongoose schemas. In the case of unique, it appears that you have to write your own methods to query MongoDB to see if the value already exists. I suppose with 'required' Mongoose just needs to check if that value was passed in the constructor of a Mongoose model. But with unique, I don't understand the purpose of that.
userSchema = mongoose.Schema({
username: {
type: String,
unique: true,
required: true,
validate: [validation.usernameValidator, 'not a valid username']
},
...
in order to validate username, I have to create my own function = validation.usernameValidator to check MongoDB to see if the username already exists. So then what is the point of isUnique?
unique will create a MongoDB "unique" index on the property, preventing documents with the same value for that property to exist in the same collection.
However, since it's enforced by the database, the only way this works is by saving a document and catching the duplicate-key-error that MongoDB will return.
You can check for that error like so:
if (err && err.code === 11000) { ...duplicate... }

Mongoose: Add new Schema property and update all current documents

I have at production a NodeJS application running using MongoDB and Mongoose, which inside has a Mongoose Schema like:
var Product = new Schema({
"name": { type: String, required: true }
, "description": { type: String }
});
Now, as a new requirement I'm adding a new property to the Schema at my local, like this:
var Product = new Schema({
"name": { type: String, required: true }
, "description": { type: String }
, "isPublic": { type: Boolean, default: true }
});
When I restart my NodeJS process, I was expecting an update to all of my current documents (products), so now every document have a property isPublic which value is true.
What happened is no document has that new property and if I do a someProduct.Save({ isPublic: true }) it gets added.
Question: is there a way to accomplish that?,
I know I can do a $set from command line with mongo client, but I want to know if there is a way where Mongoose will add the missing property after the Schema changed on a process restart.
What happened is no document has that new property and if I do a someProduct.Save({ isPublic: true }) it gets added.
That's because the mongoose default attribute works just for new documents. There're two workarounds:
Write your code to treat documents without the isPublic property as true;
Or, as you've mentioned above, set the property manually through mongodb console.
Your best bet is to use MongoSH. Adding a new property to a Mongoose Schema will never update existing documents unless the programmer does it.
Use the updateMany command in the Mongo Shell (MongoSH):
// Get all documents in the collection, and set the field to a value
db.users.updateMany( {} ,
{ $set: {"newlyCreatedField": "defaultValue"} }
);

Resources