Mongoose .find() is empty when querying on subdocument - node.js

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.

Related

Populate() of Populate() in Mongoose for querying to match $in array

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.

Mongoose Add New Field To Collection (Node.js)

I have a schema:
var userSchema = new Schema({
name: String,
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
admin: Boolean,
created_at: Date,
updated_at: Date
});
Let's assume I have made 100 Users using this schema.
Now I want to change the schema:
var userSchema = new Schema({
name: String,
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
admin: Boolean,
created_at: Date,
friends: [Schema.Types.ObjectId], //the new addition
updated_at: Date
});
I need all new Users to have this field. I also want all of the 100 existing Users to now have this field. How can I do this?
You can use Mongoose Model.update to update all your documents in the collection.
User.update({}, { friends: [] }, { multi: true }, function (err, raw) {
if (err) return handleError(err);
console.log('The raw response from Mongo was ', raw);
});
I don't recommend to do it in production if the collection is big, since it is a heavy operation. But in your case it should be fine.
Using the query interface in a client app or your terminal you could do:
db.users.updateMany({
$set: { "friends" : [] }
});
Here's the docs reference.
it doesn't work for me :x
Here is my code
let test = await this.client.db.users.updateMany({
$set: { "roles" : [] }
});
and the output
{ ok: 0, n: 0, nModified: 0 }
I don't know how to do, i tried a lot of things and uh it doesn't work :'(
EDIT: I found, here is my code
await this.client.db.users.updateMany({ }, [ {$set : { "roles": []} } ]);

How can I sum up values in my mongodb collection?

I have a model in mongoose that looks similar to this:
var TestSchema = new Schema({
test_username: {type: String, required: true},
test_content: {type: String},
reactions: [{
test_username: {type: String, required: true},
value: {type: Number, required: true},
sent_at: {type: Date, required: true, default: Date.now}
}],
created_at: {type: Date, default: Date.now},
updated_at: {type: Date, default: Date.now}
})
it stores my Test object with many reactions in it. Each reaction contains either 1 or -1 value and different usernames.
Now I'm trying to create an endpoint that gets the Test id as an input and returns the total, summed amount from all reactions that it contains.
I started writing it as:
testRoutes.get('/:id/reactions/', functions.validateRequestsGET, function(req, res){
var testId = req.params.id;
var query = Test... //here I'm stuck
query.exec(function(err, reactions){
if(err) {
res.send(err);
return;
}
res.json(reactions);
});
});
can you give me a hint of how to create a query that could return me a json with the summed amount? something like {reactions: 17} or similar?
Try this:
Test.aggregate(
{ $match: {
_id: testId // you might want to convert this from string to ObjectId()
}},
{ $project: {
sumReactions: { $sum: "$reactions.value" }
}}
)
Take a look at group accumulators $group in documentation , good examples too.

Mongoose display with relationships

I have three tables 'cases', 'partners' and 'casepartners' with the following structure:
cases: id,subject,description
partners: id,name,address
casepartners:case,partner,createdAt
I would like to list all cases by also showing for each case, casepartners records where the case id is the same.
I am using this code:
Case.find().sort('-created').exec(function (err, cases) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(cases);
}
});
It shows all cases fine, but I would like to also show for each case object a list of casepartners for that case id...
Let's say a few partners got subscribed to the same case and I would like to list all of those partners or just count how many partners got subscribed to that case.
I am using Angularjs to list all cases using the ng-repeat but I am kinda confused if I have to make a separate call to show casepartners records for each case within ng-repeat or attach this in the same function by using some kind of .populate() or something else with the entity relationships.
These are the models defined:
var CaseSchema = new Schema({
subject: {
type: String,
default: '',
trim: true,
required: 'Subject cannot be blank'
},
description: {
type: String,
default: '',
trim: true
},
created: {
type: Date,
default: Date.now
},
customer: {
type: Schema.ObjectId,
ref: 'Customer'
},
category: {
type: Schema.ObjectId,
ref: 'Category'
}
});
var CasePartnerSchema = new Schema({
case: {
type: Schema.ObjectId,
ref: 'Case'
},
partner: {
type: Schema.ObjectId,
ref: 'Partner'
},
created: {
type: Date,
default: Date.now
}
});
var PartnerSchema = new Schema({
name: {
type: String,
trim: true,
default: ''
},
created: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
Can anyone help with this?
If possible I would recommend redefining your collections (tables) since having a CasePartner collection is a little redundant.
Instead of having a case and casePartner collection, I would recommend you only have a case collection and then have an array of partners inside of that collection.
Your schema would then look like this:
var CaseSchema = new Schema({
partners: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Partner'
}],
subject: {
type: String,
default: '',
trim: true,
required: 'Subject cannot be blank'
},
description: {
type: String,
default: '',
trim: true
},
created: {
type: Date,
default: Date.now
},
customer: {
type: Schema.ObjectId,
ref: 'Customer'
},
category: {
type: Schema.ObjectId,
ref: 'Category'
}
});
Your find would then look like this:
Case
.find({})
.sort('-created')
//.populate() populates all info from the partnersSchema for each partner
.populate('partners')
.exec(function(err, cases) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(cases);
}
});
Check out this for more on MongoDB schema design.
try mongoose-reverse-populate. There is a way called populate in mongoose but that helps you when you are first searching caseparters and populate case types . however what you wanted is kind of reverse of that .

Mongoose Populate - array

can someone please help me with population of this schema? I need to populate array of Staff by their userId.
var PlaceSchema = new Schema ({
name: { type: String, required: true, trim: true },
permalink: { type: String },
country: { type: String, required: true },
...long story :D...
staff: [staffSchema],
admins: [adminSchema],
masterPlace:{ type: Boolean },
images: []
});
var staffSchema = new Schema ({
userId: { type: Schema.Types.ObjectId, ref: 'Account' },
role: { type: Number }
});
var adminSchema = new Schema ({
userId: { type: Schema.Types.ObjectId, ref: 'Account'}
})
var Places = mongoose.model('Places', PlaceSchema);
I tried to use this query, but without success.
Places.findOne({'_id' : placeId}).populate('staff.userId').exec(function(err, doc){
console.log(doc);
});
Polpulation is intended as a method for "pulling in" information from the related models in the collection. So rather than specifying a related field "directly", instead reference the related fields so the document appears to have all of those sub-documents embedded in the response:
Places.findOne({'_id' : placeId}).populate('staff','_id')
.exec(function(err, doc){
console.log(doc);
});
The second argument just returns the field that you want. So it "filters" the response.
There is more information on populate in the documentation.

Resources