Populating multiple fields with a pre "find" middleware - mongodb

I am trying to specify the fields that should always be populated for the user document in a pre "find" middleware, like this:
userSchema.pre(/^find/, function (next) {
this.populate("followers following"); next();
});
Here is the user schema:
const userSchema = new mongoose.Schema<IUser>(
{
firstName: {
type: String,
required: [true, "You must provide your first name."],
},
lastName: {
type: String,
required: [true, "You must provide your last name."],
},
profilePic: {
type: String,
},
email: {
type: String,
required: [true, "You must provide an email."],
unique: true,
},
password: {
type: String,
required: [true, "You must provide a password."],
},
isVerified: {
type: Boolean,
default: false,
},
verificationToken: {
type: String,
},
role: {
type: String,
enum: ["user", "admin"],
default: "user",
},
followers: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
following: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
},
{
timestamps: true,
}
);
But when I send the request it's just stuck not sending any response.
It works just fine when I only populate one field, either 'followers' or 'following', but together it won't work.
I tried a bunch of different ways, but nothing seems to work.
If anyone can help I would be very thankful!

#1 Have you tried to seperate them ?
userSchema.pre('find', function (next) {
this.populate("followers").populate("following");
next();
});
#2 Or with an array of paths
userSchema.pre('find', function (next) {
this.populate(['followers', 'following']);
next();
});
As documentation states here and here:
The Document#populate() method does not support chaining. You need to
call populate() multiple times, or with an array of paths, to populate
multiple paths.
#3 Or to use deep-populate if you're populating across multiple levels as documented here:

I've got the working solution.
The issue is that if the populate() method in the middleware has more than one field, it calls the middleware for each field and it triggers infinite recursion, which makes the request hanging.
The workaround is a bit weird, but pretty straight forward. We have to add the _recursed option so the middleware knows to avoid populating recursively.
userSchema.pre(/^find/, function (next) {
if (this.options._recursed) {
return next();
}
this.populate({ path: "followers following", options: { _recursed: true } });
next();
});

Related

How to navigate through documents in a circuler linked list fashion in MongoDB?

I have a really simple User model.
const userSchema = mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
address: {
type: String,
default: null,
},
description: {
type: String,
default: null,
},
active: {
type: Boolean,
default: true,
},
interests: [
{
type: String,
default: null,
},
],
favorites: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null,
},
],
});
I have a use-case where the client should be able to navigate through the users one by one in a circular fashion. I was able to implement the functionality for getting the next and previous user relative to a specific user through the
this answer.
However, the issue with this is that if I try to get the previous document on the first document in the Users collection, it of course returns an empty object. And same is the case when I try to get the next document on the last document.
I want to tweak it so that if I try to get the previous document on the first document, it would return me the last document, and if I try to get the next document on the last document, it would return me the first document. Just like how you traverse through a circular linked list. Any help would be appreciated! Thanks.

Mongoose trying to populate using the pre find hook does not work when I have nested schemas

Hello I have the following schema definition in in a Node + Express app,
changeDataSchema is nested inside the roleSchema
There are some commong options that the changeDataSchema and the roleSchema both should have and these options are declared in a javascript object called commonFields and the spread operator is used to spread these options to both the schemas.
// Following are the common options for both the schemas
const commonFields = {
roleName: {
type: String,
trim: true,
validate: { validator: requiredValidator, msg: "Role Name is required" },
},
permissions: {
type: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Permission",
},
],
required: [true, "Permissions are required"],
},
};
// CHANGE DATA SCHEMA
const changeDataSchema = new Schema({
...commonFields, // <----- Spread operator is used to spread the common options
action: {
type: String,
trim: true,
required: [true, "Action is required"],
enum: {
values: [CREATE, EDIT, DELETE],
message: "Action should either be Create, Edit or Delete",
},
},
});
// ROLE SCHEMA
const roleSchema = new Schema(
{
...commonFields, // <----- Spread operator is used to spread the common options
changeData: changeDataSchema,
lockedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
default: null,
},
},
{ timestamps: true }
);
As you can see both the schemas do a child referencing to the Permission documents. (Declared in the common fields)
So I want both the schemas to be populated with permissions when doing a pre find.
To achieve this I have written the following code,
// Following is to populate the roles with permissions
roleSchema.pre(/^find/, function (next) {
this.populate({
path: "permissions", <--------- This works
select: "-__v",
});
next();
});
// Following is to populate the changeData documents with permissions
changeDataSchema.pre(/^find/, function (next) {
this.populate({
path: "permissions", <-------- This does NOT work
select: "-__v",
});
next();
});
From the above two pre hooks, only the one I have written for the roleSchema works when querying a role. Even though I have written a pre hook for populating the changeData it does not work when finding a role.
How can I get the changeData also populated with permissions when finding a role?
Thank you.

Mongoose populate depending on conditions

My service uses MongoDB and Mongoose. I have two DBs: Users and Posts. In Posts schema I have parameters:
"author", that contains userId from Users DB
"anonymous", a boolean-parameter that shows if the post is anonymous or not.
I can't solve the problem: when I request data from Posts DB I want to populate author in the "author" parameter only for non-anonymous posts, for anonymous ones I'd like to return null or not to return this parameter at all.
I've tried to use "match", but it doesn't work.
How can I solve this problem?
Thank you.
Code example:
const postSchema = mongoose.Schema(
{
author: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
},
anonymous: {
type: Boolean,
required: true,
default: false,
},
content: {
type: String,
required: true,
},
date: {
type: Date,
required: true,
default: Date.now,
},
},
{
timestamps: true,
}
);
For population I use pre:
postSchema.pre(/^find/, function (next) {
this.populate({
path: 'author',
select: '_id login',
});
next();
});

MissingSchemaError while using Mongoose Populate with only one model

**I have answered below. In short you need to require the Model in the module in which you wish to populate, even though you do not refer to it directly.
I am hitting a strange problem with mongoose when populating just one particular array of IDs.
I have three models, User, Company and Widgets.
When I return the company populated with the users all is fine using:
Company.findOne({ name: 'xyz' })
.populate('users')
.exec(function(err, company) {
if (err) return res.send(err)
res.send(company)
})
However when I try to replace populate 'users' with 'widgets' I get the following error:
{
"message": "Schema hasn't been registered for model \"widget\".\nUse mongoose.model(name, schema)",
"name": "MissingSchemaError"
}
Here are the models:
USER:
const UserSchema = new Schema({
name: String,
email: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
company: {
type: Schema.Types.ObjectId,
ref: 'company'
}
});
const User = mongoose.model("user", UserSchema);
COMPANY:
const CompanySchema = new Schema({
name: String,
URL: {
type: String,
unique: true
},
users: [{
type: Schema.Types.ObjectId,
ref: 'user'
}],
widgets: [{
type: Schema.Types.ObjectId,
ref: 'widget'
}]
});
const Company = mongoose.model('company', CompanySchema);
WIDGET:
const WidgetSchema = new Schema({
title: {
type: String,
required: true
},
maker: String
});
const Widget = mongoose.model('widget', WidgetSchema);
I have manually inspected the _ids in the widget array of the company model and they are all correct.
OK, so this was a lack of understanding on my behalf.
In the module where I was using:
Company.findOne({ name: 'xyz' })
.populate('users')
.exec(function(err, company) {
if (err) return res.send(err)
res.send(company)
})
I had imported the User model for other uses in the module. However, as I was not directly referring to Widget I had not imported it. Having done some more research I found that you need to import a model when populating even though not referring to it directly.
Let me know if best to delete whole thread or leave for reference.

Populate query with match returns null

I have three schemas, that need them to be separated and I can't use subdocuments. The important one is this
export var TestSchema = new mongoose.Schema({
hash: { type: String, index: { unique: true }, default: common.randomHash },
date: { type: Date, default: Date.now },
result: { type: Object },
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
data: { type: Object },
finished: Date,
lang: { type: String, default: 'pt' },
benchmark: { type: String, required: true },
order: { type: mongoose.Schema.Types.ObjectId, ref: 'Transaction' },
/* TODO: remove */
name: { type: String }
});
I have a query that does the populate (it's actually a pagination helper, but I'm cutting to the chase):
TestModel.find({hide: {$ne: true}, user: id}).populate({
path: 'user',
match: {$or: [
{email: new RegExp(search, i)},
{name: new RegExp(search, i)},
{empresa: new RegExp(search, i)},
]}
}).exec().then(/*...*/)
when populate.match doesn't find anything, it sets the user to null. I tried setting the find({'user':{$ne: null}}) but it ignores it. (I guess the populate happen after the find call, maybe that's the reason).
Is there any way I can filter it in the database layer instead having to rely on iterating of the results, check for null then filter out?
This answer on GitHub clarifies that it is not possible with populate, due to how MongoDB works. However, you should be able to do it with $lookup.