I have a function that should return a count of users created in a given period and group by invited and non invited. On the $cond operator, I need to compare if the field tkbSponsor is not null and is not equals example#example.com. If this condition results to true, then the user was invited. Otherwise, he wasn't invited.
var countByPeriod = function(req,res) {
var initialDate = req.body.initialDate;
var finalDate = req.body.finalDate;
User.aggregate([
{
"$match": {
"timeStamp": {
"$gte": new Date(initialDate),
"$lt": new Date(finalDate)
}
}
},
{
"$group": {
"_id": null,
"total": { "$sum": 1 },
"invited": {
"$sum": {
"$cond": [
{
"tkbSponsor": {
"$nin": ["example#example.com",null]
}
},
1,
0
]
}
}
}
}
], (err,result) => {
if (!err) {
if (result.length) res.send(result[0]);
else res.send({"total": 0,"invited":0});
} else {
res.sendStatus(500);
console.log(err);
}
});
};
By the way, this function is giving me an error when executed:
{ [MongoError: invalid operator '$nin']
name: 'MongoError',
message: 'invalid operator \'$nin\'',
ok: 0,
errmsg: 'invalid operator \'$nin\'',
code: 15999 }
Just an observation. I used to use the $cond operator as below, because I didn't needed to compare with null:
"$cond": [
{
"$ne": ["$tkbSponsor", "example#example.com"]
},
1,
0
]
And it works. However, now I have also to compare if the tkbSponsor is not null and using $nin, is giving me that error.
Change that to use $and together with the $ifNull coalesce as :
{
"$cond": [
{
"$and": [
{ "$ne": ["$tkbSponsor", "example#example.com"] },
{
"$ne": [
{ "$ifNull": [ "$tkbSponsor", null ] },
null
]
}
]
}, 1, 0
]
}
or using $or as
{
"$cond": [
{
"$or": [
{ "$eq": ["$tkbSponsor", "example#example.com"] },
{
"$eq": [
{ "$ifNull": [ "$tkbSponsor", null ] },
null
]
}
]
}, 0, 1
]
}
The $ifNull operator's presence is to act as an $exists operator by replacing "non-existant" or null fields with a null value for evaluation.
Running this should return the correct results as the earlier revision was only evaluating documents where the tkbSponsor field exists AND has either a value of null or "example#example.com".
With $ifNull, "non-existant" fields are also evaluated as the operator gives them the null value.
Related
I have tried the below query of mongoose which does not seem to work:
Model.findOneAndUpdate(
{
name: origin
},
{
$set: {
'field1.$[id1].field2.-1': "value"
}
},
{
arrayFilters: [
{ 'id1.userId': "customerId" }
],
new: true
}
);
Note: field1 and field2 are arrays
The negative indexes are not accepted by MongoDB which is causing problems.
You may consider using the $set (aggregation) operator and use double $map operator:
db.collection.aggregate([
{ $match: { name: "myname" } }
{
$set: {
field1: {
$map: {
input: "$field1",
in: {
$cond: {
if: { $ne: [ "$$this.userId", "customerId" ] },
then: "$$this",
else: {
$mergeObjects: [
"$$this",
{
field2: {
$concatArrays: [
{ $slice: [ "$$this.field2", { $add: [ { $size: "$$this.field2" }, -1 ] } ] },
["value"]
]
}
}
]
}
}
}
}
}
}
}
])
Mongo Playground
Apply the $set operator together with the $ positional operator in your update to change the name field.
The $ positional operator will identify the correct element in the array to update without explicitly specifying the position of the element in the array, thus your final update statement should look like:
db.collection.update(
{ "friends.u.username": "michael" },
{ "$set": { "friends.$.u.name": "hello" } }
)
Answer taken from - https://stackoverflow.com/a/34431571
I need to be able to find any conditions inside an array of a document in my collection based on the value of another field.
My document:
{
"totalSteps": 3,
"currentStep": 2,
"status": "submitted",
"completed": false,
"completedDate": null,
"orderBody": [
{
"status": "complete",
"stepStarted": 1617207419303,
"stepEnded": "",
"executionOutput": ""
},
{
"status": "incomplete",
"stepStarted": 1617211111113,
"stepEnded": "",
"executionOutput": ""
},
{
"status": "incomplete",
"stepStarted": 1617207419303,
"stepEnded": "",
"executionOutput": ""
}
],
}
My query:
...find($and: [
{ orderBody: {$elemMatch: { "stepStarted" : { $lte: currentTime }, status : "incomplete"}}},
{status: { $ne: "failed"}}
])
My Issue:
I need the document returned only if the value of (currentStep - 1) is the same as the matched array. Right now the query will return the document because the conditions of orderBody[2] are fulfilled. Notice the stepStarted of orderBody[2] is < orderBody[1]. currentTime is a variable passed from server in another section of code.
I've tried:
$and: [
{ currentStep:{ {$indexOfArray: {orderBody: {$elemMatch: { "stepStarted" : { $lte: currentTime }, status : "incomplete"}}} - 1}},
{status: { $ne: "failed"}}
]
$and: [
{ currentStep: { $eq: {$indexOfArray: {orderBody: {$elemMatch: { "stepStarted" : { $lte: currentTime }, status : "incomplete"}}}},
{status: { $ne: "failed"}}
]},
{ $and: [
{orderBody[currentStep - 1]: {$elemMatch: { "stepStarted" : { $lte: currentTime }, status : "incomplete"}}},
{status: { $ne: "failed"}}
]},
Any assistance on this would be greatly appreciated.
Demo - https://mongoplayground.net/p/d2ew5peV-z-
Use $project to extract exact array element pipeline you want from orderBody. Using $arrayElemAt.
$subtract currentStep value 1 to get the correct index ($toInt)
After that run your $match query on the document.
db.collection.aggregate({
$project: {
orderBody: {
"$arrayElemAt": [ "$orderBody", { $subtract: [ { $toInt: "$currentStep" }, 1 ] } ]
}
}
},
{
$match: {
"orderBody.stepStarted": { $gte: NumberLong(1217207419302) },
"orderBody.status": "incomplete"
}
})
Note- add details you want to project in $project pipeline.
Update
Demo - https://mongoplayground.net/p/E8Wo_YfFltq
Use $addFields
db.collection.aggregate({
$addFields: {
currentOrderBody: { $arrayElemAt: [ "$orderBody", { $subtract: [ { $toInt: "$currentStep" }, 1 ] } ] }
}
},
{
$match: {
"currentOrderBody.stepStarted": { $gte: NumberLong(1217207419302) },
"currentOrderBody.status": "incomplete"
}
})
I am trying to count database records in mongoDB (using mongoose) where records have a status of pending and approved as well as rejected. So, I am basically trying to get a result where I can show a count of each and display it in my view ie:
Pending: 35
Approved: 97
Rejected: 12
I have this but it only counts 'pending'. Is there a way to count all 3 in one query or do I need to run 3 separate queries and get a result for each of them?
Product.countDocuments({status: 'pending', userId: req.session.user._id})
.then(pending => {
if (!pending) {
return next();
}
req.pending = pending;
next();
})
.catch(err => {
console.log(err);
});
EDIT: I have managed to do it like this to a certain extent, well in console I am getting a count of all of the results back but just need to figure out how to get each one into it's own variable.
Product.aggregate([
{ $group: { _id: { status: "$status"}, totalStatus: {$sum: 1} } }
])
.then(function (res) {
console.log(res);
next();
});
For finding multiple count in single query,Please use mongodb aggregation framework it manipulate data in multiple stages, your question is already answered, please visit below link.I update the query for you.
Multiple Counts with single query in mongodb
Product.aggregate([
{ "$facet": {
"Pending": [
{ "$match" : { "status": { "$exists": true, "$in":["pending"] }}},
{ "$count": "Pending" },
],
"Approved": [
{ "$match" : {"status": { "$exists": true, "$in": ["approved"] }}},
{ "$count": "Approved" }
],
"Rejected": [
{ "$match" : {"status": { "$exists": true, "$in": ["rejected"] }}},
{ "$count": "Rejected" }
]
}},
{ "$project": {
"Pending": { "$arrayElemAt": ["$Pending.Pending", 0] },
"Approved": { "$arrayElemAt": ["$Approved.Approved", 0] },
"Rejected": { "$arrayElemAt": ["$Rejected.Rejected", 0] }
}}
])
Here is my item model.
const itemSchema = new Schema({
name: String,
category: String,
occupied: [Number],
active: { type: Boolean, default: true },
});
I want to filter 'occupied' array. So I use aggregate and unwind 'occupied' field.
So I apply match query. And group by _id.
But if filtered 'occupied' array is empty, the item disappear.
Here is my code
Item.aggregate([
{ $match: {
active: true
}},
{ $unwind:
"$occupied",
},
{ $match: { $and: [
{ occupied: { $gte: 100 }},
{ occupied: { $lt: 200 }}
]}},
{ $group : {
_id: "$_id",
name: { $first: "$name"},
category: { $first: "$category"},
occupied: { $addToSet : "$occupied" }
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
Here is example data set
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 95, 100, 145, 200 ],
"active" : true
},
{
"_id" : ObjectId("59c2dbed992fb91b7421b1ad"),
"name" : "printer2",
"category" : "printer",
"occupied" : [ ],
"active" : true
}
The result above query
[
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 100, 145 ],
"active" : true
}
]
and the result I want
[
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 100, 145 ],
"active" : true
},
{
"_id" : ObjectId("59c2dbed992fb91b7421b1ad"),
"name" : "printer2",
"category" : "printer",
"occupied" : [ ],
"active" : true
}
]
how could I do this??
Thanks in advance.
In the simplest form, you keep it simply by not using $unwind in the first place. Your conditions applied imply that you are looking for the "unique set" of matches to specific values.
For this you instead use $filter, and a "set operator" like $setUnion to reduce the input values to a "set" in the first place:
Item.aggregate([
{ "$match": { "active": true } },
{ "$project": {
"name": 1,
"category": 1,
"occupied": {
"$filter": {
"input": { "$setUnion": [ "$occupied", []] },
"as": "o",
"cond": {
"$and": [
{ "$gte": ["$$o", 100 ] },
{ "$lt": ["$$o", 200] }
]
}
}
}
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
Both have been around since MongoDB v3, so it's pretty common practice to do things this way.
If for some reason you were still using MongoDB 2.6, then you could apply $map and $setDifference instead:
Item.aggregate([
{ "$match": { "active": true } },
{ "$project": {
"name": 1,
"category": 1,
"occupied": {
"$setDifference": [
{ "$map": {
"input": "$occupied",
"as": "o",
"in": {
"$cond": {
"if": {
"$and": [
{ "$gte": ["$$o", 100 ] },
{ "$lt": ["$$o", 200] }
]
},
"then": "$$o",
"else": false
}
}
}},
[false]
]
}
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
It's the same "unique set" result as pulling the array apart, filtering the items and putting it back together with $addToSet. The difference being that its far more efficient, and retains ( or produces ) an empty array without any issues.
According to the documents:
The $pull operator removes from an existing array all instances of a value or values that match a specified condition.
Is there an option to remove only the first instance of a value? For example:
var array = ["bird","tiger","bird","horse"]
How can the first "bird" be removed directly in an update call?
So you are correct in that the $pull operator does exactly what the documentation says in that it's arguments are in fact a "query" used to match the elements that are to be removed.
If your array content happened to always have the element in the "first" position as you show then the $pop operator does in fact remove that first element.
With the basic node driver:
collection.findOneAndUpdate(
{ "array.0": "bird" }, // "array.0" is matching the value of the "first" element
{ "$pop": { "array": -1 } },
{ "returnOriginal": false },
function(err,doc) {
}
);
With mongoose the argument to return the modified document is different:
MyModel.findOneAndUpdate(
{ "array.0": "bird" },
{ "$pop": { "array": -1 } },
{ "new": true },
function(err,doc) {
}
);
But neither are of much use if the array position of the "first" item to remove is not known.
For the general approach here you need "two" updates, being one to match the first item and replace it with something unique to be removed, and the second to actually remove that modified item.
This is a lot more simple if applying simple updates and not asking for the returned document, and can also be done in bulk across documents. It also helps to use something like async.series in order to avoid nesting your calls:
async.series(
[
function(callback) {
collection.update(
{ "array": "bird" },
{ "$unset": { "array.$": "" } },
{ "multi": true }
callback
);
},
function(callback) {
collection.update(
{ "array": null },
{ "$pull": { "array": null } },
{ "multi": true }
callback
);
}
],
function(err) {
// comes here when finished or on error
}
);
So using the $unset here with the positional $ operator allows the "first" item to be changed to null. Then the subsequent query with $pull just removes any null entry from the array.
That is how you remove the "first" occurance of a value safely from an array. To determine whether that array contains more than one value that is the same though is another question.
It's worth noting that whilst the other answer here is indeed correct that the general approach here would be to $unset the matched array element in order to create a null value and then $pull just the null values from the array, there are better ways to implement this in modern MongoDB versions.
Using bulkWrite()
As an alternate case to submitting two operations to update in sequence as separate requests, modern MongoDB release support bulk operations via the recommended bulkWrite() method which allows those multiple updates to be submitted as a single request with a single response:
collection.bulkWrite(
[
{ "updateOne": {
"filter": { "array": "bird" },
"update": {
"$unset": { "array.$": "" }
}
}},
{ "updateOne": {
"filter": { "array": null },
"update": {
"$pull": { "array": null }
}
}}
]
);
Does the same thing as the answer showing that as two requests, but this time it's just one. This can save a lot of overhead in server communication, so it's generally the better approach.
Using Aggregation Expressions
With the release of MongoDB 4.2, aggregation expressions are now allowed in the various "update" operations of MongoDB. This is a single pipeline stage of either $addFields, $set ( which is an alias of $addFields meant to make these "update" statements read more logically ), $project or $replaceRoot and it's own alias $replaceWith. The $redact pipeline stage also applies here to some degree. Basically any pipeline stage which returns a "reshaped" document is allowed.
collection.updateOne(
{ "array": "horse" },
[
{ "$set": {
"array": {
"$concatArrays": [
{ "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] },
{ "$slice": [
"$array",
{ "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] },
{ "$size": "$array" }
]}
]
}
}}
]
);
In this case the manipulation used is to implement the $slice and $indexOfArray operators to essentially piece together a new array which "skips" over the first matched array element. Theses pieces are joined via the $concatArrays operator, returning a new array absent of the first matched element.
This is now probably more effective since the operation which is still a single request is now also a single operation and would incur a little less server overhead.
Of course the only catch is that this is not supported in any release of MongoDB prior to 4.2. The bulkWrite() on the other hand may be a newer API implementation, but the actual underlying calls to the server would apply back to MongoDB 2.6 implementing actual "Bulk API" calls, and even regresses back to earlier versions by the way all core drivers actually implement this method.
Demonstration
As a demonstration, here is a listing of both approaches:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true, useUnifiedTopology: true };
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useCreateIndex', true);
mongoose.set('useFindAndModify', false);
const arrayTestSchema = new Schema({
array: [String]
});
const ArrayTest = mongoose.model('ArrayTest', arrayTestSchema);
const array = ["bird", "tiger", "horse", "bird", "horse"];
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.values(conn.models).map(m => m.deleteMany())
);
await ArrayTest.create({ array });
// Use bulkWrite update
await ArrayTest.bulkWrite(
[
{ "updateOne": {
"filter": { "array": "bird" },
"update": {
"$unset": { "array.$": "" }
}
}},
{ "updateOne": {
"filter": { "array": null },
"update": {
"$pull": { "array": null }
}
}}
]
);
log({ bulkWriteResult: (await ArrayTest.findOne()) });
// Use agggregation expression
await ArrayTest.collection.updateOne(
{ "array": "horse" },
[
{ "$set": {
"array": {
"$concatArrays": [
{ "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] },
{ "$slice": [
"$array",
{ "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] },
{ "$size": "$array" }
]}
]
}
}}
]
);
log({ aggregateWriteResult: (await ArrayTest.findOne()) });
} catch (e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
And the output:
Mongoose: arraytests.deleteMany({}, {})
Mongoose: arraytests.insertOne({ array: [ 'bird', 'tiger', 'horse', 'bird', 'horse' ], _id: ObjectId("5d8f509114b61a30519e81ab"), __v: 0 }, { session: null })
Mongoose: arraytests.bulkWrite([ { updateOne: { filter: { array: 'bird' }, update: { '$unset': { 'array.$': '' } } } }, { updateOne: { filter: { array: null }, update: { '$pull': { array: null } } } } ], {})
Mongoose: arraytests.findOne({}, { projection: {} })
{
"bulkWriteResult": {
"array": [
"tiger",
"horse",
"bird",
"horse"
],
"_id": "5d8f509114b61a30519e81ab",
"__v": 0
}
}
Mongoose: arraytests.updateOne({ array: 'horse' }, [ { '$set': { array: { '$concatArrays': [ { '$slice': [ '$array', 0, { '$indexOfArray': [ '$array', 'horse' ] } ] }, { '$slice': [ '$array', { '$add': [ { '$indexOfArray': [ '$array', 'horse' ] }, 1 ] }, { '$size': '$array' } ] } ] } } } ])
Mongoose: arraytests.findOne({}, { projection: {} })
{
"aggregateWriteResult": {
"array": [
"tiger",
"bird",
"horse"
],
"_id": "5d8f509114b61a30519e81ab",
"__v": 0
}
}
NOTE : The example listing is using mongoose, partly because it was referenced in the other answer given and partly to also demonstrate an important point with the aggregate syntax example. Note the code uses ArrayTest.collection.updateOne() since at the present release of Mongoose ( 5.7.1 at time of writing ) the aggregation pipeline syntax to such updates is being removed by the standard mongoose Model methods.
As such the .collection accessor can be used in order to get the underlying Collection object from the core MongoDB Node driver. This would be required until a fix is made to mongoose which allows this expression to be included.
As mentioned in this Jira this feature will never exist properly.
The approach I recommend using would be via the aggregation pipeline update syntax as proposed in a different answer, however that answer has some edge cases where it fails - for example if the element does not exist in the array, here is a working version for all edge cases.
ArrayTest.updateOne({},
[
{
"$set": {
"array": {
"$concatArrays": [
{
$cond: [
{
$gt: [
{
"$indexOfArray": [
"$array",
"horse"
]
},
0
]
},
{
"$slice": [
"$array",
0,
{
"$indexOfArray": [
"$array",
"horse"
]
}
]
},
[]
]
},
{
"$slice": [
"$array",
{
"$add": [
{
"$indexOfArray": [
"$array",
"horse"
]
},
1
]
},
{
"$size": "$array"
}
]
}
]
}
}
}
])
Mongo Playground