mongoose findOneAndSave and pre hook, and setting an initial value (addedOn date) - node.js

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.

Related

How to get MongoDB _id of an inserted document

I want to create a document in my MongoDB database and take the _id of the new document.
This is what I'm doing:
const mongoose = require("mongoose");
const billingSchema = require("./models/billing");
const { ObjectId } = require("bson");
const { MongoClient } = require("mongodb");
const mongouri = "***";
var connection = mongoose.createConnection(mongouri);
var Bills = connection.model("Fatturazione", billingSchema, "Fatturazione");
exports.createBill = (b) => {
return new Promise((resolve, reject) => {
Bills.Create(b, function (err) {
if (err) {
reject(err);
} else {
console.log(mongoose.Types.ObjectId(b._id));
resolve();
}
});
});
};
and this is my Schema:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
//schema define the structure of the document
const billingSchema = new Schema({
data_fatturazione: {
type: Date,
required: true,
},
data_saldo: {
type: Date,
required: false,
},
totale: {
type: Number,
required: false,
},
pagato: {
type: Boolean,
required: false,
},
});
module.exports = billingSchema;
In the console.log() I want to print the _id of the last inserted document but it prints a non-existing id (it doesn't correspond to the _id of the last created document in the database). I also tried without using mongoose.Types.ObjectId() but it prints undefined. I don't understand where is the problem.
I call the function createBill() in another js file, passing an object with the correct fields.
You are trying to get the _id of argument b, which is passed to your createBill, which is logically undefined. Instead you must get the _id from a result of Bill.create, mongoose callbacks take 2 arguments as #Joe mentioned in the comments, so your code must look like this:
exports.createBill = (b) => {
return new Promise((resolve, reject) => {
Bills.Create(b, function (err, result) {
if (err) {
reject(err);
} else {
console.log(result._id);
resolve(result);
}
});
});
};

Cascade Delete in mongo

I am new to MongoDB. I created 4 collections & they are connected with each other. (I am using node.js to write it)
Here, it's my question. How can I delete all records at once? Is there something like deep level population?
This one holds all models.
const DataModel = mongoose.Schema({
_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', require: true},
order: { type: mongoose.Schema.Types.ObjectId, ref: 'Order', require: true},
});
User model
const userSchema = mongoose.Schema({//other stuff});
Order model
const orderSchema = mongoose.Schema({
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
//other stuff
});
Product model
const productSchema = mongoose.Schema({//other stuff});
I can delete the entry with these code from the database, but the other entries still there
exports.delete_data = (req, res, next) => {
const id = req.params.userId;
userDataModel.deleteOne({_id: id})
.exec()
.then(docs => {
res.status(200).json({
message: 'Record Deleted',
request: {
type: 'POST'
}
});
})
.catch(err => {
console.log(err);
res.status(500).json({
error: err
});
});
};
Update: However, I wonder, Could I call other defined delete functions for order, product inside delete_data
As #Geert-Jan suggest, cascade delete is my solution. The link that geert-jan gave solve my problem. However, I wonder, Could I call other defined delete functions for order, product inside delete_data
i did this and it could be good for someone who wants to delete documents in cascade linked to any field of a model.
async blackHole() {
try {
const rtn = new ApiResponse<any>();
const userId = id;
const accountId = mongoose.Types.ObjectId(id);
var CollectionNames: any[] = [];
mongoose.connection.db.listCollections().toArray(function (err, collections) {
CollectionNames = collections.map(c => c.name);
CollectionNames.forEach((element: any) => {
mongoose.connection.db.collection(element).deleteMany({ "account": accountId });
});
});
const accountController = new AccountController(this.wsParams);
await accountController.delete(id)
await super.delete(userId);
return rtn;
} catch (error: any) {
const rtn = new ApiResponse<any>();
rtn.message = error;
rtn.success = false;
rtn.status = 422;
return rtn;
}
}
I hope you can use it :D

update mongoose document before returning

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

Mongoose Pre Save hook on Parent Object not executing

I am new to mongoose as a method for updating Mongo. I am using nmap to map my network and provide some visibility on servers and ports that are open (as part of a largeer strategy). This portion of the strategy alos pulls information from CHEF and vSphere which is all linked together in a GUI. Those portions are just simple single level objects and is working fine, however the NMAP portion has a parent/child object model. The object model has a Server object and a Port object - both of which have addedOn and updatedOn dates.
Unfortunately mongoose hook for the pre save is only firing for the children objects (each of them) and not for the parent object - although the parent object is getting saved. I really want the parent object to also have the addedOn and updatedOn dates. I can't seem to figure it out. Thanks in advance
A disclaimer, the code below is snipped out of a larger application written in folktale, ramda and point free form.
NodeJs 5.1, Mongoose 4.4.1
The port.js File
const mongoose = require('mongoose');
const Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
const PortSchema = new Schema({
id : ObjectId,
port : String,
protocol : String,
application : String,
addedOn : { type: Date, default: Date.now, setDefaultsOnInsert: true },
updatedOn : { type: Date, default: Date.now }
});
PortSchema.pre('save', function(next){
this.update({},{
$set: {
updatedOn: new Date()
}
})
next();
});
module.exports.model = mongoose.model('Port', PortSchema)
module.exports.schema = PortSchema
The server.js file
const mongoose = require('mongoose');
const PortSchema = require('./port').schema
const Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
const ServerSchema = new Schema({
id : ObjectId,
address : String,
addressType : String,
ports : [PortSchema],
addedOn : { type: Date, default: Date.now, setDefaultsOnInsert: true },
updatedOn : { type: Date, default: Date.now }
});
ServerSchema.pre('save', function(next){
this.update({},{
$set: {
updatedOn: new Date()
}
})
next();
});
module.exports.model = mongoose.model('Server', ServerSchema)
module.exports.schema = ServerSchema
Upsert code in my application
// nmapUpsert :: (criteria, {}) => Task {ok :: int, ...}
const nmapUpsert = adapt.findOneAndUpdate(Server)
// persist :: [{address :: string, ...}] => [Task {ok :: int, ...}]
const persistNmap = R.map((data) => {
return nmapUpsert({ "address": data.address }, data)
})
Here is my model -> task upsert adapter (adapt.findOneAndUpdate)
module.exports.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)
})
}
}
Admittedly that last function is a bit cludgy - but I am just trying to figure this out before I clean it up.
If I change the hooks to findOneAndUpdate I get better behavior. The server objectgets the addedOn on insert (and updatedOn as well), then subsequently just the updatedOn gets updated. Unforutnately the entire Ports object collection gets replaced - even on update - and the addedOn and updatedOn both get updated at that point.
The port.js File
const mongoose = require('mongoose');
const Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
const PortSchema = new Schema({
id : ObjectId,
port : String,
protocol : String,
application : String,
addedOn : { type: Date, default: Date.now, setDefaultsOnInsert: true },
updatedOn : { type: Date, default: Date.now }
});
PortSchema.pre('findOneAndUpdate', function(next){
this.update({},{
$set: {
updatedOn: new Date()
}
})
next();
});
module.exports.model = mongoose.model('Port', PortSchema)
module.exports.schema = PortSchema
The server.js file
const mongoose = require('mongoose');
const PortSchema = require('./port').schema
const Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
const ServerSchema = new Schema({
id : ObjectId,
address : String,
addressType : String,
ports : [PortSchema],
addedOn : { type: Date, default: Date.now, setDefaultsOnInsert: true },
updatedOn : { type: Date, default: Date.now }
});
ServerSchema.pre('findOneAndUpdate', function(next){
this.update({},{
$set: {
updatedOn: new Date()
}
})
next();
});
module.exports.model = mongoose.model('Server', ServerSchema)
module.exports.schema = ServerSchema

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