Mongoose .pre('save') does not trigger - node.js

I have the following model for mongoose.model('quotes'):
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var quotesSchema = new Schema({
created: { type: String, default: moment().format() },
type: { type: Number, default: 0 },
number: { type: Number, required: true },
title: { type: String, required: true, trim: true},
background: { type: String, required: true },
points: { type: Number, default: 1 },
status: { type: Number, default: 0 },
owner: { type: String, default: "anon" }
});
var settingsSchema = new Schema({
nextQuoteNumber: { type: Number, default: 1 }
});
// Save Setting Model earlier to use it below
mongoose.model('settings', settingsSchema);
var Setting = mongoose.model('settings');
quotesSchema.pre('save', true, function(next) {
Setting.findByIdAndUpdate(currentSettingsId, { $inc: { nextQuoteNumber: 1 } }, function (err, settings) {
if (err) { console.log(err) };
this.number = settings.nextQuoteNumber - 1; // substract 1 because I need the 'current' sequence number, not the next
next();
});
});
mongoose.model('quotes', quotesSchema);
There is an additional Schema for mongoose.model('settings') to store an incrementing number for the incrementing unique index Quote.number im trying to establish. Before each save, quotesSchema.pre('save') is called to read, increase and pass the nextQuoteNumber as this.number to the respectively next() function.
However, this entire .pre('save') function does not seem to trigger when saving a Quote elsewhere. Mongoose aborts the save since number is required but not defined and no console.log() i write into the function ever outputs anything.

Use pre('validate') instead of pre('save') to set the value for the required field. Mongoose validates documents before saving, therefore your save middleware won't be called if there are validation errors. Switching the middleware from save to validate will make your function set the number field before it is validated.
quotesSchema.pre('validate', true, function(next) {
Setting.findByIdAndUpdate(currentSettingsId, { $inc: { nextQuoteNumber: 1 } }, function (err, settings) {
if (err) { console.log(err) };
this.number = settings.nextQuoteNumber - 1; // substract 1 because I need the 'current' sequence number, not the next
next();
});
});

For people who are redirected here by Google, make sure you are calling mongoose.model() AFTER methods and hooks declaration.

In some cases we can use
UserSchema.pre<User>(/^(updateOne|save|findOneAndUpdate)/, function (next) {
But i'm using "this", inside the function to get data, and not works with findOneAndUpdate trigger
I needed to use
async update (id: string, doc: Partial<UserProps>): Promise<User | null> {
const result = await this.userModel.findById(id)
Object.assign(result, doc)
await result?.save()
return result
}
Instead of
async update (id: string, doc: Partial<UserProps>): Promise<User | null> {
const result = await this.userModel.findByIdAndUpdate(id, doc, { new: true, useFindAndModify: false })
return result
}

The short solution is use findOne and save
const user = await User.findOne({ email: email });
user.password = "my new passord";
await user.save();

I ran into a situation where pre('validate') was not helping, hence I used pre('save'). I read that some of the operations are executed directly on the database and hence mongoose middleware will not be called. I changed my route endpoint which will trigger .pre('save'). I took Lodash to parse through the body and update only the field that is passed to the server.
router.post("/", async function(req, res, next){
try{
const body = req.body;
const doc = await MyModel.findById(body._id);
_.forEach(body, function(value, key) {
doc[key] = value;
});
doc.save().then( doc => {
res.status(200);
res.send(doc);
res.end();
});
}catch (err) {
res.status(500);
res.send({error: err.message});
res.end();
}
});

Related

Mongoose auto-increment fails because of a cast error

I am trying to increment a simple number field, but it is telling me it is failing to to a casting error.
CastError: Cast to Number failed for value "{ '$inc': 1 }" (type Object) at path "times_dealt"
Says it's an object?
This is my schema for Answer
const answerSchema = new mongoose.Schema({
body: {
type: String,
trim: true,
required: true,
},
times_dealt: {
type: Number,
required: true,
},
times_picked: {
type: Number,
required: true,
},
times_won: {
type: Number,
required: true,
},
}, {
timestamps: true,
});
module.exports = { answerSchema };
This is my route for me the admin to add new answers (it's a game so only I can add them, that why the auth. Figured I'll include the complete code.)
router.post("/answers", async(req, res) => {
try {
const isMatch = await bcrypt.compare(
req.body.password,
process.env.ADMIN_PASSWORD
);
if (isMatch) {
const answer = new Answer({
body: req.body.answer.trim(),
times_dealt: 0,
times_picked: 0,
times_won: 0,
});
await answer.save();
res.status(201).send(answer);
}
res.status(401).send();
} catch (e) {
console.log("failed to save", e);
res.status(400).send(e);
}
});
Then whenever a card is dealt, I want to increase the count for times_dealt, and this is when I get the error. This is how I do it:
async function getOneAnswerCard(room) {
if (room.unused_answer_cards.length !== 0) {
// We pick a random answer's ID from our array of unused answers
const randomAnswerID = getRandomElement(room.unused_answer_cards);
// We get that answer's full object from our DB
const newAnswer = await Answer.findById(randomAnswerID);
// const newAnswer = await Answer.findByIdAndUpdate(randomAnswerID, {
// times_dealt: { $inc: 1 },
// });
await Answer.findByIdAndUpdate(randomAnswerID, {
times_dealt: { $inc: 1 },
});
// We remove it from the unused cards array
room.unused_answer_cards = room.unused_answer_cards.filter(
(answerID) => answerID !== randomAnswerID
);
// We add it to the dealt cards array
room.dealt_answer_cards.push(randomAnswerID);
// We serialize the answer (we don't want the user to get info on our answer stats)
const serializedAnswer = { _id: newAnswer._id, body: newAnswer.body };
return serializedAnswer;
}
}
Just getting the answer by itself is no issue. Getting a random ID and fetching an answer object works just fine. It's only when I've added the increment functionality that it started crashing.
I think you're using $inc with a wrong syntax. Try this:
await Answer.findByIdAndUpdate(randomAnswerID, {
{ $inc: { times_dealt: 1 } },
});

Mongoose pre-update hook working but changes aren't reflected in database

I feel like I'm missing something very simple but I cannot get the contact.fullName value to update. contact.middleName updates as expected.
The hook is triggered, and the changes made are reflected in the conlogs, but not in the DB. The fullName field refuses to update but no errors are thrown. Kinda feel like I'm losing my mind here
The Mongoose schema/model:
const Contact = new mongoose.Schema({
firstName: { type: String, required: true },
middleName: { type: String, required: false },
lastName: { type: String, required: true },
fullName: { type: String, required: false },
});
// When a Contact is created, we populate the `fullName` field using a pre-save
// hook, which is working as expected:
Contact.pre('save', function (next) {
const contact = this;
contact.fullName = `${contact.firstName || ''} ${contact.lastName || ''}`;
return next();
});
// on update, we want to update the fullName field as well. This is broken:
Contact.pre(\(updateOne|update)\, function (next) {
const contact = this;
const update = contact._update['$set'];
const newFullName = `${update['firstName'] || ''} ${update['lastName'] || ''}`;
console.log(newFullName);
contact.fullName = newFullName;
console.log(contact.fullName); // i can see the correctly updated value here!
if (update['middleName']) contact.middleName = update['middleName'];
console.log(contact.middleName); // i can also see this one!
return next();
});
If relevant, the function that calls the updateOne action:
module.exports.updateContact = (req, res, next) => {
const { contact } = req.body;
Contact.updateOne({ _id: contactId }, { $set: { ...contact } }, (err, result) => {
if (err) return next(err);
return next(result);
});
};
The problem is because you are calling Query#updateOne(), not Document#updateOne() so it will trigger pre('updateOne') query middleware and this will refer to the query, not the document. In this case, you should modify your code to something like:
Contact.pre(\(updateOne|update)\, function (next) {
const update = contact._update['$set'];
const newFullName = `${update['firstName'] || ''} ${update['lastName'] || ''}`;
this.set({ fullName: newFullName })
return next();
});
More detail here.

Node express find and return response multple models

I'm fairly new to node & express, I'm trying to implement a register application.
I have 2 models, both models have one common field 'empID'.
const RegisterEntriesSchema = mongoose.Schema({
empID: Number,
registerType: String,
registerItemsQuantity: Number,
registerItemsDesc: String
}, {
timestamps: true
});
const RegisterEmpSchema = mongoose.Schema({
empID: Number,
empName: String,
empPhone: String,
empProj:String
}, {
timestamps: true
});
For my get call in which I need to merge the values, I get from RegisterEmpSchema with its corresponding
employee details from RegisterEmpSchema.
exports.findAllRegisterEntries = (req, res) => {
registerEntriesModel.find()
.then(result => {
var updatedResponse=[];
console.log(result[0].empID);
for(var i=0;i<result.length;i++){
registerEmpModel.find({ empID: result[i].empID })
.then(result2 => {
**//unable to access result here**
}).catch(err => {
console.log("exception catch called findAllRegisterEntries, find employee details "+err);
});
}
res.send(updatedResponse);
}).catch(err => {
res.status(500).send({
message: err.message || "Some error occurred while retrieving register."
});
});
};
I basically need to get register data and its corresponding employee data.
How do I modify my find() code to use the key empID and do a join query fetch?
I think you better use populate, add ref to empID inside RegisterEntriesSchema
const RegisterEmpSchema = new mongoose.Schema({
empID: Number,
empName: String,
empPhone: String,
empProj: String
}, {
timestamps: true
});
const registerEmpModel = mongoose.model('RegisterEmpSchema', RegisterEmpSchema, 'registerEmployeeCollection');
const RegisterEntriesSchema = new mongoose.Schema({
registerType: String,
registerItemsQuantity: Number,
registerItemsDesc: String,
empID: {
type: mongoose.Schema.Types.ObjectId,
ref: 'RegisterEmpSchema'
}
}, {
timestamps: true
});
RegisterEntriesSchema.index({ createdAt: 1 }, { expires: '525601m' });
const registerEntriesModel = mongoose.model('RegisterEntriesSchema', RegisterEntriesSchema, 'registerEntriesCollection');
module.exports = {
registerEmpModel, registerEntriesModel,
}
then use populate() to populate the RegisterEntriesSchema with correspondence empID
RegisterEntriesSchema.
find().
populate('empID').
exec(function (err, data) {
if (err) return console.log(err);
res.send(data);
});
check mongoose docs: https://mongoosejs.com/docs/populate.html

Call a related collection via populate

I try to call a related list of logs for a certain user via Mongoose populate. Who can help me with finishing the response?
These are the schemes:
const logSchema = new Schema({
logTitle: String,
createdOn:
{ type: Date, 'default': Date.now },
postedBy: {
type: mongoose.Schema.Types.ObjectId, ref: 'User'}
});
const userSchema = new Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
}
logs: { type: mongoose.Schema.Types.ObjectId, ref: 'logs' }
});
mongoose.model('User', userSchema);
mongoose.model('logs', logSchema);
Inspired by the Mongoose documentary (see above) and other questions in relation to this subject I think I got pretty far in making a nice get. request for this user. I miss the expierence to 'translate it' to Express.
const userReadLogs = function (req, res) {
if (req.params && req.params.userid) {
User1
.findById(req.params.userid)
.populate('logs')
.exec((err, user) => {
if (!user) { }); // shortened
return;
} else if (err) {
return; // shortened
}
response = { //question
log: {
user: user.logs
}
};
res
.status(200)
.json(response);
});
} else { }); //
}
};
The response in Postman etc would be something like this:
{
"log": {5a57b2e6f633ce1148350e29: logTitle1,
6a57b2e6f633ce1148350e32: newsPaper44,
51757b2e6f633ce1148350e29: logTitle3
}
First off, logs will not be a list of logs; it will be an object. If you want multiple logs for each user, you will need to store is as an array: logs: [{ type: mongoose.Schema.Types.ObjectId, ref: 'logs' }]
From the Mongoose docs: "Populated paths are no longer set to their original _id , their value is replaced with the mongoose document returned from the database by performing a separate query before returning the results." In other words, in your query user.logs will be the logs document for each user. It will contain all the properties, in your case logTitle, createdOn, and postedBy.
Sending user.logs as json from the server is as easy as: res.json(user.logs). So your query can look like this:
const userReadLogs = function (req, res) {
if (req.params && req.params.userid) {
User1
.findById(req.params.userid)
.populate('logs')
.exec((err, user) => {
if (!user) { }); // shortened
return;
} else if (err) {
return; // shortened
}
res.status(200).json(user.logs)
});
} else { }); //
}
};
I hope this makes it a little bit clearer!

Mongoose: Move User Authorized functionality from controller to model

I have a question about best practice and how to add user authorization functionality. Should it be in model, controller or elsewhere.
Currently,
I have been building validation functions within my Mongoose Models
I have been building authentication/authorization checks using middleware and called from my routes.
My current challenge is when an an authenticated and authorized user attempts to update a model for which they are NOT the owner.
My authenticated user has been attached to my request, but that data is not going to be available from within the Mongoose Model so I am thinking that I should probably create some sort of validation function on the model that can be called from my controller, so that my logic lives nicely with the model but can be called from the controller.
Controller
exports.create = function (req, res) {
try {
if (!_.isEmpty(req.body.entity.ordererAccountId) && !_.isEqual(req.user.accountId.toString(), req.body.entity.ordererAccountId)) {
var err = mong.formatError({ message: 'Invalid Account Access' });
return res.status(403).json(err);
}
OrderSchema.create(req.body.entity, function (err, entity) {
if (err) {
return mong.handleError(res, err);
}
return res.status(201).json(mong.formatSuccess(entity));
});
} catch (e) {
console.log(e);
}
};
Model
'use strict';
// ------------------------------------------------------------
// Order Model
// ------------------------------------------------------------
var mongoose = require('mongoose');
var findOneOrCreate = require('mongoose-find-one-or-create');
var Schema = mongoose.Schema;
var OrderSchema = new Schema({
created_at: { type: Date },
updated_at: { type: Date },
ordererAccountId:
{
type: Schema.ObjectId, ref: 'Account',
required: true
},
supplierAccountId:
{
type: Schema.ObjectId, ref: 'Account'
},
userId:
{
type: Schema.ObjectId, ref: 'User',
required: true
},
status: {
type: String,
enum: ['Open', 'Sent'],
default: 'Open'
},
notes: String,
supplierCompanyName: String,
supplierEmail: String,
supplierContactName: String,
supplierPhone1: String,
supplierPhone2: String,
deliveryCompanyName: String,
deliveryEmail: String,
deliveryFirstName: String,
deliveryLastName: String,
deliveryAddress1: String,
deliveryAddress2: String,
deliveryCity: String,
deliveryState: String,
deliveryPostCode: String,
deliveryCountry: String,
deliveryPhone1: String,
deliveryPhone2: String,
});
OrderSchema.plugin(findOneOrCreate);
// ------------------------------------------------------------
// Validations
// ------------------------------------------------------------
// Validate only one open order at a time per user
OrderSchema
.path('status')
.validate(function (status, respond) {
var Order = mongoose.model('Order');
// Excluding this Order, make sure there are NO other orders for this user with the status of 'Open'
var condition = {
userId: this.userId,
status: 'Open',
_id: { $ne: this._id }
};
Order.count(condition, function (err, count) {
if (err) {
console.log(err);
}
else {
respond(count === 0);
}
});
}, 'There can be only one open order at a time.');
// ------------------------------------------------------------
// Pre-Save Hook
// ------------------------------------------------------------
OrderSchema.pre('save', function (next) {
var now = new Date().getTime();
this.updated_at = now;
if (!this.created_at) {
this.created_at = now;
}
next();
});
module.exports = mongoose.model('Order', OrderSchema);
you can use your "create" function as a validation middleware in your router,by doing something like this:
app.post('/yourRoute', create, function(req, res) {
//if validation success
//do somthing
});
not forgetting to pass the "next" function as a third argument to your create function

Resources