'this' is undefined in a Mongoose pre save hook [duplicate] - node.js

This question already has an answer here:
Mongoose pre/post midleware can't access [this] instance using ES6
(1 answer)
Closed 6 years ago.
I have made a Mongoose database schema for a User entity, and want to add the current date in an updated_at field. I am trying to use the .pre('save', function() {}) callback but every time I run it I get an error message telling me this is undefined. I've also decided to use ES6, which I guess could be a reason for this (everything works though). My Mongoose/Node ES6 code is below:
import mongoose from 'mongoose'
mongoose.connect("mongodb://localhost:27017/database", (err, res) => {
if (err) {
console.log("ERROR: " + err)
} else {
console.log("Connected to Mongo successfuly")
}
})
const userSchema = new mongoose.Schema({
"email": { type: String, required: true, unique: true, trim: true },
"username": { type: String, required: true, unique: true },
"name": {
"first": String,
"last": String
},
"password": { type: String, required: true },
"created_at": { type: Date, default: Date.now },
"updated_at": Date
})
userSchema.pre("save", (next) => {
const currentDate = new Date
this.updated_at = currentDate.now
next()
})
const user = mongoose.model("users", userSchema)
export default user
The error message is:
undefined.updated_at = currentDate.now;
^
TypeError: Cannot set property 'updated_at' of undefined
EDIT: Fixed this by using #vbranden's answer and changing it from a lexical function to a standard function. However, I then had an issue where, while it wasn't showing the error anymore, it wasn't updating the updated_at field in the object. I fixed this by changing this.updated_at = currentDate.now to this.updated_at = currentDate.

the issue is your arrow function uses lexical this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
change
userSchema.pre("save", (next) => {
const currentDate = new Date
this.updated_at = currentDate.now
next()
})
to
userSchema.pre("save", function (next) {
const currentDate = new Date()
this.updated_at = currentDate.now
next()
})

Related

Node.js: Cast to ObjectId failed for value "new" (type string)

I am rather new to node and am having an issue that is a bit difficult for me to decipher. I am trying to make a reddit clone. The part that I am working on right now is to allow users to view posts other users have posted. I was trying to implement a new route by pasting in this bit of code
app.get('/', (req, res) => {
Post.find({}).lean()
.then((posts) => res.render('posts-index', { posts }))
.catch((err) => {
console.log(err.message);
})
})
However, I am getting this error when I run it:
Cast to ObjectId failed for value "new" (type string) at path "_id" for model "Post"
It seems like something is wrong with my post.js model, but I can't identify it. Could someone review this and help me identify the triggering factor?
const { Schema, model } = require('mongoose');
const postSchema = new Schema({
title: { type: String, required: true },
url: { type: String, required: true },
summary: { type: String, required: true },
}, { timestamps: true });
module.exports = model('Post', postSchema);

Mongoose: How to set _id manually before saving?

With the following code given:
const schema = new Schema({
_id: {
type: String
},
name: {
type: String,
required: true,
trim: true
}
}
schema.pre('validate', (next) => {
console.log(this.name);
this._id = crypto.createHash('md5').update(this.name).digest("hex");
next();
});
const myObject = new MyObject({ name: 'SomeName' });
myObject.save();
The application throws this error message:
MongooseError: document must have an _id before saving
My Question is, how is it possible to set the _id manually for a model?
And why is this.name undefined
(next) => ... is arrow function where this is lexical and refers to enclosing scope, which is module.exports in Node.js module scope.
In order to get dynamic this inside a function, it should be regular function:
schema.pre('validate', function (next) { ... })

Prevent field modification with Mongoose Schema

Is there any way to set a field with an "unmodifiable" setting (Such as type, required, etc.) when you define a new Mongoose Schema? This means that once a new document is created, this field can't be changed.
For example, something like this:
var userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unmodifiable: true
}
})
From version 5.6.0 of Mongoose, we can use immutable: true in schemas (exactly as the aforementioned answer on mongoose-immutable package). Typical use case is for timestamps, but in your case, with username it goes like this:
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
immutable: true
}
});
If you try to update the field, modification will be ignored by Mongoose.
Going a little further than what have been asked by OP, now with Mongoose 5.7.0 we can conditionally set the immutable property.
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
immutable: doc => doc.role !== 'ADMIN'
},
role: {
type: String,
default: 'USER',
enum: ['USER', 'MODERATOR', 'ADMIN'],
immutable: true
}
});
Sources: What's New in Mongoose 5.6.0: Immutable Properties and What's New in Mongoose 5.7: Conditional Immutability, Faster Document Arrays.
Please be aware that the documentation explicitly states that when using functions with update in their identifier/name, the 'pre' middleware is not triggered:
Although values are casted to their appropriate types when using update, the following are not applied:
- defaults
- setters
- validators
- middleware
If you need those features, use the traditional approach of first retrieving the document.
Model.findOne({ name: 'borne' }, function (err, doc) {
if (err) ..
doc.name = 'jason bourne';
doc.save(callback);
})
Therefore either go with the above way by mongooseAPI, which can trigger middleware (like 'pre' in desoares answer) or triggers your own validators e.g.:
const theOneAndOnlyName = 'Master Splinter';
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
default: theOneAndOnlyName
validate: {
validator: value => {
if(value != theOneAndOnlyName) {
return Promise.reject('{{PATH}} do not specify this field, it will be set automatically');
// message can be checked at error.errors['username'].reason
}
return true;
},
message: '{{PATH}} do not specify this field, it will be set automatically'
}
}
});
or always call any update functions (e.g. 'findByIdAndUpdate' and friends) with an additional 'options' argument in the form of { runValidators: true } e.g.:
const splinter = new User({ username: undefined });
User.findByIdAndUpdate(splinter._id, { username: 'Shredder' }, { runValidators: true })
.then(() => User.findById(splinter._id))
.then(user => {
assert(user.username === 'Shredder');
done();
})
.catch(error => console.log(error.errors['username'].reason));
You can also use the validator function in a non-standard way i.e.:
...
validator: function(value) {
if(value != theOneAndOnlyName) {
this.username = theOneAndOnlyName;
}
return true;
}
...
This does not throw a 'ValidationError' but quietly overrides the specified value. It still only does so, when using save() or update functions with specified validation option argument.
I had the same problem with field modifications.
Try https://www.npmjs.com/package/mongoose-immutable-plugin
The plugin will reject each modification-attempt on a field and it works for
Update
UpdateOne
FindOneAndUpdate
UpdateMany
Re-save
It supports array, nesting objects, etc. types of field and guards deep immutability.
Plugin also handles update-options as $set, $inc, etc.
You can do it with Mongoose only, in userSchema.pre save:
if (this.isModified('modified query')) {
return next(new Error('Trying to modify restricted data'));
}
return next();
You can use Mongoose Immutable. It's a small package you can install with the command below, it allows you to use the "immutable" property.
npm install mongoose-immutable --save
then to use it:
var userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
immutable: true
}
});
userSchema.plugin(immutablePlugin);

Mongoose TypeError: User is not a constructor

I'm trying to add a subdocument to a parent schema with Mongoose and MongoDB however I'm being thrown the following error:
TypeError: User is not a constructor
This is based off Mongoose's documentation on subdocuments and I think everything is the same. How can I debug this further?
Router
// Add a destination to the DB
router.post('/add', function(req, res, next) {
let airport = req.body.destination
let month = req.body.month
let id = (req.user.id)
User.findById(id , function (err, User) {
if (err) return handleError(err)
function addToCart (airport, month, id) {
var user = new User ({
destinations: [(
airport = '',
month = ''
)]
})
dog.destinations[0].airport = airport
dog.destinations[0].month = month
dog.save(callback)
res.status(200).send('added')
}
addToCart()
})
console.log(airport)
})
Schema
var destinationSchema = new Schema({
airport: String,
month: String
})
// Define the scheme
var User = new Schema ({
firstName: {
type: String,
index: true
},
lastName: {
type: String,
index: true
},
email: {
type: String,
index: true
},
homeAirport: {
type: String,
index: true
},
destinations: [destinationSchema]
})
User.plugin(passportLocalMongoose)
module.exports = mongoose.model('User', User)
JavaScript is case sensitive about the variable names. You have User model and the User result with the same name.
Your code will work with the following change :
User.findById(id , function (err, user) {
/* ^ use small `u` */
if (err) return handleError(err)
/* rest of your code */
Also keep in mind that further in your code you are declaring another variable named user. You will need to change that to something different.

schema mongoose Required field if otherfield is true [duplicate]

I want to use mongoose custom validation to validate if endDate is greater than startDate. How can I access startDate value? When using this.startDate, it doesn't work; I get undefined.
var a = new Schema({
startDate: Date,
endDate: Date
});
var A = mongoose.model('A', a);
A.schema.path('endDate').validate(function (value) {
return diff(this.startDate, value) >= 0;
}, 'End Date must be greater than Start Date');
diff is a function that compares two dates.
You can do that using Mongoose 'validate' middleware so that you have access to all fields:
ASchema.pre('validate', function(next) {
if (this.startDate > this.endDate) {
next(new Error('End Date must be greater than Start Date'));
} else {
next();
}
});
Note that you must wrap your validation error message in a JavaScript Error object when calling next to report a validation failure.
An an alternative to the accepted answer for the original question is:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
// schema definition
var ASchema = new Schema({
startDate: {
type: Date,
required: true
},
endDate: {
type: Date,
required: true,
validate: [dateValidator, 'Start Date must be less than End Date']
}
});
// function that validate the startDate and endDate
function dateValidator(value) {
// `this` is the mongoose document
return this.startDate <= value;
}
I wanted to expand upon the solid answer from #JohnnyHK (thank you) by tapping into this.invalidate:
Schema.pre('validate', function (next) {
if (this.startDate > this.endDate) {
this.invalidate('startDate', 'Start date must be less than end date.', this.startDate);
}
next();
});
This keeps all of the validation errors inside of a mongoose.Error.ValidationError error. Helps to keep error handlers standardized. Hope this helps.
You could try nesting your date stamps in a parent object and then validate the parent. For example something like:
//create a simple object defining your dates
var dateStampSchema = {
startDate: {type:Date},
endDate: {type:Date}
};
//validation function
function checkDates(value) {
return value.endDate < value.startDate;
}
//now pass in the dateStampSchema object as the type for a schema field
var schema = new Schema({
dateInfo: {type:dateStampSchema, validate:checkDates}
});
Using 'this' within the validator works for me - in this case when checking the uniqueness of email address I need to access the id of the current object so that I can exclude it from the count:
var userSchema = new mongoose.Schema({
id: String,
name: { type: String, required: true},
email: {
type: String,
index: {
unique: true, dropDups: true
},
validate: [
{ validator: validator.isEmail, msg: 'invalid email address'},
{ validator: isEmailUnique, msg: 'Email already exists'}
]},
facebookId: String,
googleId: String,
admin: Boolean
});
function isEmailUnique(value, done) {
if (value) {
mongoose.models['users'].count({ _id: {'$ne': this._id }, email: value }, function (err, count) {
if (err) {
return done(err);
}
// If `count` is greater than zero, "invalidate"
done(!count);
});
}
}
This is the solution I used (thanks to #shakinfree for the hint) :
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
// schema definition
var ASchema = new Schema({
dateSchema : {
type:{
startDate:{type:Date, required: true},
endDate:{type:Date, required: true}
},
required: true,
validate: [dateValidator, 'Start Date must be less than End Date']
}
});
// function that validate the startDate and endDate
function dateValidator (value) {
return value.startDate <= value.endDate;
}
module.exports = mongoose.model('A', ASchema);

Resources