update mongoose document before returning - node.js

I want to add a property to the returned docs of mongoose query. This property is as well a mongoose query
Buildung.find({_id: {$in: user.favorites}}, function (err, buildungs) {
if (err) { return res.status(400).json({error: err}); }
const mbuildungs = buildungs.map(buildung => {
let buildungObject = buildung.toObject();
const now = new Date();
Time.findOne({buildung: buildung._id, validFrom: { $lte: Date.parse(now) }}, null,
{sort: {validFrom: -1}}, (err, time)=> {
buildungObject.time = time;
});
return buildungObject;
});
return res.status(200).json({buildungs: mbuildungs})
});
The modified object should be returned, but it isnt adding the property time to the result.
I've also attempted to work with callback function but I could solve the problem, that I want to achieve.
Update
1) Schema
// Time
const TimeSchema = new mongoose.Schema({
validFrom: {type: Date, required: true},
....
building: {type: Schema.Types.ObjectId, ref: 'Building', index: true, required: true}
// Building
const BuildingSchema = new mongoose.Schema({
name: {type: String, required: true},
.....
// no relation to timeSchma
})

There are a few things that you could fix in your code!
Asynchronous functions cannot be called inside a map. You'd need some way to aggregate every findOne result - the easiest way out is using the async module. However, I'd personally recommend a Promise-based solution as that looks much cleaner.
Mongoose returns its own Object (called MongooseDocument) that is decorated with a lot of Mongoose-specific functions, and that makes the return object act differently. To work around this, use lean(). This returns a plain object that you can freely modify as you would any other JS object. Using lean() also has the additional advantage of huge performance improvements!
I've included both these changes to your code —
const async = require('async');
function findBuildungTime(buildung, done) {
const now = new Date();
Time.findOne({ buildung: buildung._id, validFrom: { $lte: Date.parse(now) } }, null, { sort: { validFrom: -1 } })
.lean()
.exec((err, time) => {
buildung.time = time;
return done(err, buildung);
});
}
Buildung.find({ _id: { $in: user.favorites } })
.lean()
.exec((err, buildungs) => {
if (err) {
return res.status(400).json({ error: err });
}
async.map(buildungs, findBuildungTime, (err, results) => {
return res.status(200).json({ buildungs: results })
});
});

Related

Update field within nested array using mongoose

I'm trying to update the subdocument within the array without success. The new data doesn't get saved.
Express:
router.put('/:id/:bookid', (req, res) => {
library.findOneAndUpdate(
{ "_id": req.params.id, "books._id": req.params.bookid},
{
"$set": {
"title.$": 'new title'
}
}
});
LibraryScema:
const LibarySchema = new Library({
Name: {
type: String,
required: false
},
books: [BookSchema]
});
bookScema:
const BookSchema = new Schema({
title: {
type: String,
required: false
},
Chapters: [
{
chapterTitle: {
type: String,
required: false
}
}
]
});
I only aim to update the sub-document, not parent- and sub-document at same time.
I had a similar issue. I believe there is something wrong with the $set when it comes to nested arrays (There was an entire issue thread on GitHub). This is how I solved my issue.
var p = req.params;
var b = req.body;
Account.findById(req.user._id, function (err, acc) {
if (err) {
console.log(err);
} else {
acc.websites.set(req.params._id, req.body.url); //This solved it for me
acc.save((err, webs) => {
if (err) {
console.log(err);
} else {
console.log('all good');
res.redirect('/websites');
}
});
}
});
I have a user with a nested array.
Try this code
router.put('/:id/:bookid', (req, res) => {
library.findById(
req.params.id, (err, obj) => {
if (err) console.log(err); // Debugging
obj.books.set(req.params.bookid, {
"title": 'new title',
'Chapters': 'your chapters array'
});
obj.save((err,obj)=>{
if(err) console.log(err); // Debugging
else {
console.log(obj); // See if the saved object is what expected;
res.redirect('...') // Do smth here
}
})
})
});
Let me know if it works, and I'll add explanation.
Explanation: You start by finding the right object (library in this case), then you find the correct object in the array called books.
Using .set you set the whole object to the new state. You'll need to take the data that's not changing from a previous instance of the library object.
I believe this way will overwrite and remove any data that's not passed into the .set() method. And then you save() the changed.

Mongoose document.populate() is not working

I am using a pretty simple Node/Mongo/Express setup and am trying to populate referenced documents. Consider my schemas for "Courses" which contain "Weeks":
// define the schema for our user model
var courseSchema = mongoose.Schema({
teachers : { type: [String], required: true },
description : { type: String },
previous_course : { type: Schema.Types.ObjectId, ref: 'Course'},
next_course : { type: Schema.Types.ObjectId, ref: 'Course'},
weeks : { type: [Schema.Types.ObjectId], ref: 'Week'},
title : { type: String }
});
// create the model for Course and expose it to our app
module.exports = mongoose.model('Course', courseSchema);
I specifically want to populate my array of weeks (though when I changed the schema to be a single week, populate() still didn't work).
Here is my schema for a Week (which a Course has multiple of):
var weekSchema = mongoose.Schema({
ordinal_number : { type: Number, required: true },
description : { type: String },
course : { type: Schema.Types.ObjectId, ref: 'Course', required: true},
title : { type: String }
});
// create the model for Week and expose it to our app
module.exports = mongoose.model('Week', weekSchema);
Here is my controller where I am trying to populate the array of weeks inside of a course. I have followed this documentation:
// Get a single course
exports.show = function(req, res) {
// look up the course for the given id
Course.findById(req.params.id, function (err, course) {
// error checks
if (err) { return res.status(500).json({ error: err }); }
if (!course) { return res.sendStatus(404); }
// my code works until here, I get a valid course which in my DB has weeks (I can confirm in my DB and I can console.log the referenced _id(s))
// populate the document, return it
course.populate('weeks', function(err, course){
// NOTE when this object is returned, the array of weeks is empty
return res.status(200).json(course);
});
};
};
I find it strange that if I remove the .populate() portion from the code, I get the correct array of _ids back. But when I add the .populate() the returned array is suddenly empty. I am very confused!
I have also tried Model population (from: http://mongoosejs.com/docs/api.html#model_Model.populate) but I get the same results.
Thanks for any advice to get my population to work!
below should return course with populated weeks array
exports.show = function(req, res) {
// look up the course for the given id
Course.findById(req.params.id)
.populate({
path:"weeks",
model:"Week"
})
.exec(function (err, course) {
console.log(course);
});
};
### update: you can populate from instance also ###
Course.findById(req.params.id, function (err, course) {
// error checks
if (err) { return res.status(500).json({ error: err }); }
if (!course) { return res.sendStatus(404); }
// populate the document, return it
Course.populate(course, { path:"weeks", model:"Weeks" }, function(err, course){
console.log(course);
});
});
### Update2: Perhaps even more cleanly, this worked: ###
Course.findById(req.params.id, function (err, course) {
// error checks
if (err) { return res.status(500).json({ error: err }); }
if (!course) { return res.sendStatus(404); }
// populate the document, return it
console.log(course);
}).populate(course, { path:"weeks", model:"Weeks" });
here it seems like you are using course.populate() instead of Course.populate()
Use this code instead of yours,I change only one single word course.populate() to Course.populate()
In your case "course" is instance but you need to use Course(Model)
Course.findById(req.params.id, function (err, course) {
if (err) { return res.status(500).json({ error: err }); }
if (!course) { return res.sendStatus(404); }
// Guys in some case below three-line does not work in that case you must comment these lines and uncomments the last three-line
Course.populate('weeks', function(err, course){
return res.status(200).json(course);
});
// Course.populate({ path:"weeks", model:"Weeks" }, function(err, course){
// return res.status(200).json(course);
// });
};

MongoDB + ExpressJS - Observe insertions

I have a rapidly growing collection in my mongodb. I want to take certain actions when new documents are inserted into those collection. How can I observe and then trigger actions, when such a new model is inserted?
I did discover old solutions such as mongo-observer, but those seem to be pretty old and did not work for me.
Can anybody recommend a relatively new and maintained solution?
schema.pre() hooks would do it. Example:
export const schema = new mongoose.Schema({
name: String,
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
}, { timestamps: { createdAt: "created_at", updatedAt: "updated_at" }
});
schema.pre("save", function (next) {
bcrypt.hash(this.password, 10, (err, hash) => {
this.password = hash;
next();
});
});
schema.pre("update", function (next) {
bcrypt.hash(this.password, 10, (err, hash) => {
this.password = hash;
next();
});
});
You can refer to npm module - mongohooks.
Update:
Adding sample code:
const db = require('mongojs')('mydb', ['members']); // load mongojs as normal
const mongohooks = require('mongohooks');
// Add a `createdAt` timestamp to all new documents
mongohooks(db.members).save(function (document, next) {
document.createdAt = new Date();
next();
});
// Now just use the reqular mongojs API
db.members.save({ name: "Thomas" }, function (error, result) {
console.log("Created %s at %s", result.name, result.createdAt);
});

mongoose findOneAndSave and pre hook, and setting an initial value (addedOn date)

I am new to mongoose and honestly have a ton of reading to do still, but I am working on a small pet project that reads and writes to a mongo db. In this instance I would like to use mongoose to assure a consistent document model.
The problem is assuring default valuesin both an insert and update operations independently. In particualr I am referring to addedOn and updatedOn field respectively. The method I am calling for persistence is the findOneAndUpdate which now has support for middleware hooks (as of 4.0).
I was able to get the basic hook to work (applying the updatedOn date every time), but have been unable to figure out how to add suport (the mongoose way) for the insert case where the addedOn is defaulted. This is the simple schema and hook (attmpeting to handle insert and update):
const TestSchema = new Schema({
id : ObjectId,
name : String,
addedOn : { type: Date, default: Date.now },
updatedOn : { type: Date, default: Date.now }
});
TestSchema.pre('findOneAndUpdate', function(next) {
console.log('hook::findOneAndUpdate')
// update defaults
this.update({addedOn: {$exists:true}},{
$set: {
updatedOn: new Date()
}
})
// insert defaults
this.update({addedOn: {$exists:false}},{
$set: {
addedOn: new Date(),
updatedOn: new Date()
}
})
next();
});
No surprise (because it is really swag) it is not working. THis inserts a row every time the app is run. It appears if adding a criteria to the update method overwrites the default match (haven't dug into the source).
This is my initial snippet for the solely update case and it finds the row and updates correctly, but as mentioned cannot handle the addedOn (it would update it everytime)
TestSchema.pre('findOneAndUpdate', function(next) {
console.log('hook::findOneAndUpdate')
this.update({},{
$set: {
updatedOn: new Date()
}
})
next();
});
Driving all of this is this snippet (to keep the code as close to the idiom of the folktale implmentation of the application I am working on)
mongoose.connect('mongodb://localhost/network');
const model = mongoose.model('TestSchema', TestSchema)
const schema = TestSchema
const collection = model
const findOneAndUpdate = (collection) => {
return (criteria, record) => {
return new Task((reject, resolve) => {
const callback = (error, data) => {
if (error) {
reject(error)
}
else {
resolve(data)
}
}
collection.findOneAndUpdate(criteria, record, {upsert: true}, callback)
})
}
}
const upsert = findOneAndUpdate(collection)
upsert({name: "me"}, {name: "me"}).fork(console.log, console.error)
I could modify my findOneAndUpdate function to add the addedOn - but I would really like to use the mongoose hooks properly.
Well here is one answer, although it requires a second "save" after the findOne and update:
const mongoose = require('mongoose')
const Schema = mongoose.Schema
, ObjectId = Schema.ObjectId
, Task = require('data.task') ;
const TestSchema = new Schema({
id : ObjectId,
name : String,
genre: {type: String, default: 'Action'},
addedOn : { type: Date, default: Date.now, setDefaultsOnInsert: true },
updatedOn : { type: Date, default: Date.now }
});
TestSchema.pre('findOneAndUpdate', function(next) {
console.log('hook::findOneAndUpdate')
this.update({},{
$set: {
updatedOn: new Date()
}
})
next();
});
mongoose.connect('mongodb://localhost/network');
const model = mongoose.model('TestSchema', TestSchema)
const findOneAndUpdate = (originalModel) => {
return (criteria, record) => {
return new Task((reject, resolve) => {
const callback = (error, updatedModel) => {
if (error) {
reject(error)
}
else {
if(!updatedModel) {
resolve(null)
}else {
// this looks to be required to apply defaults from the Schema
updatedModel.save((error) => {
if (error) {
reject(error)
}
resolve(updatedModel)
})
}
}
}
originalModel.findOneAndUpdate(criteria, record, {upsert: true}, callback)
})
}
}
const upsert = findOneAndUpdate(model)
upsert({name: "me"}, {name: "me"}).fork(console.log, console.error)
I don't love it... BUT it works.

Mongoose .pre('save') does not trigger

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

Resources