Mongoose custom validation of several fields on update - javascript

First of all, this doesn't help.
Let's say, we have a User model:
const schema = new mongoose.Schema({
active: { type: Boolean },
avatar: { type: String }
});
const User = mongoose.model('User', schema);
When we update it (set an avatar):
// This should pass validation
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
We want to validate it based on current (or changed) active attribute value.
Case #1
active is false
we should not be able to set avatar - it should not pass the validation
Case #2
active is true
we should be able to set avatar - it should pass the validation
Ideas
Use a custom validator
const schema = new mongoose.Schema({
active: { type: Boolean },
avatar: { type: String, validate: [validateAvatar, 'User is not active'] }
});
function validateAvatar (value) {
console.log(value); // user.avatar
console.log(this.active); // undefined
}
So this will not work as we don't have an access to active field.
Use pre "validate" hook
schema.pre('validate', function (next) {
// this will never be called
});
This hook doesn't work with update method.
Use pre "update" hook
schema.pre('update', function (next) {
console.log(this.active); // undefined
});
This will not work for us as it doesn't have an access to model fields.
Use post "update" hook
schema.post('update', function (next) {
console.log(this.active); // false
});
This one works, but in terms of validation is not quite good choice, as the function is being called only when model was already saved.
Question
So is there a way to validate the model based on several fields (both saved in DB and new ones) before saving it, while using model.update() method?
As a summary:
Initial user object
{ active: false, avatar: null }
Update
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
Validation should have an access to
{ active: false, avatar: 'user1.png' }
If validation fails, changes should not be passed to DB

Due to limitation of working with update() I've decided to solve the problem this way:
Use custom validators (idea #1 mentioned in the question)
Don't use update()
So instead of
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
I use
User.findOne({ _id: id })
.then((user) => {
user.avatar = 'user1.png';
user.save();
});
In this case custom validators work as expected.
P.S. I choose this answer as a correct one for me, but I will give bounty to the most relevant answer.

You can do this with the context option specified in the mongoose documentation.
The context option
The context option lets you set the value of this in update validators
to the underlying query.
So in your code you can define your validator on the path like this:
function validateAvatar (value) {
// When running update validators with the `context` option set to
// 'query', `this` refers to the query object.
return this.getUpdate().$set.active;
}
schema.path('avatar').validate(validateAvatar, 'User is not active');
And while updating you need to enter two options runValidators and context. So your update query becomes:
var opts = { runValidators: true, context: 'query' };
user.update({ _id: id }, { $set: { avatar: 'user1.png' }, opts });

Did you try giving active a default value so it would not be undefined in mongodb.
const schema = new mongoose.Schema({
active: { type: Boolean, 'default': false },
avatar: { type: String,
trim: true,
'default': '',
validate: [validateAvatar, 'User is not active']
}});
function validateAvatar (value) {
console.log(value); // user.avatar
console.log(this.active); // undefined
}
When creating do you set the user in this way
var User = mongoose.model('User');
var user_1 = new User({ active: false, avatar: ''});
user_1.save(function (err) {
if (err) {
return res.status(400).send({message: 'err'});
}
res.json(user_1);
});

You can try with pre "save" hook. I used it before and can get the value in "this".
schema.pre('save', function (next) {
console.log(this.active);
});
Hope this work with you too !

You have to use an asynchronous custom validator for that:
const schema = new mongoose.Schema({
active: { type: Boolean },
avatar: {
type : String,
validate : {
validator : validateAvatar,
message : 'User is not active'
}
}
});
function validateAvatar(v, cb) {
this.model.findOne({ _id : this.getQuery()._id }).then(user => {
if (user && ! user.active) {
return cb(false);
} else {
cb();
}
});
}
(and pass the runValidators and context options to update(), as suggested in the answer from Naeem).
However, this will require an extra query for each update, which isn't ideal.
As an alternative, you could also consider using something like this (if the constraint of not being able to update inactive users is more important than actually validating for it):
user.update({ _id : id, active : true }, { ... }, ...);

Related

Update or push object into nested array in found document mongoose

I have this mongoose Schema:
const UserSchema = new mongoose.Schema({
name: {
type: String,
trim: true
},
userId: {
type: String,
required: [true, "user ID required."],
unique: [true, "user ID must be unique"]
},
votes: [
{
pollId: type: mongoose.Schema.Types.ObjectId,
candidates: [
{
candidateId: mongoose.Schema.Types.ObjectId
}
]
}
],
role: {
type: String,
enum: ["user", "admin"],
default: "user"
}
});
I find user first, because I want to authorize the user. and now I want to update some parts. just want to be clear, I want to update some parts of a found user document.
I have multiple polls that user can vote for multiple candidates in each one. so if user has not voted at all, votes array will be empty, and we have to push first pollId and also first candidateId that he/she votes. and if the pollId exists,we have to find that subdocument first by pollId then we should just add candidateId inside candidates array.
how can I do this? preferred is just one operation not multiple. and if I can get updated user its better.
if it's not clear let me know. I'll try to explain more.
thanks.
I would do something like this
function updateUsersVotes(userId, newVote) {
User.findById(userId)
.exec()
.then((dbUser) => {
if (!dbUser.votes.length) { // no votes yet
dbUser.votes.push(newVote);
} else { // check current votes
const voteIndex = dbUser.votes.findIndex((vote) => vote.pollId.toString() === newVote.pollId.toString());
if (voteIndex > 0) { // if the incoming vote pollId matches an existing pollId
dbUser.votes[voteIndex].candidates.push(...newVote.candidates);
} else {
dbUser.votes.push(newVote); // if the incoming pollId can't be found, then add it.
}
}
dbUser.save({ validateBeforeSave: true }); // update the user.
})
.catch((error) => {
handleError(error); // you should always handle your errors.
});
}

How to make custom incremental id or number in MongoDB? [duplicate]

According to this mongodb article it is possible to auto increment a field and I would like the use the counters collection way.
The problem with that example is that I don't have thousands of people typing the data in the database using the mongo console. Instead I am trying to use mongoose.
So my schema looks something like this:
var entitySchema = mongoose.Schema({
testvalue:{type:String,default:function getNextSequence() {
console.log('what is this:',mongoose);//this is mongoose
var ret = db.counters.findAndModify({
query: { _id:'entityId' },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
}
});
I have created the counters collection in the same database and added a page with the _id of 'entityId'. From here I am not sure how to use mongoose to update that page and get the incrementing number.
There is no schema for counters and I would like it to stay that way because this is not really an entity used by the application. It should only be used in the schema(s) to auto increment fields.
Here is an example how you can implement auto-increment field in Mongoose:
var CounterSchema = Schema({
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
testvalue: {type: String}
});
entitySchema.pre('save', function(next) {
var doc = this;
counter.findByIdAndUpdate({_id: 'entityId'}, {$inc: { seq: 1} }, function(error, counter) {
if(error)
return next(error);
doc.testvalue = counter.seq;
next();
});
});
You can use mongoose-auto-increment package as follows:
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
/* connect to your database here */
/* define your CounterSchema here */
autoIncrement.initialize(mongoose.connection);
CounterSchema.plugin(autoIncrement.plugin, 'Counter');
var Counter = mongoose.model('Counter', CounterSchema);
You only need to initialize the autoIncrement once.
The most voted answer doesn't work. This is the fix:
var CounterSchema = new mongoose.Schema({
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
sort: {type: String}
});
entitySchema.pre('save', function(next) {
var doc = this;
counter.findByIdAndUpdateAsync({_id: 'entityId'}, {$inc: { seq: 1} }, {new: true, upsert: true}).then(function(count) {
console.log("...count: "+JSON.stringify(count));
doc.sort = count.seq;
next();
})
.catch(function(error) {
console.error("counter error-> : "+error);
throw error;
});
});
The options parameters gives you the result of the update and it creates a new document if it doesn't exist.
You can check here the official doc.
And if you need a sorted index check this doc
So combining multiple answers, this is what I ended up using:
counterModel.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const counterSchema = new Schema(
{
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
}
);
counterSchema.index({ _id: 1, seq: 1 }, { unique: true })
const counterModel = mongoose.model('counter', counterSchema);
const autoIncrementModelID = function (modelName, doc, next) {
counterModel.findByIdAndUpdate( // ** Method call begins **
modelName, // The ID to find for in counters model
{ $inc: { seq: 1 } }, // The update
{ new: true, upsert: true }, // The options
function(error, counter) { // The callback
if(error) return next(error);
doc.id = counter.seq;
next();
}
); // ** Method call ends **
}
module.exports = autoIncrementModelID;
myModel.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const autoIncrementModelID = require('./counterModel');
const myModel = new Schema({
id: { type: Number, unique: true, min: 1 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date },
someOtherField: { type: String }
});
myModel.pre('save', function (next) {
if (!this.isNew) {
next();
return;
}
autoIncrementModelID('activities', this, next);
});
module.exports = mongoose.model('myModel', myModel);
Attention!
As hammerbot and dan-dascalescu pointed out this does not work if you remove documents.
If you insert 3 documents with id 1, 2 and 3 - you remove 2 and insert another a new one it'll get 3 as id which is already used!
In case you don't ever remove documents, here you go:
I know this has already a lot of answers, but I would share my solution which is IMO short and easy understandable:
// Use pre middleware
entitySchema.pre('save', function (next) {
// Only increment when the document is new
if (this.isNew) {
entityModel.count().then(res => {
this._id = res; // Increment count
next();
});
} else {
next();
}
});
Make sure that entitySchema._id has type:Number.
Mongoose version: 5.0.1.
This problem is sufficiently complicated and there are enough pitfalls that it's best to rely on a tested mongoose plugin.
Out of the plethora of "autoincrement" plugins at http://plugins.mongoosejs.io/, the best maintained and documented (and not a fork) is mongoose sequence.
I've combined all the (subjectively and objectively) good parts of the answers, and came up with this code:
const counterSchema = new mongoose.Schema({
_id: {
type: String,
required: true,
},
seq: {
type: Number,
default: 0,
},
});
// Add a static "increment" method to the Model
// It will recieve the collection name for which to increment and return the counter value
counterSchema.static('increment', async function(counterName) {
const count = await this.findByIdAndUpdate(
counterName,
{$inc: {seq: 1}},
// new: return the new value
// upsert: create document if it doesn't exist
{new: true, upsert: true}
);
return count.seq;
});
const CounterModel = mongoose.model('Counter', counterSchema);
entitySchema.pre('save', async function() {
// Don't increment if this is NOT a newly created document
if(!this.isNew) return;
const testvalue = await CounterModel.increment('entity');
this.testvalue = testvalue;
});
One of the benefits of this approach is that all the counter related logic is separate. You can store it in a separate file and use it for multiple models importing the CounterModel.
If you are going to increment the _id field, you should add its definition in your schema:
const entitySchema = new mongoose.Schema({
_id: {
type: Number,
alias: 'id',
required: true,
},
<...>
});
test.pre("save",function(next){
if(this.isNew){
this.constructor.find({}).then((result) => {
console.log(result)
this.id = result.length + 1;
next();
});
}
})
I didn't wan to use any plugin (an extra dependencie, initializing the mongodb connection apart from the one I use in the server.js, etc...) so I did an extra module, I can use it at any schema and even, I'm considering when you remove a document from the DB.
module.exports = async function(model, data, next) {
// Only applies to new documents, so updating with model.save() method won't update id
// We search for the biggest id into the documents (will search in the model, not whole db
// We limit the search to one result, in descendant order.
if(data.isNew) {
let total = await model.find().sort({id: -1}).limit(1);
data.id = total.length === 0 ? 1 : Number(total[0].id) + 1;
next();
};
};
And how to use it:
const autoincremental = require('../modules/auto-incremental');
Work.pre('save', function(next) {
autoincremental(model, this, next);
// Arguments:
// model: The model const here below
// this: The schema, the body of the document you wan to save
// next: next fn to continue
});
const model = mongoose.model('Work', Work);
module.exports = model;
Hope it helps you.
(If this Is wrong, please, tell me. I've been having no issues with this, but, not an expert)
Here is a proposal.
Create a separate collection to holds the max value for a model collection
const autoIncrementSchema = new Schema({
name: String,
seq: { type: Number, default: 0 }
});
const AutoIncrement = mongoose.model('AutoIncrement', autoIncrementSchema);
Now for each needed schema, add a pre-save hook.
For example, let the collection name is Test
schema.pre('save', function preSave(next) {
const doc = this;
if (doc.isNew) {
const nextSeq = AutoIncrement.findOneAndUpdate(
{ name: 'Test' },
{ $inc: { seq: 1 } },
{ new: true, upsert: true }
);
nextSeq
.then(nextValue => doc[autoIncrementableField] = nextValue)
.then(next);
}
else next();
}
As findOneAndUpdate is an atomic operation, no two updates will return same seq value. Thus each of your insertion will get an incremental seq regardless of number of concurrent insertions. Also this can be extended to more complex auto incremental logic and the auto increment sequence is not limited to Number type
This is not a tested code. Test before you use until I make a plugin for mongoose.
Update I found that this plugin implemented related approach.
The answers seem to increment the sequence even if the document already has an _id field (sort, whatever). This would be the case if you 'save' to update an existing document. No?
If I'm right, you'd want to call next() if this._id !== 0
The mongoose docs aren't super clear about this. If it is doing an update type query internally, then pre('save' may not be called.
CLARIFICATION
It appears the 'save' pre method is indeed called on updates.
I don't think you want to increment your sequence needlessly. It costs you a query and wastes the sequence number.
I had an issue using Mongoose Document when assigning value to Schema's field through put(). The count returns an Object itself and I have to access it's property.
I played at #Tigran's answer and here's my output:
// My goal is to auto increment the internalId field
export interface EntityDocument extends mongoose.Document {
internalId: number
}
entitySchema.pre<EntityDocument>('save', async function() {
if(!this.isNew) return;
const count = await counter.findByIdAndUpdate(
{_id: 'entityId'},
{$inc: {seq: 1}},
{new: true, upsert: true}
);
// Since count is returning an array
// I used get() to access its child
this.internalId = Number(count.get('seq'))
});
Version: mongoose#5.11.10
None of above answer works when you have unique fields in your schema
because unique check at db level and increment happen before db level validation, so you may skip lots of numbers in auto increments like above solutions
only in post save can find if data already saved on db or return error
schmea.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('email must be unique'));
} else {
next(error);
}
});
https://stackoverflow.com/a/41479297/10038067
that is why none of above answers are not like atomic operations auto increment in sql like dbs
I use together #cluny85 and #edtech.
But I don't complete finish this issues.
counterModel.findByIdAndUpdate({_id: 'aid'}, {$inc: { seq: 1} }, function(error,counter){
But in function "pre('save...) then response of update counter finish after save document.
So I don't update counter to document.
Please check again all answer.Thank you.
Sorry. I can't add comment. Because I am newbie.
var CounterSchema = Schema({
_id: { type: String, required: true },
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
testvalue: { type: String }
});
entitySchema.pre('save', function(next) {
if (this.isNew) {
var doc = this;
counter.findByIdAndUpdate({ _id: 'entityId' }, { $inc: { seq: 1 } }, { new: true, upsert: true })
.then(function(count) {
doc.testvalue = count.seq;
next();
})
.catch(function(error) {
throw error;
});
} else {
next();
}
});

delete object from document array in mongodb collection using mongoose

I try to remove an element from an array attribute of my object.
This is my schema :
const userSchema = new mongoose.Schema({
userID: {
type: Number
},
name: {
type: String
},
names: [
{
text: { type: String, required: true },
order: {
type: Number,
required: true
}
}
]
});
this is my mongoose function :
User.findOne({ userID: Number(req.params.id) })
.then((user) => {
user.names.remove({text: "john", order: 3});
recipe.save(() => {
res.json(recipe);
});
})
I don't understand why it's not good :/
As per documentation of mongoose remove method remove operation is only executed when a callback is passed. To force execution without a callback, you must first call remove() and then execute it by using the exec() method.
Since you are trying to delete from array of objects then better would be to use pull operator. You don't have to do find and remove, you can simply use update method.
As per documentation of $pull operator you can either specify a value or a condition
i.e.
{ $pull: { <field1>: <value|condition>, <field2>: <value|condition>, ... } }
In your scenario you need to either specify complete value of one or more names item object or an condition that matches one or more names item
Add the condition where you match id of names item or if you don't know that then you can use elemMatch to match on few fields i.e.
Use following pull condition to solve the issue:
User.update(
{ _id: Number(req.params.id) },
{ $pull: { 'names': { $elemMatch: { 'text': "john", 'order': 3 }} } },
(error, success) => {
if (error) console.log(error);
console.log(success);
}
);
To Remove Element from array in document please follow as below
User.update(
{
userID: Number(req.params.id),
},
{
$pull: { names: { $elemMatch: { text: "john", order: 3 } } }
},
{
multi: false
}
).lean().then((Status) => {
console.log("Status-->", Status);
res.json('Removed Successfully');
})
Refer $pull operator at link

Mongoose: updating array in document not working

I'm trying to update an array in document by adding object if it doesn't exist, and replacing the object in array otherwise. But nothing ($push, $addToSet) except the $set parameter does anything, and $set works as expected - overwrites the whole array.
My mongoose schema:
var cartSchema = mongoose.Schema({
mail: String,
items: Array
});
The post request handler:
app.post('/addToCart', function(req, res) {
var request = req.body;
Cart.findOneAndUpdate({
"mail": request.mail
}, {
$addToSet: {
"items": request.item
}
}, {
upsert: true
},
function(err, result) {
console.log(result);
}
);
res.send(true);
});
The data that I'm sending from the client:
{
"mail":"test#gmail.com",
"item":{
"_id":"59da78db7e9e0433280578ec",
"manufacturer":"Schecter",
"referenceNo":"Daemon-412",
"type":"Gitare",
"image":"images/ba9727909d6c3c26412341907e7e12041507489988265.jpeg",
"__v":0,
"subcategories":[
"Elektricne"
]
}
}
EDIT:
I also get this log when I trigger 'addToCart' request:
{ MongoError: The field 'items' must be an array but is of type object in
document {_id: ObjectId('5a19ae2884d236048c8c91e2')}
The comparison in $addToSet would succeeded only if the existing document has the exact same fields and values, and the fields are in the same order. Otherwise the operator will fail.
So in your case, request.item always need to be exactly the same.
I would recommend creating a model of "item". Then, your cart schema would be like:
var cartSchema = mongoose.Schema({
mail: String,
items: [{
type: ObjectId,
ref: 'item',
}],
});
And let MongoDB determine if the item exist.
this should work you just need to implement objectExits function that test if the item is that one you're looking for :
Cart.findOne({ "mail": request.mail })
.exec()
.then(cart => {
var replaced = cart.items.some((item, i) => {
if (item._id == request.item._id)) {
cart.items[i] = request.item;
return true;
}
})
if (!replaced) {
cart.items.push(request.item);
}
cart.save();
return cart;
})
.catch(err => {
console.log(err)
});

How do i tell Mongoose NOT to save a field?

How do i tell Mongoose not to save the age field if it's null or undefined?
Or could i do this in Express somehow?
Express
router.put('/edit/:id', function(req, res) {
Person.findOneAndUpdate({ _id: req.params.id }, {
name: req.body.updateData.name,
age: req.body.updateData.age
}, { new: true });
})
Mongoose Schema
var PersonSchema = new Schema({
name: { type: String},
age: {type: String}
})
An explination (if u ask why i need this)
I'm using the same html template for new person and edit person. When i create a new person, Mongoose will save just the name field if i leave the age field empty. But when i use the edit template, Mongoose will always set the age field as null, even if the field is empty. I can't think of anything to stop this.
You could manage your request data before the db update, like:
router.put('/edit/:id', function(req, res) {
let update = {name: req.body.updateData.name};
if (req.body.updateData.age != "") {
update.age = req.body.updateData.age;
}
Person.findOneAndUpdate({ _id: req.params.id }, update, { new: true });
})
#Aioros, you answer gave me an idea, however i found this solution to match my issue. I'm just deleteing the null or undefined object elements before they are send to Mongoose.
let update = {
name: req.body.updateData.name,
age: req.body.updateData.age
};
if (update.age === null || update.age === undefined) delete update.age
Person.findOneAndUpdate({ _id: req.params.id }, update, { new: true });

Categories