I am trying to build a dynamic $match: for my MongoDB aggregate
This is what I wish I could do:
var matchStr = "";
if (req.body.propertyID) {
matchStr += "property: { $in: properties },"
}
if (req.body.status=="ACTIVE" || req.body.status=="NOPAY") {
matchStr += "status : {$lte: 4},"
} else if (req.body.status) {
matchStr += "status : {$lte: req.body.status},"
}
matchStr += "checkin : {$gte: (checkin)}, checkout: {$lte: (checkout)}"
And this is what I end up doing:
Its so messy, and now I have to add even more statements to the $match which will give me even bigger if structures.
There has to be a better way to do this!
I hope someone out there has an idea how to build these dynamically without the "if hell :)"
if (req.body.propertyID & req.body.status=="ALL") {
var matchStr = {
$match: {
$and:
[{
property: { $in: properties },
checkin : {$gte: (checkin)},
checkout: {$lte: (checkout)}
}]
}
}
} else if (!req.body.propertyID & !req.body.status) {
var matchStr = {
$match: {
$and:
[{
checkin : {$gte: (checkin)},
checkout: {$lte: (checkout)}
}]
}
}
} else if ((req.body.status=="ACTIVE" || req.body.status=="NOPAY")) {
if (req.body.propertyID) {
var matchStr = {
$match: {
$and:
[{
property: { $in: properties },
status : {$lte: 4},
checkin : {$gte: (checkin)},
checkout: {$lte: (checkout)}
}]
}
}
} else {
var matchStr = {
$match: {
$and:
[{
status : {$lte: 4},
checkin : {$gte: (checkin)},
checkout: {$lte: (checkout)}
}]
}
}
}
} else {
if (req.body.propertyID) {
var matchStr = {
$match: {
$and:
[{
property: { $in: properties },
status : {$lte: req.body.status},
checkin : {$gte: (checkin)},
checkout: {$lte: (checkout)}
}]
}
}
} else {
var matchStr = {
$match: {
$and:
[{
status : {$lte: req.body.status},
checkin : {$gte: (checkin)},
checkout: {$lte: (checkout)}
}]
}
}
}
}
UPDATE! - Almost got it now, except it puts the "myMatch" inside the mongoDB statement.
Here is the new code:
var myMatch = {
checkin: {$gte: checkin},
checkout: {$lte: checkout}
};
if (req.body.hasOwnProperty('propertyID')) {
var properties = req.body.propertyID.map(function (obj) {
return obj._id;
});
console.log("properties: " + properties);
myMatch["property"] = { "$in": properties };
}
if (req.body.status=="ACTIVE" || req.body.status=="NOPAY") {
myMatch["status"] = { "$lte": 4 };
} else if (req.body.hasOwnProperty('status')) {
myMatch["status"] = { "$lte": +req.body.status };
}
And here is a console log of that structure:
myMatch: {
"checkin": {
"$gte": 1473571600
},
"checkout": {
"$lte": 1596163600
},
"property": {
"$in": [
"PAP-720",
"ATL-D406",
"WAT-606"
]
},
"status": {
"$lte": 3
}
}
Here is where I inject the myMatch object:
bookingTable.aggregate([
{
$match: {
$and:
[{
myMatch
}]
}
},
And here is the final mongodb statement:
Mongoose:
booking.aggregate([
{ '$match': { '$and': [ { myMatch: { checkin: { '$gte': 1473571600 }, checkout: { '$lte': 1596163600 }, property: { '$in': [ 'PAP-720', 'ATL-D406', 'WAT-606' ] }, status: { '$lte': 3 } } } ] } },
First thing you really need to note is that you almost never need to write an explicit $and statements. It has a usage, but typically all expressions in a MongoDB query are already an "AND" operation.
The next thing is that the solution will look remarkably familiar:
var myMatch = {
checkin: checkin,
checkout: checkout
};
if (req.body.propertyID) {
myMatch["property"] = { "$in": properties };
}
if (req.body.status=="ACTIVE" || req.body.status=="NOPAY") {
myMatch["status"] = { "$lte": 4 };
} else if (req.body.hasOwnProperty('status')) {
myMatch["status"] = { "$lte": +req.body.status };
}
The property names are pretty normal so you could alternately do:
myMatch.property = { "$in": properties };
But this all comes down to basic manipulation of JavaScript objects. And it's pretty much the same in any dynamically typed language.
Then you can just do later:
{ "$match": myMatch }
And its done.
Related
I am using the mongo change stream to update some of my data in the cache. This is the Documentation i am following.
I want to know when one of the keys in my object gets updated.
Schema :- attributes: { position: { type: Number } },
How will I come to know when the position will get updated?
This is how my updateDescription object looks like after updation
updateDescription = {
updatedFields: { 'attributes.position': 1, randomKey: '1234'},
removedFields: []
}
Tried this and its not working
Collection.watch(
[
{
$match: {
$and: [
{ operationType: { $in: ['update'] } },
{
$or: [
{ 'updateDescription.updatedFields[attributes.position]': { $exists: true } },
],
},
],
},
},
],
{ fullDocument: 'updateLookup' },
);
I was able to fix it like this,
const streamUpdates = <CollectionName>.watch(
[
{
$project:
{
'updateDescription.updatedFields': { $objectToArray: '$updateDescription.updatedFields' },
fullDocument: 1,
operationType: 1,
},
},
{
$match: {
$and: [
{ operationType: 'update' },
{
'updateDescription.updatedFields.k': {
$in: [
'attributes.position',
],
},
},
],
},
},
],
{ fullDocument: 'updateLookup' },
);
and then do something with the stream
streamUpdates.on('change', async (next) => {
//your logic
});
Read more here MongoDB
I'm trying to find stocks in the Stock collection where the sum of all owners' shares is less than 100. Here is my schema.
const stockSchema = new mongoose.Schema({
owners: [
{
owner: {
type: Schema.Types.ObjectId,
ref: "Owner"
},
shares: {
type: Number,
min: 0,
max: 100
}
}
]
}
const Stock = mongoose.model("Stock", stockSchema);
I've tried to use aggregate but it returns a single object computed over all stocks in the collection, as opposed to multiple objects with the sum of each stock's shares.
stockSchema.statics.getUnderfundedStocks = async () => {
const result = await Stock.aggregate([
{ $unwind: "$owners" },
{ $group: { _id: null, shares: { $sum: "$owners.shares" } } },
{ $match: { shares: { $lt: 100 } } }
]);
return result;
};
So, rather than getting:
[ { _id: null, shares: 150 } ] from getUnderfundedStocks, I'm looking to get:
[ { _id: null, shares: 90 }, { _id: null, shares: 60 } ].
I've come across $expr, which looks useful, but documentation is scarce and not sure if that's the appropriate path to take.
Edit: Some document examples:
/* 1 */
{
"_id" : ObjectId("5ea699fb201db57b8e4e2e8a"),
"owners" : [
{
"owner" : ObjectId("5ea62a94ccb1b974d40a2c72"),
"shares" : 85
}
]
}
/* 2 */
{
"_id" : ObjectId("5ea699fb201db57b8e4e2e1e"),
"owners" : [
{
"owner" : ObjectId("5ea62a94ccb1b974d40a2c72"),
"shares" : 20
},
{
"owner" : ObjectId("5ea62a94ccb1b974d40a2c73"),
"shares" : 50
},
{
"owner" : ObjectId("5ea62a94ccb1b974d40a2c74"),
"shares" : 30
}
]
}
I'd like to return an array that just includes document #1.
You do not need to use $group here. Simply use $project with $sum operator.
db.collection.aggregate([
{ "$project": {
"shares": { "$sum": "$owners.shares" }
}},
{ "$match": { "shares": { "$lt": 100 } } }
])
Or even you do not need to use aggregation here
db.collection.find({
"$expr": { "$lt": [{ "$sum": "$owners.shares" }, 100] }
})
MongoPlayground
I'm currently trying to get the total amount of items in my News object, and return a slice of the items as objects.
I found out how to use the $slice operator in my query, but I don't know how to get the original size of the array of items.
The code I'm currently using in NodeJS:
if (req.query.limit) {
limit = 5;
}
News.findOne({ connected: club._id }, {items: {$slice: limit}}).exec(function (err, news) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else if (!news || news.items.length === 0) {
res.jsonp([]);
} else {
const returnObj = { items: [], totalNumber: 0 };
const items = news.items.sort(function (a, b) {
return b.date - a.date
});
res.jsonp({
items: items,
totalNumber: news.items.length
});
}
});
The Mongo model:
var mongoose = require('mongoose'),
validator = require('validator'),
Schema = mongoose.Schema;
var NewsSchema = new Schema({
connected: {
type: Schema.Types.ObjectId,
required: 'Gelieve een club toe te wijzen.',
ref: 'Club'
},
items: [{
userFirstName: String,
action: String,
date: Date,
targetName: String
}],
created: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('News', NewsSchema);
How would I do this efficiently?
Thanks!
EDIT: final code which works:
News.aggregate([{
$match: {
connected: club._id
}
}, {
$project: {
totalNumber: {
$size: '$items'
},
items: {
$slice: ['$items', limit]
}
}
}
]).exec(function (err, news) {
console.log(news);
if (!news || news[0].items.length === 0) {
res.jsonp([]);
} else {
res.jsonp(news[0]);
}
});
You cannot have both information at once using find and $slice.
The soluce you have :
Use aggregate to return the count and only the sliced values.
Like :
[{
$project: {
count: {
$size: "$params",
},
params: {
$slice: ["$params", 5],
},
},
}]
To help you out making aggregate, you can use the awesome mongodb-compass software and its aggregate utility tool.
Use a find without $slice, get the number of item there, and then slice in javascript the array before returning it.
EDIT :
[{
$sort: {
'items.date': -1,
},
}, {
$project: {
count: {
$size: "$items",
},
params: {
$slice: ["$items", 5],
},
},
}]
how to make a mongodb collection find (db.collection.find) with empty values?
currently i have:
function test(s) {
if (!s) {
return null;
} else {
return s;
}
}
var content = {
date: {
from: '2017-10-15',
to: '2017-11-15'
},
'name': 'some text', //this can be null or empty
'text': 'some other text' //this can be null or empty
}
col.find({
"date": {
$gte: new Date(content.date.from),
$lte: new Date(content.date.to)
},
"name": {
$ne: {
$type: null
},
$eq: test(content.name)
},
"text": {
$ne: {
$type: null
},
$eq: test(content.text)
},
}).toArray((err, items) => {
console.log(items)
});
but it returns an empty array, because "name" or "text" is null / an empty string,
i want that it query only the values that have something specified or ignore it (like content.name is something in it or its empty)
how do i get it? i already searched ... but didnt found something
thanks!
( already testet mongoDB : multi value field search ignoring null fields)
Versions:
Node: 8.9.0
(npm) mongodb: 2.2.33
mongodb: 3.4
Try using $and, $or operators. Something like.
col.find({
$and:[
{"date": {$gte: new Date(content.date.from),$lte: new Date(content.date.to)}},
{"$or":[{"name": {$ne: {$type: null}}},{"name":test(content.name)}]},
{"$or":[{"text": {$ne: {$type: null}}},{"text":test(content.text)}]}
]
}).toArray((err, items) => {
console.log(items)
});
col.find({
$and: [
{
$and: [
{
"date": {
$gte: new Date(content.date.from)
}
},
{
"date": {
$lte: new Date(content.date.to)
}
},
]
},
{
$or: [
{ 'name': null },
{ 'name': content.name }
]
},
{
$or: [
{ 'text': null },
{ 'text': content.text }
]
}
]
})
Edited:
col.find({
$and: [
{
$and: [
{
"date": {
$gte: new Date(content.date.from)
}
},
{
"date": {
$lte: new Date(content.date.to)
}
},
]
},
{
$or: [
{ 'name': null },
{ 'name':'' }
{ 'name': content.name }
]
},
{
$or: [
{ 'text': null },
{ 'text': '' },
{ 'text': content.text }
]
}
]
})
null and empty is different, you need to add one more condition for empty string in query.
Logical Query Operators
I have a very loooong aggregate in my nodejs controller:
agentSaleModel.aggregate([
{
$match: {
$and:
[{
_id: { $in: req.body.propertyID },
active : true
}]
}
}, etc....
And it works great when I got elements in my req.body.propertyID
like ["property01","property02"] etc...
My problem is that I also want the aggregate to work when there are nothing in req.body.propertyID. (when its blank) - and then get all records.
This does not work:
agentSaleModel.aggregate([
{
$match: {
$and:
[{
_id: { $in: '' },
active : true
}]
}
}, etc....
So rather than doing an "if" with two huge sets of almost identical code:
if (req.body.propertyID) {
...the entire aggregate...
} else {
...the entire aggregate minus the _id: { $in: req.body.propertyID },...
}
Is there a smarter way to do this?
SOLUTION!! Thanks to FluffyNights :)
if (req.body.propertyID!='') {
var matchStr = {
$match: {
$and:
[{
_id: { $in: req.body.propertyID },
active : true
}]
}
}
} else {
var matchStr = {
$match: {
active : true
}
}
}
agentSaleModel.aggregate([ matchStr, etc..... (rest of pipeline)
you could do it like this:
let query = [
{
$match: {
$and:
[{
active : true
}]
}
}];
if(req.body.propertyID) {
query[0]["$match"]["$and"][0]["_id"] = { $in: req.body.propertyID };
}
agentSaleModel.aggregate(query, ...)
you could also use regex, like:
if(!req.body.properyID){
req.body.propertyID = [ ".*" ];
}
agentSaleModel.aggregate([
{
$match: {
$and:
[{
_id: { $in: req.body.propertyID },
active : true
}]
}
}, etc....
however, this might get slow.
Im not sure if passing null to $in would work the way you want, you could try it though.
What about trying to construct query before running it.
For example.
var query = req.body.propertyID ? { $and: [{_id: { $in: req.body.propertyID }, active : true}]} : {active : true}
agentSaleModel.aggregate([
{
$match: query
}, etc....
Hope this helps.
Here's an inline solution using computed property names:
$match: {
$and: [
{
_id: { [ req.body.propertyID ? '$in' : '$exists' ] : req.body.propertyID || true },
active: true,
},
],
}
When req.body.propertyID exists, the query becomes:
_id : { $in : req.body.propertyID }
If not:
_id : { $exists : true }
EDIT: this will also allow req.body.propertyID to equal "ALL" if you explicitly want to match all documents:
let selectAll = ! req.body.propertyID || req.body.propertyID === 'ALL';
const query = {
$match: {
$and: [
{
_id: { [ selectAll ? '$exists' : '$in' ] : selectAll || req.body.propertyID },
active: true,
},
],
},
};