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);
});
Related
What Im trying to do is have one user send an amount to another user, Id like for the amount to be subtracted from the sender's balance, and added to the receiver's balance. The problem is that the receivers balance is updated and works, but the previous query to subtract the amount from the sender isn't working.
I understand there are no joins in mongoose (at least not in the classical sense), so Id need to query the user's balance first and then update it in another query. But surely there is a way to nest these queries? Im hoping I just have the syntax wrong.
user.js
const { Decimal128 } = require("mongodb");
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
useraddress: {
type: String,
required: true,
},
userbalance: {
type: Decimal128,
required: true,
}
});
module.exports = User = mongoose.model("users", UserSchema);
server.js
app.post("/sendamount", function (req, res) {
var amount = 100;
var senderAddress = "Bob123";
var receiverAddress = req.body.receiver;
// Take amount from sender balance
User.findOne({useraddress:senderAddress}, (err, sub) => {
if (err) return handleError(err);
var mybalance = parseFloat(sub.userbalance)
console.log(mybalance)
User.findOneAndUpdate(
{ useraddress: senderAddress },
{ userbalance: mybalance - amount }),
// send to receiver balance
User.findOne({useraddress:receiverAddress}, (err, sub2) => {
if (err) return handleError(err);
var receiverbalance = parseFloat(sub2.userbalance)
console.log(receiverbalance)
// add amount to receiver's balance
User.findOneAndUpdate(
{ useraddress: receiverAddress },
{ userbalance: receiverbalance + amount },
function (err, data) {
if (err) res.send(err);
res.send(data);
console.log("found " + data)
}
)
})
})
})
Would someone mind checking my code? Thanks
try to this subtrack process
I added example field, you have this areas change
const asyncSubtrackProcess = async (id, amount) => await User.findOneAndUpdate({
_id: mongoose.Types.ObjectId(id),
userbalance: { $gt: 0 }
},
{
$inc: {
userbalance: - parseInt(amount)
}
});
const result = () => amounts.map(async item => await asyncSubtrackProcess(item.id, item.amount));
result();
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 })
});
});
Given yon schema, how do I save userId to createdBy and updatedBy?
This seems like it should be an easy use case. How do I do it?
I'm not sure how to get userId from req.user.id to the model before being written.
// graph.model.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var schema = new Schema({
title: String,
createdAt: Date,
createdBy: String,
updatedAt: Date,
updatedBy: String,
});
// This could be anything
schema.pre('save', function (next) {
- if (!this.createdAt) {
this.createdAt = this.updatedAt = new Date;
this.createdBy = this.updatedBy = userId;
} else if (this.isModified()) {
this.updatedAt = new Date;
this.updatedBy = userId;
}
next();
});
Here's the controller code if you're interested:
var Graph = require('./graph.model');
// Creates a new Graph in the DB.
exports.create = function(req, res) {
Graph.create(req.body, function(err, thing) {
if(err) { return handleError(res, err); }
return res.status(201).json(thing);
});
};
// Updates an existing thing in the DB.
exports.update = function(req, res) {
if(req.body._id) { delete req.body._id; }
Graph.findById(req.params.id, function (err, thing) {
if (err) { return handleError(res, err); }
if(!thing) { return res.send(404); }
var updated = _.merge(thing, req.body);
updated.save(function (err) {
if (err) { return handleError(res, err); }
return res.json(thing);
});
});
};
The following is just another way of saving userId.
Sample model with createdBy, updatedBy, createdAt, updatedAt fields:
import mongoose from 'mongoose';
const SupplierSchema = new mongoose.Schema(
{
name: {
type: String,
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
updatedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
},
{
timestamps: {
createdAt: true,
updatedAt: true,
},
},
);
export default mongoose.model('Supplier', SupplierSchema);
Note that in mongoose starting from version ^4.13.17 you can simply specify timestamps createdAt, updatedAt directly in the schema.
https://mongoosejs.com/docs/4.x/docs/guide.html#timestamps
Then in supplier controller assign req.user._id to the fields createdBy, updatedBy:
import mongoose from 'mongoose';
import { Supplier } from '../models';
exports.create = async (req, res) => {
const supplierToCreate = new Supplier({
_id: new mongoose.Types.ObjectId(),
name: req.body.name,
createdBy: req.user._id,
updatedBy: req.user._id,
});
return supplierToCreate
.save()
.then(() =>
res.status(201).json({
message: 'New supplier is created successfully.',
}),
)
.catch(errSaving => res.status(500).json({ error: errSaving }));
};
You can't access req object inside of mongoose hook.
I think, you should define virtual field with a smart setter instead:
schema.virtual('modifiedBy').set(function (userId) {
if (this.isNew()) {
this.createdAt = this.updatedAt = new Date;
this.createdBy = this.updatedBy = userId;
} else {
this.updatedAt = new Date;
this.updatedBy = userId;
}
});
Now all you have to do is to set modifiedBy field with correct userId value in your controller:
var updated = _.merge(thing, req.body, {
modifiedBy: req.user.id
});
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();
}
});
i created a schema for user like this:
var schema = new Schema({
username: {
type: String,
unique: true,
required: true
},
hashedPassword: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});
schema.virtual('password')
.set(function(password) {
this._plainPassword = password;
this.salt = Math.random() + '';
this.hashedPassword = this.encryptPassword(password);
})
.get(function() { return this._plainPassword; });
schema.methods.encryptPassword = function(password) {
return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
};
then I'm trying to change the password using the two approaches:
work nice
User.findById('userId..', function(err, user) {
user.password = '456';
user.save(cb);
})
why is this approach not working ?
User.findByIdAndUpdate('userId', {$set: {password: '456'}}, cb)
This happens because Mongoose doesn't apply any of the following on a findByIdAndUpdate() operation:
defaults
setters
validators
middleware
From the docs:
If you need those features, use the traditional approach of first
retrieving the document.
Model.findById(id, function (err, doc) {
if (err) ..
doc.name = 'jason borne';
doc.save(callback);
})
with version 4.0.9+ middleware supports findByIdAndUpdate().
CustomerSchema.pre('findOneAndUpdate', function(next, done) {
console.log("findOneAndUpdate pre middleware!!!");
next();
});