Populate() of Populate() in Mongoose for querying to match $in array - node.js

I am developing an eCommerce web App. I am experiencing a problem described as follows.
I am suppose to run a cron job. When a brand adds a new product to the system, every shop that sells that brand will automatically show the newly added product by the brand. The Cron Job will run every morning at 12 to go through all the shops individually and will add the newly added product.
Now the shop will have an array of products. From that array of products, I need to fetch all the unique brands to compare it with the brand of newly added product. If the brand of newly added product exists in the shop.products[], then that product._id will be pushed in that array.
Here is my code.
CRON JOB CODE :
module.exports.newProductAddedCronJob = function(req,res){
// console.log("New Products Added Cron Job");
ProductReplacement.find({
status: "Pending",
"type": "new"
})
.populate("primaryProduct")
.exec(function(err,newProducts){
if(err) return res.status(500).send({err});
if(newProducts.length < 1) return res.status(404).send({err : "No New Products Added."});
async.forEachSeries(newProducts,function(item){
console.log("New Product : ", item.primaryProduct.brand._id);
Shop.find({
"products.brand._id" : {
$in: [item.primaryProduct.brand._id]
}
})
.populate("products")
.exec(function(err,shopsWithBrand){
console.log("SHOPS WITH Brand "+item.primaryProduct.brand._id + " Total = "+shopsWithBrand.length);
console.log("SHOPS WITH BRAND : ",shopsWithBrand);
for(var j = 0 ; j < shopsWithBrand.length; j++){
console.log("Shop = "+shopsWithBrand[i].name);
}
});
},function(err,data){
console.log("Final Callback!")
if(err) return res.status(500).send({err})
res.send({newProducts});
})
})
}
ProductReplacement is used for multiple purposes. In that particular schema, the type = "new" means the products that are newly added by a brand and status = "Pending" means, it is not yet processed.
var Product = new mongoose.Schema({
brand: {
_id: mongoose.Schema.Types.ObjectId,
name: {
type: String,
required: true
},
nameSlug: {
type: String,
required: true
},
images: [Image]
}
});
Brand Schema :
var Brand = new mongoose.Schema({
name: {
type: String,
minlength: 2,
maxlength: 30,
required: true
},
images: [{
src: String,
alt: String,
type: {
type: String,
enum: imageTypes,
validate: validators.isIn({message: 'The image type should be one of the following: (' + imageTypes + ')'}, imageTypes)
}
}]
});
Shop Schema :
var Shop = new mongoose.Schema({
products: [{
_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product'
},
stock: {
type: Number,
required: true
},
price: {
type: Number
}]
});
ProductReplacement Schema :
var ProductReplacement = new mongoose.Schema({
primaryProduct : {
type: mongoose.Schema.ObjectId,
ref: 'Product'
},
duplicateProduct: {
type: mongoose.Schema.ObjectId,
ref: 'Product'
},
"type": {
type: String,
required: true,
default: "merge"
},
created_at : {
type: Date,
required: true,
default: Date.now
},
status: {
type: String,
required: true,
default: "Pending"
}
});
So all I need is Shop.products.brand.id to compare it with the brand ID of the newly added product. If they match, I will push it into the array of products in that particular shop.
But I am not able to get shops using the query.
The last option of iterating through all the products of each shop one by one is certainly open. But I was wondering if there could be some way to do the same using query.

Related

How to populate sub document of another model in mongoose?

I have two mongodb model as following.
const CompanySchema = new Schema(
{
sections: [{
name: { type: String },
budgets: [{ // indicates from CalcSchema
index: { type: Number },
title: { type: String },
values: [Number],
sum: { type: Number, default: 0 },
}],
}]
},
{ timestamps: true }
);
const CalcSchema = new Schema({
budget: {
type: Schema.Types.ObjectId, // I want to populate this field. this indicates budget document in Company model
ref: "Company.sections.budgets" //it's possible in mongoose?
},
expense: {
type: Number,
default: 0
}
});
budget field indicate one of budgets field in CompanySchema.
So I want to populate when get Calc data.
But I don't how to populate embedded document.
I tried set ref value to ref: "Company.sections.budgets". but it's not working.
Please anyone help.
Finally, I found answer myself.
There is useful plugin for it.
https://github.com/QuantumGlitch/mongoose-sub-references-populate#readme
And I learned that my schema structure was wrong. It's anti-pattern in mongodb.

Best way to check category owner

Task model
const TaskSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'User'
},
title: {
type: Schema.Types.String,
required: true
},
description: Schema.Types.String,
createdDate: {
type: Schema.Types.Date,
default: Date.now()
},
position: {
type: Schema.Types.Number,
default: 0
},
categoryId: [{
type: Schema.Types.ObjectId,
ref: 'Category'
}]
});
Category model
const CategorySchema = new Schema({
title: {
type: Schema.Types.String,
required: true
},
description: {
type: Schema.Types.String,
},
categoryThumbnail: {
type: Schema.Types.String,
default: ''
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
createdDate: {
type: Schema.Types.Date,
default: Date.now()
}
});
When creating a task, the user can assign a category. Do I need to check the category owner before adding the task to Mongodb. If so, what is the best way to do this? Options:
1. Make a request to the database for all categories and check the user id.
2. Store the category id in the user document and, upon receipt of the request, check this list.
So if the User can create multiple categories and each category is only accessible by the User who created it you have a one-to-many association. In this situation it seems your Option 1 is what you want. Keep the user id on the category and then query all categories that have the user id you're looking for.
Edit:
If possible, I would recommend that you limit the categories the user sees when creating a task to only be categories created by that user. If that is not possible, then you could do one query to grab all the categories from the list of category IDs sent to the server and loop through the results checking if the user IDs are the same.
Category.find({
'id': { $in: [
mongoose.Types.ObjectId('4ed3ede8844f0f351100000c'),
mongoose.Types.ObjectId('4ed3f117a844e0471100000d'),
mongoose.Types.ObjectId('4ed3f18132f50c491100000e')
]}
}, function(err, docs){
docs.forEach(item => {
return item.userId === userId; //compare to userId that sent the request
}
});

MongoDB database design for products and bundles

I am trying to build an e-commerce website based on Node.js with a mongoDB database and I am encountering problems about some database design or some logic I am missing
To sum up, I have Product that contain price, name, description etc... and Bundle that contains an array of products (by reference). The main problem come when I have to order, I can't get Product AND Bundle together ...
So I have already a Product schema :
const productSchema = new mongoose.Schema({
file: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
preparation: String,
allergics: {
type: Array,
required: true,
},
price: {
type: Number,
required: true,
},
// More fields
});
module.exports = mongoose.model('Product', productSchema);
And a Bundle schema that contains ref to Product (A bundle contains multiple products) :
const bundleSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
itemsId: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true,
}],
description: String,
reduction: {
type: Number,
min: 0,
default: 0,
max: 100,
},
});
module.exports = mongoose.model('Bundle', bundleSchema);
So when a user orders a bundle OR a single product, I use this schema :
const orderSchema = new mongoose.Schema({
orderedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
articlesId: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
},
],
itemsNumber: {
type: Array,
required: true,
},
amount: Number,
orderedAt: Date,
placeToShip: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Place',
},
});
module.exports = mongoose.model('Order', orderSchema);
As you can see, I only reference to Product , but I want to reference to Product AND Bundle , I don't know if this is possible, or if this is the wrong way to design the database like that.
Sorry if the post is a bit long, but I am trying to be as clear as possible! Thanks a lot.
if you want to reference product or bundle(depending on user buys bundle or single product) in articleId, you can do it like this:
Dont give ref in the articleId field of your orderSchema, just specify its type as ObjectId.
const orderSchema = new mongoose.Schema({
...
articlesId: [
{
type: mongoose.Schema.Types.ObjectId
},
],
...
});
And, while populating tell it which model to populate from.
//In case user bought a product
Order.find({findQuery})
.populate({path : '',model : 'Product'})
.exec(function(err,result){...});
//In case user bought a Bundle
Order.find({findQuery})
.populate({path : '',model : 'Bundle'})
.exec(function(err,result){...});
But, you must have a way to find out user bought a single product or a bundle.
Hope that helps you!

How to mongoose list() - filtering values by subdocument field value

So I'm implementing a paginated table that list some records that have subdocuments and i need to add filtering capabilities so that I can filter the list() results using the subdocument field values.
let HardwareSchema = new mongoose.Schema({
name: { type: String, required: true }, // the exact name of the hardware.
slug: { type: String, unique: true }, // the hardware's slug.
category: { type: String, ref: 'HardwareCategory', required: true },
}
let HardwareCategorySchema = new mongoose.Schema({
name: { type: String, unique: true, required: true }, // the name of the category.
slug: { type: String, unique: true }, // the slug of the category.
description: { type: String, unique: true, required: true }, // the name of the category.
createdBy: { type: String, ref: 'User', required: true } // the original submitter of the document.
});
So basically i want to to filter our Hardwares using category selected category.
Here is my the actual code;
const options = {
perPage: 10,
page: (req.query.page > 0 ? req.query.page : 1) - 1,
sortBy: {'rank': -1 },
criteria: {
status: 'approved',
category: {
$elemMatch: { 'slug': category }
}
}
};
Hardware.list(options)
.then(results => {});
I'm getting the error; Can't use $elemMatch with String.
So basically i need to;
be able to filter results based on subdocuments field value.
be able to paginate results
be able to sort results.
How can I manage this?
Try this option.
const options = {
perPage: 10,
page: (req.query.page > 0 ? req.query.page : 1) - 1,
sortBy: {'rank': -1 },
criteria: {
status: 'approved',
'category.slug' : category
}
};
or you can use find() like
Hardware.find({
'category.slug' : category
}, function(err, data){
})

Mongoose .find() is empty when querying on subdocument

I'm trying to query a document based off of its subdocument data. When I do this I get no data returned. When I remove the "where" or when I query on a primitive type field within the parent it works fine. What am I missing?
My collections are broken up into separate files, here they are together for simplicity:
var PlayerSchema = new Schema({
firstName: { type: String, default: '', required: true},
lastName: { type: String, default: '', required: true},
nickname: { type: String, default: '' },
});
module.exports = mongoose.model('Player', PlayerSchema);
var GameSchema = new Schema({
winner: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
datePlayed: { type: Date, default: Date.now, required: true },
});
module.exports = mongoose.model('Game', GameSchema);
var GamePlayerSchema = new Schema({
game: { type: Schema.Types.ObjectId, ref: 'Game', required: true},
player: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
points: { type: Number, default: 0 },
place: { type: Number, default: 0 },
});
module.exports = mongoose.model('GamePlayer', GamePlayerSchema);
My query:
GamePlayerModel.find()
//.where('player.firstName').equals('Brian') // returns empty
//.where(place).equals(1) // returns correct dataset
.where('game.datePlayed').gte(startDateRange).lt(endDateRange) // returns empty
.select('game player points place')
.populate('game')
.populate('player')
.exec(function (err, gamePlayers) {
if(err) return next(err);
res.json(gamePlayers);
});
So again, if I query on a subdocument in any way it returns an empty dataset. I've tried game.datePlayed and even games.datePlayed. I'm not sure what to do. I don't need the player.firstName results, however I figured that'd be an easy thing to test to make sure the query is setup correctly.
Lastly, this is how I setup the date ranges. The date objects come out correctly, but are they possibly the wrong type?
var now = new Date();
var month = req.query.month ? parseInt(req.query.month) : now.getUTCMonth();
var year = req.query.year ? parseInt(req.query.year) : now.getUTCFullYear();
var endMonth = month+1;
if(endMonth > 11) endMonth = 0;
var startDateRange = new Date(year, month, 1);
var endDateRange = new Date(year, endMonth, 1);
Joins are not supported in MongoDB, which is what you are trying to do. The closest you can get is to filter as part of your .populate call:
.populate('game', null, {datePlayed: {$gte: startDateRange, $lt: endDateRange}})
.populate('player', null, {firstName: 'Brian'})
However what this will do is get all GamePlayer documents and only get the Game and Player subdocuments that match your criteria. If the subdocuments don't match your criteria, the GamePlayer document will still be returned with .game or .player equal to null.
You may want to reconsider your schema to be less like a SQL schema and to take advantage of the benefits of MongoDB. I'm not sure what your requirements are, but consider something like this:
var PlayerSchema = new Schema({
firstName: { type: String, default: '', required: true},
lastName: { type: String, default: '', required: true},
nickname: { type: String, default: '' },
});
var GameSchema = new Schema({
winner: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
datePlayed: { type: Date, default: Date.now, required: true },
players: [{
player: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
points: { type: Number, default: 0 },
place: { type: Number, default: 0 }
}]
});
Then your query could look something like:
GameModel.find()
.where('players.place').equals(1) // This will limit the result set to include only Games which have at least one Player with a place of 1
.where('datePlayed').gte(startDateRange).lt(endDateRange)
.populate('players.player')
.exec(function (err, games) {
if(err) return next(err);
res.json(games);
});
The above example will include all Players in each returned Game, regardless of whether their place is 1, however it will only include Games which have at least one player with a place of 1.
If you want to limit the items in the players array, you might need to take it a step further and use an aggregate command to unwind the array then filter. For example:
GameModel.aggregate([
{$unwind: 'players'},
{$match: {'players.place' : 1}}
], function(err, results) {
if (err) return next(err);
res.json(results);
});
This will return a separate Game object for each Player with a place of 1. If a Game has more than one Player with a place of 1, it will return duplicate Game objects, each with a different Player.

Resources