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"
}
})
Related
I have made several efforts to select a single specific document that contains the minimum value from the database.
let Lowestdate = await BTCMongo.aggregate(
[
// { "$sort": { "name": 1,
{
$match : { "createdAt" : { $gte: new Date(last),$lte: new Date(NEW) } } },
{
$group:
{
_id:null,
minFee_doc:{$min: "$$ROOT"},
minFee: { $min:{$toDouble:"$one"}},
firstFee: { $first: "$one" },
lastFee: { $last: "$one" },
maxFee: { $max: {$toDouble:"$one"}},
}
},
]
).then(result => {}):
with minFee_doc:{$min: "$$ROOT"}, I have been trying to return the document containing the minimum $min but it keeps returning document containing $first
How do i select the document with minimum value?
Note : i will like to return the whole document including the "CreatedAt" "UpdatedAt", and _id. of the document containing the minimum value
Expected Result should look like:
{
"minFee_doc": {
"_id": "61e84c9f622642463640e05c",
"createdAt": "2022-01-19T17:38:39.034Z",
"updatedAt": "2022-04-24T14:48:38.100Z",
"__v": 0,
"one": 2
},
"minFee": 2,
"firstFee": 3,
"lastFee": 5,
"maxFee": 6
}
Edit: also to provide a single document not multiple
$push all docs in $group then $set the array with $filter
db.collection.aggregate([
{
$match: {}
},
{
$group: {
_id: null,
minFee_doc: { $push: "$$ROOT" },
minFee: { $min: { $toDouble: "$one" } },
firstFee: { $first: "$one" },
lastFee: { $last: "$one" },
maxFee: { $max: { $toDouble: "$one" } }
}
},
{
$set: {
minFee_doc: {
$filter: {
input: "$minFee_doc",
as: "m",
cond: { "$eq": [ "$$m.one", "$minFee" ] }
}
}
}
}
])
mongoplayground
I have an app with MongoDB (Mongoose) in NodeJs.
In a collection I have this type of documents, defined by weeks:
{
"_id":
{"$oid":"617f3f51f883fab2de3e7260"},
"endDate":{"$date":"2021-11-07T23:59:59.000Z"},
"startDate":{"$date":"2021-11-01T00:00:00.000Z"},
"wastes":[
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":780},
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":1140},
{"timestamp":{"$date":"2021-11-01T03:00:58.000Z"},"duration":540},
{"timestamp":{"$date":"2021-11-01T07:00:58.000Z"},"duration":540},
{"timestamp":{"$date":"2021-11-01T09:00:58.000Z"},"duration":960},
{"timestamp":{"$date":"2021-11-01T09:00:58.000Z"},"duration":1140},
{"timestamp":{"$date":"2021-11-01T15:00:58.000Z"},"duration":180},
{"timestamp":{"$date":"2021-11-01T15:00:58.000Z"},"duration":540}
...
]}
I have a function that finds wastes with the same timestamp, for example "2021-11-01T01:00:58.000Z", gives the longest duration for this timestamp.
I want to delete all entries with that timestamp:
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":780},
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":1140}
And insert only the one with the highest duration:
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":1140}
I'm using updateOne with $pull and $push, but it doesn't work.
let query = {
startDate: new Date(startDayWeek),
};
let deleteProjection = {
$pull: {
wastes: { timestamp: new Date(timestampDeleteInsertion) },
},
};
let insertProjection = {
$push: { wastes: insertRegisterForTimestamp },
};
//Delete
await coleccion.updateOne(query, deleteProjection);
//Insertion
await coleccion.updateOne(query, insertProjection);
I have also tried with {upsert: false}, {multi: true}.
If I use the same commands in the MongoDB Compass shell, it works without problems:
//Delete
db.coleccion.updateOne({startDate: ISODate('2021-11-01T00:00:00')}, {$pull: {'wastes': {timestamp: ISODate('2021-11-01T01:00:58.000Z')}}})
//Insertion
db.coleccion.updateOne({startDate: ISODate('2021-11-01T00:00:00')}, {$push: {'wastes': {'timestamp':ISODate('2021-11-01T01:00:58.000Z'), 'duration': 1140}}})
You can achieve expected behaviour with Updates with Aggregation Pipeline
The aggregation will consists of 3 steps:
find out the max duration using $reduce; stored the result into a field
$filter the wastes array by keeping only elements not equal to the selected timestamp or the duration is not the max duration
$unset the helper field created in step 1
db.collection.update({},
[
{
$addFields: {
maxDuration: {
"$reduce": {
"input": "$wastes",
"initialValue": null,
"in": {
"$cond": {
"if": {
$and: [
{
$eq: [
"$$this.timestamp",
{
"$date": "2021-11-01T01:00:58.000Z"
}
]
},
{
$gt: [
"$$this.duration",
"$$value"
]
}
]
},
"then": "$$this.duration",
"else": "$$value"
}
}
}
}
}
},
{
$set: {
wastes: {
$filter: {
input: "$wastes",
as: "w",
cond: {
$or: [
{
$ne: [
"$$w.timestamp",
{
"$date": "2021-11-01T01:00:58.000Z"
}
]
},
{
$eq: [
"$$w.duration",
"$maxDuration"
]
}
]
}
}
}
}
},
{
"$unset": "maxDuration"
}
])
Here is the Mongo playground for your reference.
I have the same issue with the updateOne and pull command, if use the updateOne with push, it works.
In the mongo shell or in the compass, both situations (push/pull) works, but with mongoose, it finds the criteria but don't update/modify.
Result
{
"acknowledged" : true,
"matchedCount" : 1.0,
"modifiedCount" : 0.0
}
hello I have this function where I want to calculate the number of orders for each status in one array, the code is
let statusEnum = ["pending", "canceled", "completed"];
let userOrders = await Orders.aggregate([
{
$match: {
$or: [
{ senderId: new mongoose.Types.ObjectId(req.user._id) },
{ driverId: new mongoose.Types.ObjectId(req.user._id) },
{ reciverId: new mongoose.Types.ObjectId(req.user._id) },
],
},
},
{
$group: {
_id: null,
totalOrders: { $sum: 1 },
totalPendingOrders: "??", //I want to determine this for each order status
totalCompletedOrders: "??",
totalCanceledOrders: "??",
},
},
]);
so I could add add a $match and use {status : "pending"} but this will filter only the pending orders, I could also map the status enum and replace each element instead of the "pending" above and then push each iteration in another array , but that just seems so messy, is there any other way to calculate total for each order status with using only one aggregate?
thanks
You can use group as you used, but with condition
db.collection.aggregate([
{
$group: {
_id: null,
totalPendingOrders: {
$sum: { $cond: [ { $eq: [ "$status", "pending" ] }, 1, 0 ] }
},
totalCompletedOrders: {
$sum: { $cond: [ { $eq: [ "$status", "completed" ] }, 1, 0 ] }
},
totalCanceledOrders: {
$sum: { $cond: [ { $eq: [ "$status", "canceled" ] }, 1, 0 ] }
}
}
}
])
Working Mongo playground
Please i am new to node js and MongoDB.
When i want to retrieve a post by id, i want to be able to retrieve the previous post and next post also.
this is my post, it only retrieves the current post by id.
Post.findById(req.params.postId)
.then((existingpost) => {
console.log(Post.find(req.params.postId))
if (existingpost) {
res.send(existingpost);
}
return res.status(404).send({
message: "Post does not exist with id " + req.params.postId,
});
})
.catch((err) => {
if (err.kind === "ObjectId") {
return res.status(404).send({
message: "Post does not exist with id " + req.params.postId,
});
}
return res.status(500).send({
message:
"Some error occurred while retrieving the post with postId " +
req.params.postId,
});
});
};
I currently receive the object with the id like this which is fine.
{
"_id": "6009f3e294d8a033402a76e7",
"title": "Covid 19 in Italy",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
}
But i will love to receive the object of the current id, the previous object and the next object.
something like this.
[{
"_id": "3230g5e382d8a033402a76e7",
"title": "Effect of Covid on the Economy",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
},
{
"_id": "6009f3e294d8a033402a76e7",
"title": "Covid 19 in Italy",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
},
{
"_id": "4567hye294d8a033402a76e7",
"title": "Life after Covid",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
}]
Since its a UUID, this approach might help you..
$sort to sort the documents by asc
$group and $unwind to get the index
$facet to categorize the incoming data into current and allDocs
We know current is only one object, so we do $unwind to deconstruct the array
We already know the index, so we use $filter to get prev, current and next using index
$unwind to deconstruct the array
$replaceRoot to make the objects to the root
Here is the script
db.collection.aggregate([
$sort: { createdAt: 1 } },
{
$group: {
_id: null,
data: { $push: "$$ROOT"}
}
},
{ $unwind: { path: "$data", includeArrayIndex: "index" } },
{
$facet: {
current: [
{ $match: { "data._id": "3230g5e382d8a033402a76e7" } }
],
allDocs: [
{ $match: {} }
]
}
},
{
$unwind: "$current"
},
{
$project: {
docs: {
$filter: {
input: "$allDocs",
cond: {
$or: [
{ $eq: [ "$$this.index", { $subtract: [ "$current.index", 1 ] } ] },
{ $eq: [ "$$this.index", "$current.index" ] },
{ $eq: [ "$$this.index", { $add: [ "$current.index", 1 ] } ] }
]
}
}
}
}
},
{ "$unwind": "$docs" },
{ "$replaceRoot": { "newRoot": "$docs.data" } }
])
Working Mongo playground
There are many ways to do this, this is one of the way. If you feel you have a lot of document, then try to avoid $unwind which is expensive, in that case you can try using createdDate instead of index
I am not sure is there any straight way to do this, you can try aggregation,
Using UUID and CreatedAt:
$facet to get all documents in all after sorting in ascending order by createdAt
$let to define vars states with start and total documents,
$cond check condition if index of input uuid is zero then return start: 0 and total: 2 documents we have to slice from all array, else get current index and subtract minus 1 and total: 3
in to return slice documents on the base of start and total
Post.aggregate([
{ $facet: { all: [{ $sort: { createdAt: 1 } }] } },
{
$project: {
result: {
$let: {
vars: {
states: {
$cond: [
{ $eq: [{ $indexOfArray: ["$all._id", req.params.postId] }, 0] },
{ start: 0, total: 2 },
{
start: {
$subtract: [{ $indexOfArray: ["$all._id", req.params.postId] }, 1]
},
total: 3
}
]
}
},
in: { $slice: ["$all", "$$states.start", "$$states.total"] }
}
}
}
}
])
Playground
Using ObjectID:
convert your string input id req.params.postId to object id using mongoose.Types.ObjectId
$facet to separate result,
first, $match to get current and next documents, $sort _id in descending order, $limit 2
second, $match to get previous document, $sort _id in descending order, $limit 1
$project to get result after concat both array first and second using $concatArrays
req.params.postId = mongoose.Types.ObjectId(req.params.postId);
Post.aggregate([
{
$facet: {
first: [
{ $match: { _id: { $gte: req.params.postId } } },
{ $sort: { _id: 1 } },
{ $limit: 2 }
],
second: [
{ $match: { _id: { $lt: req.params.postId } } },
{ $sort: { _id: -1 } },
{ $limit: 1 }
]
}
},
{ $project: { result: { $concatArrays: ["$first", "$second"] } } }
])
Playground
I have the following metrics collection:
{
name: "Hello",
values: [
{
value: 2629,
date: "2016-10-28T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee8"
},
{
value: 1568,
date: "2016-10-29T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee7"
},
{
value: 1547,
date: "2016-10-30T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee6"
},
{
value: 1497,
date: "2016-10-31T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee5"
},
{
value: 3031,
date: "2016-11-01T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee4"
},
{
value: 2559,
date: "2016-11-02T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee3"
},
{
value: 2341,
date: "2016-11-03T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee2"
},
{
value: 2188,
date: "2016-11-04T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee1"
},
{
value: 3280,
date: "2016-11-05T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee0"
},
{
value: 4638,
date: "2016-11-06T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdedf"
}
]
},
.... more of the same
What I would like to get is all the values between a custom date range.
I've tried the following query but I still get the entire values array returned:
{
name: "Hello",
values: {
$elemMatch: {
date: {
$lt: "2016-11-03T07:00:00.000Z",
$gt: "2016-10-28T07:00:00.000Z"
}
}
}
}
Maybe I saved my dates in a wrong format ? Any help would be greatly appreciated.
You can run an aggregation pipeline that uses the $filter operator on the values array. The following mongo shell query demonstrates this:
var start = new Date("2016-10-28T07:00:00.000Z"),
end = new Date("2016-11-03T07:00:00.000Z");
db.metrics.aggregate([
{
"$match": {
"name": "Hello",
"values.date": { "$gt": start, "$lt": end }
}
},
{
"$project": {
"name": 1,
"values": {
"$filter": {
"input": "$values",
"as": "value",
"cond": {
"$and": [
{ "$gt": [ "$$value.date", start ] },
{ "$lt": [ "$$value.date", end ] }
]
}
}
}
}
}
])
Sample Output
/* 1 */
{
"_id" : ObjectId("5845453145fda1298fa50db9"),
"name" : "Hello",
"values" : [
{
"value" : 1568,
"date" : ISODate("2016-10-29T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee7")
},
{
"value" : 1547,
"date" : ISODate("2016-10-30T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee6")
},
{
"value" : 1497,
"date" : ISODate("2016-10-31T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee5")
},
{
"value" : 3031,
"date" : ISODate("2016-11-01T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee4")
},
{
"value" : 2559,
"date" : ISODate("2016-11-02T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee3")
}
]
}
For MongoDB 3.0, the following workaround applies:
var start = new Date("2016-10-28T07:00:00.000Z"),
end = new Date("2016-11-03T07:00:00.000Z");
db.metrics.aggregate([
{
"$match": {
"name": "Hello",
"values.date": { "$gt": start, "$lt": end }
}
},
{
"$project": {
"name": 1,
"values": {
"$setDifference": [
{
"$map": {
"input": "$values",
"as": "value",
"in": {
"$cond": [
{
"$and": [
{ "$gt": [ "$$value.date", start ] },
{ "$lt": [ "$$value.date", end ] }
]
},
"$$value",
false
]
}
}
},
[false]
]
}
}
}
])
The Aggregation Framework in MongoDB 2.2+ provides an alternative to Map/Reduce. The $unwind operator can be used to separate your values array into a stream of documents that can be matched:
db.tmp.aggregate(
// Start with a $match pipeline which can take advantage of an index and limit documents processed
{ $match : {
name: "Hello",
"values.date": {
$lt: "2016-11-03T07:00:00.000Z",
$gt: "2016-10-28T07:00:00.000Z" }
}},
{ $unwind : "$values" },
{ $match : {
name: "Hello",
"values.date": {
$lt: "2016-11-03T07:00:00.000Z",
$gt: "2016-10-28T07:00:00.000Z" }
}}
)
Sample output:
{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":1568,
"date":"2016-10-29T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee7"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":1547,
"date":"2016-10-30T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee6"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":1497,
"date":"2016-10-31T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee5"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":3031,
"date":"2016-11-01T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee4"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":2559,
"date":"2016-11-02T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee3"
}
}