Mongoose: Infinite scroll with filtering - node.js

I have these two models:
User.js
const UserSchema = new Schema({
profile: {
type: Schema.Types.ObjectId,
ref: "profiles",
},
following: [
{
type: Schema.Types.ObjectId,
ref: "users",
},
],
});
module.exports = User = mongoose.model("users", UserSchema);
Profile.js
const ProfileSchema = new Schema({
videoURL: {
type: String,
},
});
module.exports = Profile = mongoose.model("profiles", ProfileSchema);
Here's an example of a User document:
{
"following": [
{
"profile":{
"videoURL":"video_url_1"
}
},
{
"profile":{
"videoURL":"video_url_2"
}
},
{
"profile":{}
},
{
"profile":{
"videoURL":"video_url_3"
}
},
{
"profile":{
"videoURL":"video_url_4"
}
},
{
"profile":{
"videoURL":"video_url_5"
}
},
{
"profile":{}
},
{
"profile":{
"videoURL":"video_url_6"
}
}
]
}
I am trying to implement an infinite scroll of the videos of the users followed by the connected user.
This means, I will have to filter user.following.profile.videoURL
WHERE videoURL exists
Suppose, I will be loading two videos, by two videos:
Response 1: ["video_url_1","video_url_2"]
Response 2: ["video_url_3","video_url_4"]
Response 3: ["video_url_5","video_url_6"]
Usually, infinite scroll is easy because all I have to load the documents 2 by 2 by order of storage without filtering on any field.
Example: Displaying the followed users two by two in an infinite scroll
User.findById(user_id).populate({
path: "following",
options: {
skip: 2 * page,
limit: 2,
},
});
But, now I have to perform filtering on each followed_user.profile.video, and return two by two. And I don't see how I can perform BOTH the filtering and the infinite scroll at the same time.
NOTE: According to the documentation:
In general, there is no way to make populate() filter stories based on properties of the story's author. For example, the below query won't return any results, even though author is populated.
const story = await Story.
findOne({ 'author.name': 'Ian Fleming' }).
populate('author').
exec();
story; // null
So I suppose, there is no way for me to use populate to filter based user.followers, based on each user.follower.profile.videoURL

I am not sure it is possible with populate method, but you can try aggregation pipeline,
$match user_id condition
$lookup with aggregation pipeline in users collection for following
$match following id condition
$lookup with profile for following.profile
$match videoURL should exists
$project to show profile field and get first element using $arrayElemAt
$slice to do pagination in following
let page = 0;
let limit = 2;
let skip = limit * page;
User.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(user_id) } },
{
$lookup: {
from: "users",
let: { following: "$following" },
pipeline: [
{ $match: { $expr: { $in: ["$_id", "$$following"] } } },
{
$lookup: {
from: "profiles",
localField: "profile",
foreignField: "_id",
as: "profile"
}
},
{ $match: { "profile.videoURL": { $exists: true } } },
{
$project: {
profile: { $arrayElemAt: ["$profile", 0] }
}
}
],
as: "following"
}
},
{
$addFields: {
following: {
$slice: ["$following", skip, limit]
}
}
}
])
Playground
Suggestion:
You can improve your schema design,
removing profile schema and add profile object in users collection, so you can achieve easily your requirement using populate method,
put match condition in following populate for videoURL exists
const UserSchema = new Schema({
profile: {
type: {
videoURL: {
type: String
}
}
},
following: [
{
type: Schema.Types.ObjectId,
ref: "users"
}
]
});
module.exports = User = mongoose.model("users", UserSchema);
User.findById(user_id).populate({
path: "following",
match: {
"profile.videoURL": { $ne: null }
},
options: {
skip: 2 * page,
limit: 2,
}
});

So what you want is table with infinite scroll and:
You can opt given ways to approach your problem :
Load data (first page) into grid.
Set filter on a col.
Load data again, this time using the filter.

Related

MongoDB find documents using an array of objects as search argument

Is there a way to search a mongodb database using an array of objects as search arguments?
Lets say I have the following search preferences:
preferences = [{
product: "PRODUCT_A",
minqty: 5,
maxqty: 50
},
{
product: "PRODUCT_B",
minqty: 100,
maxqty: 500
}
]
In my database I have Jobs with the following structure:
{
jobName: "job name",
items: [{
product: "PRODUCT_A"
qty: 25
},
{
product: "PRODUCT_F"
qty: 300
}
]
}
I would like to query the database using preferences and returning any jobs that match at least one of the criteria's.
ATTEMPT 1:
I managed to use all my preferences as filters, but $match is cumulative, the way it's written it works like && in javascript. My goal is to have "match THIS || match THAT".
let pipeline = [];
preferences.map((item) => {
let search = {
product: item.product,
quantity: { $gte: item.minqty, $lte: item.maxqty },
};
return pipeline.push({
$match: {
items: {
$elemMatch: search,
},
},
});
});
const jobs = await Job.aggregate(pipeline);
ATTEMPS 2 (SUCCESS):
let search = [];
preferences.map((item, index) => {
let arguments = {
product: item.product,
quantity: { $gte: item.minqty, $lte: item.maxqty },
};
search.push(arguments);
});
let pipeline = [
{
$match: {
items: {
$elemMatch: {
$or: search,
},
},
},
},
];
const jobs = await Job.aggregate(pipeline);
Use aggregation
Denormalize items using $Unwind
Once denormalized, you can use simple match with $or
Use $lte and $gte
And update the question with your attempts of these or post a new one.

MongoDB aggregation $match with $or

Is there a way to search a mongodb database using an array of objects as search arguments?
Lets say I have the following search preferences:
preferences = [{
product: "PRODUCT_A",
minqty: 5,
maxqty: 50
},
{
product: "PRODUCT_B",
minqty: 100,
maxqty: 500
}
]
In my database I have Jobs with the following structure:
{
jobName: "job name",
items: [{
product: "PRODUCT_A"
qty: 25
},
{
product: "PRODUCT_F"
qty: 300
}
]
}
I would like to query the database using preferences and returning any jobs that match at least one of the criteria's.
ATTEMPT 1:
I managed to use all my preferences as filters, but $match is cumulative, the way it's written it works like && in javascript. My goal is to have "match THIS || match THAT".
let pipeline = [];
preferences.map((item) => {
let search = {
product: item.product,
quantity: { $gte: item.minqty, $lte: item.maxqty },
};
return pipeline.push({
$match: {
items: {
$elemMatch: search,
},
},
});
});
const jobs = await Job.aggregate(pipeline);
ATTEMPS 2 (SUCCESS):
let search = [];
preferences.map((item, index) => {
let arguments = {
product: item.product,
quantity: { $gte: item.minqty, $lte: item.maxqty },
};
search.push(arguments);
});
let pipeline = [
{
$match: {
items: {
$elemMatch: {
$or: search,
},
},
},
},
];
const jobs = await Job.aggregate(pipeline);
I think you can create your search object by reducing the preferences array and use the $or operator. When you map the preferences array it is returning an array that will perform and operation. you need an object like -
{
$or: [{product1, quantity1}, {product2, quantity2}]
}
I guess you got my point.

Mongodb update multiple documents with different values

I have been trying to use updatemany with mongoose. I want to update the values in database using an array of objects.
[
{
"variantId": "5e1760fbdfaf28038242d676",
"quantity": 5
},
{
"variantId": "5e17e67b73a34d53160c7252",
"quantity": 13
}
]
I want to use variantId as filter.
Model schema is:
let variantSchema = new mongoose.Schema({
variantName: String,
stocks: {
type: Number,
min: 0
},
regularPrice: {
type: Number,
required: true
},
salePrice: {
type: Number,
required: true
}
})
I want to filter the models using variantId and then decrease the stocks.
As you need to update multiple documents with multiple criteria then .updateMany() wouldn't work - it will work only if you need to update multiple documents with same value, Try this below query which will help you to get it done in one DB call :
const Mongoose = require("mongoose");
let variantSchema = new mongoose.Schema({
variantName: String,
stocks: {
type: Number,
min: 0
},
regularPrice: {
type: Number,
required: true
},
salePrice: {
type: Number,
required: true
}
})
const Variant = mongoose.model('variant', variantSchema, 'variant');
let input = [
{
"variantId": "5e1760fbdfaf28038242d676",
"quantity": 5
},
{
"variantId": "5e17e67b73a34d53160c7252",
"quantity": 13
}
]
let bulkArr = [];
for (const i of input) {
bulkArr.push({
updateOne: {
"filter": { "_id": Mongoose.Types.ObjectId(i.variantId) },
"update": { $inc: { "stocks": - i.quantity } }
}
})
}
Variant.bulkWrite(bulkArr)
Ref : MongoDB-bulkWrite
I don't think this can be done with a single Model.updateMany query. You will need to loop the array and use Model.update instead.
for (const { variantId, quantity } of objects) {
Model.update({ _id: variantId }, { $inc: { stocks: -quantity } });
}
To run this in a transaction (https://mongoosejs.com/docs/transactions.html), the code should look something like this (however I have not tried or tested this):
mongoose.startSession().then(async session => {
session.startTransaction();
for (const { variantId, quantity } of objects) {
await Model.update({ _id: variantId }, { $inc: { stocks: -quantity } }, { session });
}
await session.commitTransaction();
});

Delete an array element from a document with Mongoose

I am having a schema called ReferralHistory.
It contains set of users and array of referred users.
ReferralHistory
var mongoose = require('mongoose');
var refferalHistorySchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
unique: true
},
referrals: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
});
var ReferralHistoryModel = mongoose.model('ReferralHistory', refferalHistorySchema);
module.exports = {
referralHistory: ReferralHistoryModel
}
I need to delete a particular user from referrals array in the collection ReferralHistory[Here i only know id of referred user].How can i achieve this?
Edit
Collection
I tried
db.referralhistories.update({ "u_referrals": "593281ef966d7f0eeb94db3d" }, { "$pull": { "u_referrals": "593281ef966d7f0eeb94db3d" } });
O/P
But document is not updating.
You use the $pull operator with .update(). So assuming referredId as the value you know
ReferralHistoryModel.update(
{ "referrals": referredId },
{ "$pull": { "referrals": referredId } },
{ "multi": true },
function(err,status) {
}
)
Noting the { "multi": true } means the update can be applied to more than one matched document in the collection. If you really only intend to match and update one document then you don't include that option since updating only the first match is the default.
If you want to be more specific and also have the "user" to match, then you can do:
ReferralHistoryModel.update(
{ "user": userId, "referrals": referredId },
{ "$pull": { "referrals": referredId } },
{ "multi": true },
function(err,status) {
}
)
And then the match needs both values to be present as opposed to any ReferralhistoryModel documents which matched the referredId you supplied.

Get result as an array instead of documents in mongodb for an attribute

I have a User collection with schema
{
name: String,
books: [
id: { type: Schema.Types.ObjectId, ref: 'Book' } ,
name: String
]
}
Is it possible to get an array of book ids instead of object?
something like:
["53eb797a63ff0e8229b4aca1", "53eb797a63ff0e8229b4aca2", "53eb797a63ff0e8229b4aca3"]
Or
{ids: ["53eb797a63ff0e8229b4aca1", "53eb797a63ff0e8229b4aca2", "53eb797a63ff0e8229b4aca3"]}
and not
{
_id: ObjectId("53eb79d863ff0e8229b97448"),
books:[
{"id" : ObjectId("53eb797a63ff0e8229b4aca1") },
{ "id" : ObjectId("53eb797a63ff0e8229b4acac") },
{ "id" : ObjectId("53eb797a63ff0e8229b4acad") }
]
}
Currently I am doing
User.findOne({}, {"books.id":1} ,function(err, result){
var bookIds = [];
result.books.forEach(function(book){
bookIds.push(book.id);
});
});
Is there any better way?
It could be easily done with Aggregation Pipeline, using $unwind and $group.
db.users.aggregate({
$unwind: '$books'
}, {
$group: {
_id: 'books',
ids: { $addToSet: '$books.id' }
}
})
the same operation using mongoose Model.aggregate() method:
User.aggregate().unwind('$books').group(
_id: 'books',
ids: { $addToSet: '$books.id' }
}).exec(function(err, res) {
// use res[0].ids
})
Note that books here is not a mongoose document, but a plain js object.
You can also add $match to select some part of users collection to run this aggregation query on.
For example, you may select only one particular user:
User.aggregate().match({
_id: uid
}).unwind('$books').group(
_id: 'books',
ids: { $addToSet: '$books.id' }
}).exec(function(err, res) {
// use res[0].ids
})
But if you're not interested in aggregating books from different users into single array, it's best to do it without using $group and $unwind:
User.aggregate().match({
_id: uid
}).project({
_id: 0,
ids: '$books.id'
}).exec(function(err, users) {
// use users[0].ids
})

Resources