Mongoose | populate on post middleware for "save" - node.js

Please consider the following parentSchema and childSchema:
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
How do I access the name of the parent within a post middleware of the childSchema?
I am trying the code below but parent is assigned the ObjectId instead of the actual parent model.
childSchema.post("save", async function (child) {
const parent = child.populate("parent").parent;
});
What's the right way to do this?

If you want to populate something after the initial fetch you need to call execPopulate -- e.g.:
childSchema.post("save", async function (child) {
try {
if (!child.populated('parent')) {
await child.populate('parent').execPopulate();
}
const parent = child.populate("parent").parent;
} catch (err) {}
});

I just found out about this.model("Model") so I'm sharing another solution. Not sure how it compares with the accepted one, though:
childSchema.post("save", async function (child) {
try {
const parent = await this.model("Parent").findOne({ _id: child.parent });
parent.children.push(child._id);
parent.save();
} catch (error) {}
});

Related

Update multiple documents instance in MongoDb collection with Mongoose. save() is not a function

Mongoose newbe here. I got the following function to update the references (deleting them) in the document Post when a Tag is deleted. When I call my GraphQl API this is what I got:
message": "posts.save is not a function"
The function in my gql resolver:
async deleteTag(root, { id }, context) {
const posts = await Post.find();
const tag = await Tag.findById(id);
if(!tag){
const error = new Error('Tag not found!');
error.code = 404;
throw error;
}
posts?.forEach(async (post) => {
await post.tags.pull(id);
})
await posts.save()
await Tag.findByIdAndRemove(id);
return true;
}
This is the Post model:
const PostSchema = new Schema({
body: {
type: String,
required: true
},
tags: {
type: [Schema.Types.ObjectId],
ref: 'Tag',
required: false
},
});
and this is the Tag model:
const TagSchema = new Schema(
{
name: {
type: String,
required: true
},
},
{ timestamps: true }
);
Looks like I can't call the method save() on the array of objects returned by Exercise.find()
I used the same pattern in other functions, the difference is that there I used .findById()
Any solution? Advice and best practice advide are super welcome.
You have to save the posts individually:
posts?.forEach(async (post) => {
await post.tags.pull(id);
await post.save();
})
Or use Model.updateMany() combined with the $pull operator.
FWIW, you should probably limit the number of matching Post documents by selecting only documents that have the specific tag listed:
await Post.find({ 'tags._id' : id });

Mongoose | Middleware | Rollback operations performed by pre/post hooks when an error is thrown

Considering the code below, how can a transaction be implemented in order to ensure that someParentDocument doesn't get deleted and any operations performed inside the hooks are rolledback, when an error is thrown in any of the hooks?
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
parentSchema.pre("remove", async function(next){
// Add and remove documents to Parent and Child...
//...
next();
});
parentSchema.post("remove", async function(parent){
throw new Error("Exception!");
});
// (...)
try {
await someParentDocument.delete(); // <-- will trigger the hooks above
} catch {}
TLDR; Mongoose middleware was not designed for this.
This method of inserting transactions is actually patching the middleware functionality, and you are essentially creating an api completely separate from the mongoose middleware.
What would be better is inverting the logic for your remove query in a separate function.
Simple & Intended Solution
Allow a transaction handling method to do its magic, and create a separate remove method for your parent model. Mongoose wraps mongodb.ClientSession.prototype.withTransaction with mongoose.Connection.prototype.transaction and we don't even have to instantiate or manage a session! Look at the different between the length of this and that below. And you save the mental headache of remembering the internals of that middleware at the cost of one separate function.
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
// Assume `parent` is a parent document here
async function fullRemoveParent(parent) {
// The document's connection
const db = parent.db;
// This handles everything with the transaction for us, including retries
// session, commits, aborts, etc.
await db.transaction(async function (session) {
// Make sure to associate all actions with the session
await parent.remove({ session });
await db
.model("Child")
.deleteMany({ _id: { $in: parent.children } })
.session(session);
});
// And done!
}
Small Extension
Another way to make this easy, is to register a middleware which simply inherits a session iff_ the query has one registered. Maybe throw an error if a transaction has not been started.
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
parentSchema.pre("remove", async function () {
// Look how easy!! Just make sure to pass a transactional
// session to the removal
await this.db
.model("Child")
.deleteMany({ _id: { $in: parent.children } })
.session(this.$session());
// // If you want to: throw an error/warning if you forgot to add a session
// // and transaction
// if(!this.$session() || !this.$session().inTransaction()) {
// throw new Error("HEY YOU FORGOT A TRANSACTION.");
// }
});
// Assume `parent` is a parent document here
async function fullRemoveParent(parent) {
db.transaction(async function(session) {
await parent.remove({ session });
});
}
Risky & Complex Solution
This works, and is totally, horribly complex. Not recommended. Will likely break some day because it relies on intricacies of the mongoose API. I don't know why I coded this, please don't include it in your projects.
import mongoose from "mongoose";
import mongodb from "mongodb";
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
// Choose a transaction timeout
const TRANSACTION_TIMEOUT = 120000; // milliseconds
// No need for next() callback if using an async function.
parentSchema.pre("remove", async function () {
// `this` refers to the document, not the query
let session = this.$session();
// Check if this op is already part of a session, and start one if not.
if (!session) {
// `this.db` refers to the documents's connection.
session = await this.db.startSession();
// Set the document's associated session.
this.$session(session);
// Note if you created the session, so post can clean it up.
this.$locals.localSession = true;
//
}
// Check if already in transaction.
if (!session.inTransaction()) {
await session.startTransaction();
// Note if you created transaction.
this.$locals.localTransaction = true;
// If you want a timeout
this.$locals.startTime = new Date();
}
// Let's assume that we need to remove all parent references in the
// children. (just add session-associated ops to extend this)
await this.db
.model("Child") // Child model of this connection
.updateMany(
{ _id: { $in: this.children } },
{ $unset: { parent: true } }
)
.session(session);
});
parentSchema.post("remove", async function (parent) {
if (this.$locals.localTransaction) {
// Here, there may be an error when we commit, so we need to check if it
// is a 'retryable' error, then retry if so.
try {
await this.$session().commitTransaction();
} catch (err) {
if (
err instanceof mongodb.MongoError &&
err.hasErrorLabel("TransientTransactionError") &&
new Date() - this.$locals.startTime < TRANSACTION_TIMEOUT
) {
await parent.remove({ session: this.$session() });
} else {
throw err;
}
}
}
if (this.$locals.localSession) {
await this.$session().endSession();
this.$session(null);
}
});
// Specific error handling middleware if its really time to abort (clean up
// the injections)
parentSchema.post("remove", async function (err, doc, next) {
if (this.$locals.localTransaction) {
await this.$session().abortTransaction();
}
if (this.$locals.localSession) {
await this.$session().endSession();
this.$session(null);
}
next(err);
});

Saving ALL nested documents on parent update Mongoose

My parent model looks like this:
var OrderSchema = new mongoose.Schema({
serviceNotes: {type: mongoose.Schema.Types.ObjectId, ref: 'Service'},
vehicle: {type: mongoose.Schema.Types.ObjectId, ref: 'Vehicle'}
});
The children look like this:
var VehicleSchema = new mongoose.Schema({
name: String
});
var ServiceSchema = new mongoose.Schema({
baseCost: Number
});
I am trying to find an easy solution to updating all of these documents at once. The problem is, when I call an update on an order, it does not update the nested documents. Please see the following:
exports.updateOrder = function (req, res) {
var order = req.body.order,
update = {
$set: order
};
Order.findOneAndUpdate({_id: order._id}, update, {new: true}, function (err, order) {
if (err) handleError(res);
if (!order) return res.status(404).send('Order not found.');
return res.json({order: order});
});
}
An example of req.body in this case may look like this:
{
order: {
_id: 829198218932shdbn,
serviceNotes: {
_id: 8932838nsd2sdnbd,
baseCost: 1
},
vehicle: {
_id: iu283823872378bd,
name: 'Honda'
}
}
}
The order update should also update the serviceNotes with the updated information, and the vehicle with the updated information.
The only way I have been able to update the nested documents is by calling a findOneAndUpdate on the children and updating them one by one. I am looking for a solution to just call update on the parent (order) and have the children update as well.

Finding Mongoose subdocument by time updated

I have two Mongoose Schemas:
var ItemSchema = new Schema({
trade: {
type: Schema.Types.ObjectId,
ref: 'Trade'
}
});
var Item = mongoose.model('Item', ItemSchema);
and
var TradeSchema = new Schema({
expiresOn: {
type: Date
}
});
var Trade = mongoose.model('Trade', TradeSchema);
I am trying to use Item.find() to find a item if its trade date is less than the date the user passes in via the query string in the request. I'm using the following code:
if (req.query.expiresBefore) {
Item.find({
'trade.expiresOn': {
$lte: req.query.expiresBefore
}
}, function (err, trades) {
console.log(trades)
})
}
However, I am receiving an empty array on the console.log() call. Using $gte also returns an empty array (though my research tells me that I need to use $lte for my purposes). What do I need to do to find the item document by matching the property (expiresOn) of its child (Trade)?
Referring to this one Stackoverflow question, what you want to do is not possible.I am not sure why you set the schemas like this, but if you want to keep them as they are. I would suggest you make a little change like the following
var ItemSchema = new Schema({
trade: {
type: Schema.Types.ObjectId,
ref: 'Trade'
}
});
var Item = mongoose.model('Item', ItemSchema);
and
var TradeSchema = new Schema({
itemId: { //add this
type: Schema.Types.ObjectId,
ref: 'Item'
},
expiresOn: {
type: Date
}
});
var Trade = mongoose.model('Trade', TradeSchema);
if (req.query.expiresBefore) {
Trade.
find({
'expiresOn': {
$lte: req.query.expiresBefore
}
}).
populate('itemId').
exec(function (err, trades) {
console.log(trades)
});
}

Storing a copy of a document embedded in another document in MongoDB via Mongoose

We have a requirement to store a copy of a Mongo document, as an embedded subdocument in another document. It should have a reference to the original document. The copied document needs to be a deep copy, like a snapshot of the original.
The original document's schema (defined with Mongoose) is not fixed -
it currently uses a type of inheritance to allow different additions to the Schema depending on "type".
Is there a way to such a flexible embedded schema within a Mongoose model?
Is it something that needs to be injected at runtime, when we can know
the schema?
The models / schemas we have currently look like this:
///UserList Schema: - this should contain a deep copy of a List
user: {
type: ObjectId,
ref: 'User'
},
list: {
/* Not sure if this is a how we should store the reference
type: ObjectId,
ref: 'List'
*/
listId: ObjectId,
name: {
type: String,
required: true
},
items: [{
type: ObjectId,
ref: 'Item'
}]
}
///List Schema:
name: {
type: String,
required: true
},
items: [{
type: ObjectId,
ref: 'Item'
}],
createdBy: {
type: ObjectId,
ref: 'User'
}
The code we currently have uses inheritance to allow different item types. I realise this technique may not be the best way to achieve the flexibility we require and is not the focus of my question.
///Item Model + Schema
var mongoose = require('mongoose'),
nodeutils = require('util'),
Schema = mongoose.Schema,
ObjectId = Schema.Types.ObjectId;
function ItemSchema() {
var self = this;
Schema.apply(this, arguments);
self.add({
question: {
type: String,
required: true
}
});
self.methods.toDiscriminator = function(type) {
var Item = mongoose.model('Item');
this.__proto__ = new Item.discriminators[type](this);
return this;
};
}
nodeutils.inherits(ItemSchema, Schema);
module.exports = ItemSchema;
I think you just need to create an empty {} object for the document in your parent mongoose schema. This way you´ll be able to store any object with a hardcopy of all it´s data.
parentobj : {
name: Sring,
nestedObj: {}
}
I think at this point, what you´ll need is to mark your nested objet as modified before you save it. Here is an example of my mongoose code.
exports.update = function(req, res) {
User.findById(req.params.id, function (err, eluser) {
if (err) { return handleError(res, err); }
if(!eluser) { return res.send(404); }
var updated = _.merge(eluser, req.body);
//This makes NESTEDDATA OBJECT to be saved
updated.markModified('nestedData');
updated.save(function (err) {
if (err) { return handleError(res, err); }
return res.json(200, eluser);
});
});
};
In addition, if you need an array of different documents in nestedDocument, the right way is this one:
parentobj : {
name: Sring,
nestedObjs: [Schema.Types.Mixed]
}
Please check Mongoose Schema Types carefully
EDIT
As you said, I´ll add you final solution as including ItemSchema in the nestedObj array definition to clarifythe type of the object to a determined one..
var ItemSchema = new Schema({
item1: String,
item2: String
});
var parentobj = new Schema({
name: Sring,
nestedObj: [ItemSchema]
});
EDIT 2:
Remember adding new Items to the nestedArray, must be done with nestedArray.push(item)
regards!!

Resources