Mongoose - find all documents whose array property contains a given subset? - node.js

I have a (simplified) model based on the following schema:
schema = mongoose.Schema({
foo: { type: String },
bars: [{ type: String }]
});
model = mongoose.model ('model', schema);
and I want to create an API to return all documents that contain all of the 'bars' which are provided in a comma separated list. So I have:
exports.findByBars = function (req, res) {
var barsToFind = req.body.bars.split(',');
// find the matching document
};
Does Mongoose provide an API for this or is there a query param I can pass to Model#find to get this functionality? I only want a document to be returned if its bar property contains all of the values in the barsToFind array.

There are ways to structure your query to do this, the approaches are different depending on your mongodb server version. So following on from your code:
For MongoDB version 2.6 and above, use the $all operator:
model.find({
"bars": { "$all": barsToFind }
},
And though the operator does exist for previous versions, it behaves differently so you actually need to generate an $and statement to explicitly match each entry:
var andBars = [];
barsToFind.forEach(function(bar) {
andBars.push({ "bars": bar })
});
model.find({
"$and": andBars
},
Both ensure that you only match documents that contain all of the entries in the array as you have specified, it's just that the syntax available to MongoDB 2.6 is a little nicer.

Related

Mongoose Query with Dynamic Key that also contains the "and" operator

So I'm trying to query my MongoDB database using mongoose to fetch documents that have a specific family AND a specific analysis ID at the same time. Here is an example of the document structure:
_id: ObjectId("62b2fb397fda9ba6fe24aa5c")
day: 1
family: "AUTOMOTIVE"
prediction: -233.99999999999892
analysis: ObjectId("629c86fc67cfee013c5bf147")
The problem I face in this case is that the name of the key of the family field is set dynamically and could therefore have any other name such as "product_family", "category", etc. This is why, in order to fetch documents with a dynamic key name, I have to use the where() and equals() operators like so:
// Get the key of the field that is set dyncamically.
let dynamicKey = req.body.dynamicKey;
// Perform a query using a dynamic key.
documents = await Model.find().where(dynamicKey).equals(req.body.value);
HOWEVER, my goal here is NOT to just fetch all the documents with the dynamic key, but rather to fetch the documents that have BOTH the dynamic key name AND ALSO a specific analysis Id.
Had the family field NOT been dynamic, I could have simply used a query like so:
documents = await Model.find({
$and: [{analysis: req.body.analysis_id}, {family: req.body.value}]
});
but this does not seem possible in this case since the keys inside the find() operator are mere text strings and not variables. I also tried using the following queries with no luck:
documents = await Model.find().where(dynamicKey).equals(req.body.value).where('analysis').equals(req.body.analysis_id);
documents = await Model.find().where(dynamicKey).equals(req.body.value).where('analysis').equals(req.body.analysis_id);
Can somebody please help?
As #rickhg12hs mentioned in the comments, part of the answer is to use the [] brackets to specify your dynamic key like so:
await Model.find({[dynamicKey]: req.body.value, analysis: req.body.analysis_id});
I also found out that another query that can work is this:
await Model.find({analysis:req.body.analysis_id}).where(dynamicKey).equals(req.body.value);
However, it seems that for either of these solutions to work you also need to set your schema's strict mode to "false", since we are working with a dynamic key value.
Example:
var predictionSchema = new mongoose.Schema({
day: {
type: Number,
required: true
},
prediction: {
type: Number,
required: true
},
analysis: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Analysis', // Reference the Analysis Schema
required: true
}
}, { strict: false });

Mongoose query using Javascript Object vs Javascript Map

I have a mongoose model that has a field called timeLimit which is an Object
timeLimit: {
type: Object,
// of: Date,
},
If I try to find documents in this collection using the following query
const docs = await Model.find({
'timeLimit.end': {
$gt: moment().utc(),
},
});
I get no result, despite the fact that there are valid values to be returned. However, if I change the field in the model to match a Map of Dates...
timeLimit: {
type: Map,
of: Date,
},
The query returns the valid values of this query.
My question is, why is the query invalid when using the Object mongoose type if MongoDB doesn't even have the Map type, only the Object type?

Mongoose - get length of array in model

I have this Mongoose schema:
var postSchema = mongoose.Schema({
postId: {
type: Number,
unique: true
},
upvotes: [
{
type: Number,
unique: true
}
]
});
what the best query to use to get the length of the upvotes array? I don't believe I need to use aggregation because I only want to query for one model, just need the length of the upvotes array for a given model.
Really struggling to find this info online - everything I search for mentions the aggregation methodology which I don't believe I need.
Also, as a side note, the unique schema property of the upvotes array doesn't work, perhaps I am doing that wrong.
find results can only include content from the docs themselves1, while aggregate can project new values that are derived from the doc's content (like an array's length). That's why you need to use aggregate for this, even though you're getting just a single doc.
Post.aggregate([{$match: {postId: 5}}, {$project: {upvotes: {$size: '$upvotes'}}}])
1Single exception is the $meta projection operator to project a $text query result's score.
I'm not normally a fan of caching values, but it might be an option (and after finding this stackoverflow answer is what I'm going to do for my use case) to calculate the length of the field when the record is updated in the pre('validate') hook. For example:
var schema = new mongoose.Schema({
name: String,
upvoteCount: Number,
upvotes: [{}]
});
schema.pre('validate', function (next) {
this.upvoteCount = this.upvotes.length
next();
});
Just note that you need to do your updates the mongoose way by loading the object using find and then saving changes using object.save() - don't use findOneAndUpdate
postSchema.virtual('upvoteCount').get(function () {
return this.upvotes.length
});
let doc = await Post.findById('foobar123')
doc.upvoteCount // length of upvotes
My suggestion would be to pull the entire upvotes fields data and use .length property of returned array in node.js code
//logic only, not a functional code
post.find( filterexpression, {upvote: 1}, function(err, res){
console.log(res.upvotes.length);
});
EDIT:
Other way of doing would be stored Javascript. You can query the
upvote and count the same in mongodb side stored Javascript using
.length

Mongoose $push cannot push object into correct document

I have a mongoose schema like this:
A = {
_id: Schema.Types.ObjectId,
arrayA:[{
_id,
nestedArray: [Schema.Types.ObjectId]
}],
arrayB: [Schema.Types.ObjectId]
}
I would like to push an Object Id into nestedArray in specific arrayA object AND
arrayB should contains an specific Object Id by following code:
A.update({'arrayA._id': arrayAId, arrayB: {$in: [arrayContainsSomeArrayBIds]}},
{$push: {'arrayA.$.nestedArray': nestedArrayId}}, function(err) {
});
However, the Object Id is pushed into nestedArray of the last object in arrayA.
If arrayB: {$in: [arrayContainsSomeArrayBIds]} is removed, the Object Id can be pushed into correct object in arrayA.
mongoose version: 3.8.21
Can anyone help me to find out the problem?
Currently it is not possible in MongoDB to update an a array element with the positional operator, when the query document contains references to other arrays apart from the one being updated.
The below code, contains reference to two arrays fields: arrayA and arrayB, when
the update is issued on arrayA. This is invalid and would lead to undesired behavior.
A.update({'arrayA._id': arrayAId, arrayB: {$in: [arrayContainsSomeArrayBIds]}},
{$push: {'arrayA.$.nestedArray': nestedArrayId}}, function(err) {
});
From the docs,
Only one array field may appear in the query document.
The query document should only contain a single condition on the array field
being projected.
Multiple conditions may override each other
internally and lead to undefined behavior.
Under these requirements,
the following query is incorrect:
db.collection.find( { <array>: <value>, <someOtherArray>: <value2> },
{ "<array>.$": 1 } )
The solution is to modify your code to fire two queries:
Get the _ids of the documents, which match our condition.
Then perform the update.
Sample Code flow:
A.find({'arrayA._id': arrayAId, arrayB: {$in: [arrayContainsSomeArrayBIds]}},
function(err,data){
data.forEach(function(doc){
A.update({'arrayA._id': arrayAId,
"_id":doc._id},
{$push: {'arrayA.$.nestedArray': nestedArrayId}},
function(err) {
});
})
});

Mongoose Query: compare two values on same document

How can I query a Mongo collection using Mongoose to find all the documents that have a specific relation between two of their own properties?
For example, how can I query a characters collections to find all those characters that have their currentHitPoints value less than their maximumHitPoints value? Or all those projects that have their currentPledgedMoney less than their pledgeGoal?
I tried to something like this:
mongoose.model('Character')
.find({
player: _currentPlayer
})
.where('status.currentHitpoints').lt('status.maximumHitpoints')
.exec(callback)
but I am getting errors since the lt argument must be a Number. The same goes if I use $.status.maximumHitpoints (I was hoping Mongoose would be able to resolve it like it does when doing collection operations).
Is this something that can be done within a Query? I would expect so, but can't find out how. Otherwise I can filter the whole collection with underscore but I suspect that is going to have a negative impact on performance.
PS: I also tried using similar approaches with the find call, no dice.
MongoDB 3.6 and above supports aggregation expressions within the query language:
db.monthlyBudget.find( { $expr: { $gt: [ "$spent" , "$budget" ] } } )
https://docs.mongodb.com/manual/reference/operator/query/expr/
Thanks to Aniket's suggestion in the question's comments, I found that the same can be done with Mongoose using the following syntax:
mongoose.model('Character')
.find({
player: _currentPlayer
})
.$where('this.status.currentHitpoints < this.status.maximumHitpoints')
.exec(callback)
Notice the $where method is used instead of the where method.
EDIT: To expand on Derick's comment below, a more performance sensitive solution would be to have a boolean property inside your Mongoose schema containing the result of the comparison, and update it everytime the document is saved. This can be easily achieved through the use of Mongoose Schema Plugin, so you would have something like:
var CharacterSchema = new mongoose.Schema({
// ...
status: {
hitpoints: Number,
maxHitpoints: Number,
isInFullHealth: {type: Boolean, default: false}
}
})
.plugin(function(schema, options) {
schema.pre('save', function(next) {
this.status.isInFullHealth = (this.status.hitPoints >= this.status.maxHitpoints);
next();
})
})
mongoose.model('Character')
.find({
player: _currentPlayer, $expr: { $lt: ['$currentHitpoints', '$maximumHitpoints'] }
})
This above query means find the record which has currentHitpoints less than maximumHitpoints
Starting in MongoDB 5.0, the $eq, $lt, $lte, $gt, and $gte comparison operators placed in an $expr operator can use an index on the from collection referenced in a $lookup stage.
Example
The following operation uses $expr to find documents where the spent amount exceeds the budget:
db.monthlyBudget.find( { $expr: { $gt: [ "$spent" , "$budget" ] } } )

Resources