Mongoose: How to set _id manually before saving? - node.js

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) { ... })

Related

Update multiple documents instance in MongoDb collection with Mongoose. save() is not a function

Mongoose newbe here. I got the following function to update the references (deleting them) in the document Post when a Tag is deleted. When I call my GraphQl API this is what I got:
message": "posts.save is not a function"
The function in my gql resolver:
async deleteTag(root, { id }, context) {
const posts = await Post.find();
const tag = await Tag.findById(id);
if(!tag){
const error = new Error('Tag not found!');
error.code = 404;
throw error;
}
posts?.forEach(async (post) => {
await post.tags.pull(id);
})
await posts.save()
await Tag.findByIdAndRemove(id);
return true;
}
This is the Post model:
const PostSchema = new Schema({
body: {
type: String,
required: true
},
tags: {
type: [Schema.Types.ObjectId],
ref: 'Tag',
required: false
},
});
and this is the Tag model:
const TagSchema = new Schema(
{
name: {
type: String,
required: true
},
},
{ timestamps: true }
);
Looks like I can't call the method save() on the array of objects returned by Exercise.find()
I used the same pattern in other functions, the difference is that there I used .findById()
Any solution? Advice and best practice advide are super welcome.
You have to save the posts individually:
posts?.forEach(async (post) => {
await post.tags.pull(id);
await post.save();
})
Or use Model.updateMany() combined with the $pull operator.
FWIW, you should probably limit the number of matching Post documents by selecting only documents that have the specific tag listed:
await Post.find({ 'tags._id' : id });

Autoincrement with Mongoose

I'm trying to implement an autoicremental user_key field. Looking on this site I came across two questions relevant for my problem but I don't clearly understand what I should do. This is the main one
I have two Mongoose models, this is my ProductsCounterModel.js
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var Counter = new Schema({
_id: {type: String, required: true},
sequence_value: {type: Number, default: 0}
});
module.exports = mongoose.model('products_counter', Counter);
and this is the Mongoose model where I try to implement the auto-increment field:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var products_counter = require('./ProductsCounterModel.js');
var HistoricalProduct = new Schema({
product_key: { type: Number },
class: { type: String },
brand: { type: String },
model: { type: String },
description: { type: String }
});
HistoricalProduct.pre("save", function (next) {
console.log("first console log:",products_counter);
var doc = this;
products_counter.findOneAndUpdate(
{ "_id": "product_key" },
{ "$inc": { "sequence_value": 1 } },
function(error, products_counter) {
if(error) return next(error);
console.log("second console log",products_counter);
doc.product_key = products_counter.sequence_value;
next();
});
});
module.exports = mongoose.model('HistoricalProduct', HistoricalProduct);
Following the steps provided in the above SO answer I created the collection products_counter and inserted one document.
The thing is that I'm getting this error when I try to insert a new product:
"TypeError: Cannot read property 'sequence_value' of null"
This are the outputs of the above console logs.
first console log output:
function model (doc, fields, skipId) {
if (!(this instanceof model))
return new model(doc, fields, skipId);
Model.call(this, doc, fields, skipId);
}
second console log:
Null
can you see what I'm doing wrong?
You can run following line in your middleware:
console.log(products_counter.collection.collectionName);
that line will print products_counters while you expect that your code will hit products_counter. According to the docs:
Mongoose by default produces a collection name by passing the model name to the utils.toCollectionName method. This method pluralizes the name. Set this option if you need a different name for your collection.
So you should either rename collection products_counter to products_counters or explicitly configure collection name in your schema definition:
var Counter = new Schema({
_id: {type: String, required: true},
sequence_value: {type: Number, default: 0}
}, { collection: "products_counter" });

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);

'this' is undefined in a Mongoose pre save hook [duplicate]

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()
})

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