How I can compile $cond with $ne on mongoose aggregate - mongodb

This is the sample about my data:
[{
_id: ObjectId('5e982040227ddfb12bf43e39'),
classId: 'class-1',
state: 'active',
logs: []
},
{
_id: ObjectId('5e982040227ddfb12bf43e38'),
classId: 'class-1',
state: 'unactive',
logs: []
},
{
_id: ObjectId('5e982040227ddfb12bf43e40'),
classId: 'class-1',
state: 'graduated',
logs: []
}]
I want to change state for all students in class class-1 and state not equal unactive.
This is my query:
db.getCollection('student').aggregate([{
$addFields: {
state: {
$cond: [{ classId: 'class-1', $ne: ['$state', 'unactive']}, 'unactive', '$state']
},
logs: {
$cond: [{
{ classId: 'class-1', $ne: ['$state', 'unactive']},
{
$concatArrays: ['$logs', [{ from: '$state', to: 'unactive', at: new Date() }],
'$logs'
]}
}]
}
}
}, {
$out: 'student'
}]);
But it's throw an error: FieldPath field names may not start with '$'.
Here is document I'm following: https://docs.mongodb.com/manual/reference/operator/aggregation/cond/
If I remove $ne condition and keep only filter by classId, It's working well, but I don't want to unactive users who guys unactived.
Here my expected resuult:
[{
_id: ObjectId('5e982040227ddfb12bf43e39'),
classId: 'class-1',
state: 'unactive',
logs: [{ from: 'active', to: 'unactive', at: ... }]
},
{
_id: ObjectId('5e982040227ddfb12bf43e38'),
classId: 'class-1',
state: 'unactive',
logs: []
},
{
_id: ObjectId('5e982040227ddfb12bf43e40'),
classId: 'class-1',
state: 'unactive',
logs: [{ from: 'graduated', to: 'unactive', at: ...}]
}]
Please give me an idea if you know the way to resolve this problem

I believe you are trying to update. If that's not the case then, let me know.
You are using the $cond operator in a wrong way. The first argument in it, expects some logical condition like $and, $or, or any combination of logical operators.
The below query will be helpful:
db.student.aggregate([
{ // Unwind the 'logs' array field, along with preserving empty arrays.
$unwind: {
path: "$logs",
preserveNullAndEmptyArrays: true
}
},
{ // Group the documents by '_id' field.
$group: {
_id: "$_id",
classId: {
$first: "$classId"
},
oldState: {
$first: "$state"
},
logs: { // Conditionally push the object.
$push: {
$cond: [
{
$and: [
{
$eq: [
"$classId",
"class-1"
]
},
{
$ne: [
"$state",
"unactive"
]
}
]
},
{
"from": "$state",
"to": "unactive",
"at": "20200522"
},
"$$REMOVE" // If the above condition results false, then don't push.
]
}
}
}
},
{ // Project the required fields.
$project: {
classId: 1,
logs: 1,
state: { // conditionally set the 'state' field.
$cond: [
{
$and: [
{
$eq: [
"$classId",
"class-1"
]
},
{
$ne: [
"$oldState",
"unactive"
]
}
]
},
"unactive",
"$oldState"
]
}
}
},
{ // overwrite the output of this aggregation to 'student' collection.
$out : 'student'
}
])

Related

How do I find multiple mongo documents using queries but always start with the first query?

I need the first part of $or (or equivalent query) to be resolved first and to make sure that the first query is always part of the result.
Must use query, not aggregation.
[
{ "docId": "x1" },
{ "docId": "x2" },
{ "docId": "x3" },
{ "docId": "x4" },
{ "docId": "x5" },
...
{ "docId": "xn" },
]
Query:
{
'$or': [ { docId: 'x434' },{} ],
}
I need x434 to be part of the query result, regardless of all other results.
Expected result:
[
{ docId: 'x434' },
{ docId: 'x12' },
{ docId: 'x1' },
...
]
Return:
[
{ docId: 'xn' },
{ docId: 'xn' },
{ docId: 'xn' },
...
]
Results by x434 is not always returned
I tried $or and $and queries but nothing worked. I tried regex too
{
'$or': [ { docId: 'x434' },{} ],
}
So the solution can only be an aggregation:
$match: {
'$or': [ { docId: 'x434' },{} ],
},
$addFields: {
order: {
$cond: [
{
$in: [
"$docId",
['x434']
]
},
0,
1
]
}
},
$sort: {
order: 1
},
$limit: 20
Result:
{ docId: 'x434' },
{ docId: 'x12' },
{ docId: 'x1' },
...
]```
A straightforward solution can use $facet:
db.collection.aggregate([
{$facet: {
allOther: [{$match: {docId: {$ne: "x434"}}}, {$limit: 19}],
wanted: [{$match: {docId: "x434"}}]
}},
{$project: {data: {$concatArrays: ["$wanted", "$allOther"]}}},
{$unwind: "$data"},
{$replaceRoot: {newRoot: "$data"}}
])
See how it works on the playground example
You can use $unionWith. It's behaviour is similar to UNION ALL in SQL so you can persist x434 at the start. Remember to exclude x434 in the $unionWith pipeline to avoid duplicate if needed
db.collection.aggregate([
{
$match: {
"docId": "x434"
}
},
{
"$unionWith": {
"coll": "collection",
"pipeline": [
// exclude x434 to avoid duplicate
{
$match: {
"docId": {
$ne: "x434"
}
}
}// put your other queries here
]
}
}
])
Mongo Playground

Update document where all items in an array has the same value

I am making a migration script where I need to update a status value on all documents where the items in an array has the same value
Data structure
[
{
_id: 'asdasd',
status: 'active',
approvers: [
{status: 'approved'},
{status: 'approved'}
]
},
{
_id: 'fghfgh',
status: 'active',
approvers: [
{status: 'approved'},
{status: 'awaiting_approval'},
]
}
]
So, in this case in want to update all documents to have status 'completed' where all approvers has status 'approved'
I haven't found a good way how to create a filter like this.
What I've currently tried to do is:
db.getCollection("assignmentRequest").aggregate([
{
$match: {
'approver.0.status': {$exists:true}
}
},
{
$project: {
_id: 0,
approver: 1,
status: 1,
noOfApprovers: { $cond: { if: { $isArray: "$approvers" }, then: { $size: "$approvers" }, else: 0}},
noOfApproversThatHasApproved: {
$size: {$filter: {
'input': '$approvers',
'as': 'approver',
'cond': {
'$and': [
{
$eq: ['$$approver.status', 'approved']
}
]
}
}}
},
},
},
{
$match: {$expr: { $eq: ["$noOfApprovers", "$noOfApproversThatHasApproved"] } }
},
{
$set: {'status': 'completed'}
},
{
$project: {_id:1, status:1 }
},
{
$merge: {into: 'assignmentRequests_copy', on: '_id', whenMatched: "replace" }
}])
The filter works, but I can't get the status to update. I'm sure there are plenty of things worng with my query, but I feel like I am going down the wrong path and that there must be a simpler way of achieving this. Any pointers or help would be highly appreciated.
Edit:
After the update I want the documents to look like this:
{
_id: 'asdasd',
status: 'completed',
approvers: [
{status: 'approved'},
{status: 'approved'}
]
},
{
_id: 'fghfgh',
status: 'active',
approvers: [
{status: 'approved'},
{status: 'awaiting_approval'},
]
}
]
Here's one way you could do it by using a pipeline in the update.
db.collection.update({
"approvers.status": "approved"
},
[
{
"$set": {
"status": {
"$cond": [
{
"$reduce": {
"input": "$approvers",
"initialValue": true,
"in": {
"$and": [
"$$value",
{"$eq": ["$$this.status", "approved"]}
]
}
}
},
"completed",
"$status"
]
}
}
}
],
{"multi": true}
)
Try it on mongoplayground.net.

MongoDb query to exclude omission of rows based on criteria

In below example, looking for new partner suggestions for user abc. abc has already sent a request to 123 so that can be ignored. rrr has sent request to abc but rrr is in the fromUser field so rrr is still a valid row to be shown as suggestion to abc
I have two collections:
User collection
[
{
_id: "abc",
name: "abc",
group: 1
},
{
_id: "xyz",
name: "xyyy",
group: 1
},
{
_id: "123",
name: "yyy",
group: 1
},
{
_id: "rrr",
name: "tttt",
group: 1
},
{
_id: "eee",
name: "uuu",
group: 1
}
]
Partnership collection (if users have already partnered)
[
{
_id: "abc_123",
fromUser: "abc",
toUser: "123"
},
{
_id: "rrr_abc",
fromUser: "rrr",
toUser: "abc"
},
{
_id: "xyz_rrr",
fromUser: "xyz",
toUser: "rrr"
}
]
My query below excludes the user rrr but it should not because its not listed in toUser field in the partnership collection corresponding to the user abc.
How to modify this query to include user rrr in this case?
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
let: {
userId: "$_id"
},
as: "prob",
pipeline: [
{
$set: {
users: [
"$fromUser",
"$toUser"
],
u: "$$userId"
}
},
{
$match: {
$expr: {
$and: [
{
$in: [
"$$userId",
"$users"
]
},
{
$in: [
"abc",
"$users"
]
}
]
}
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$sample: {
size: 1
}
},
{
$unset: "prob"
}
])
https://mongoplayground.net/p/utGMeHFRGmt
Your current query does not allow creating an existing connection regardless of the connection direction. If the order of the connection is important use:
db.users.aggregate([
{$match: {
group: 1,
_id: {$ne: "abc"}
}
},
{$lookup: {
from: "partnership",
let: { userId: {$concat: ["abc", "_", "$_id"]}},
as: "prob",
pipeline: [{$match: {$expr: {$eq: ["$_id", "$$userId"]}}}]
}
},
{$match: {"prob.0": {$exists: false}}},
{$sample: {size: 1}},
{$unset: "prob"}
])
See how it works on the playground example
For MongoDB 5 and later, I'd propose the following aggregation pipeline:
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
as: "prob",
localField: "_id",
foreignField: "toUser",
pipeline: [
{
$match: {
fromUser: "abc",
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$unset: "prob"
}
])
The following documents are returned (full result without the $sample stage):
[
{
"_id": "eee",
"group": 1,
"name": "uuu"
},
{
"_id": "rrr",
"group": 1,
"name": "tttt"
},
{
"_id": "xyz",
"group": 1,
"name": "xyyy"
}
]
The main difference is that the lookup connects the collections by the toUser field (see localField, foreignField) and uses a minimal pipeline to restrict the results further to only retrieve the requests from the current user document to "abc".
See this playground to test.
When using MongoDB < 5, you cannot use localField and foreignField to run the pipeline only on a subset of the documents in the * from*
collection. To overcome this, you can use this aggregation pipeline:
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
as: "prob",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$fromUser",
"abc"
]
},
{
$eq: [
"$toUser",
"$$userId"
]
}
]
}
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$unset: "prob"
}
])
The results are the same as for the upper pipeline.
See this playground to test.
For another, another way, this query starts from the partnership collection, finds which users to exclude, and then does a "$lookup" for everybody else. The remainder is just output formatting, although it looks like you may want to add a "$sample" stage at the end.
db.partnership.aggregate([
{
"$match": {
"fromUser": "abc"
}
},
{
"$group": {
"_id": null,
"exclude": {"$push": "$toUser" }
}
},
{
"$lookup": {
"from": "users",
"let": {
"exclude": {"$concatArrays": [["abc"], "$exclude"]
}
},
"pipeline": [
{
"$match": {
"$expr": {
"$not": {"$in": ["$_id", "$$exclude"]}
}
}
}
],
"as": "output"
}
},
{
"$project": {
"_id": 0,
"output": 1
}
},
{"$unwind": "$output"},
{"$replaceWith": "$output"}
])
Try it on mongoplayground.net.

Referencing root _id in aggregate lookup match expression not working

This is my first experience using aggregate pipeline. I'm not able to get a "$match" expression to work inside the pipeline. If I remove the "_id" match, I get every document in the collection past the start date, but once I add the $eq expression, it returns empty.
I read a lot of other examples and tried many different ways, and this seems like it is correct. But the result is empty.
Any suggestions?
let now = new Date()
let doc = await Team.aggregate([
{ $match: { created_by: mongoose.Types.ObjectId(req.params.user_oid)} },
{ $sort: { create_date: 1 } },
{ $lookup: {
from: 'events',
let: { "team_oid": "$team_oid" },
pipeline: [
{ $addFields: { "team_oid" : { "$toObjectId": "$team_oid" }}},
{ $match: {
$expr: {
$and: [
{ $gt: [ "$start", now ] },
{ $eq: [ "$_id", "$$team_oid" ] }
]
},
}
},
{
$sort: { start: 1 }
},
{
$limit: 1
}
],
as: 'events',
}},
{
$group: {
_id: "$_id",
team_name: { $first: "$team_name" },
status: { $first: "$status" },
invited: { $first: "$invited" },
uninvited: { $first: "$uninvited" },
events: { $first: "$events.action" },
dates: { $first: "$events.start" } ,
team_oid: { $first: "$events.team_oid" }
}
}])
Example Docs (added by request)
Events:
_id:ObjectId("60350837c57b3a15a414d265")
invitees:null
accepted:null
sequence:7
team_oid:ObjectId("60350837c57b3a15a414d263")
type:"Calendar Invite"
action:"Huddle"
status:"Questions Issued"
title:"Huddle"
body:"This is a Huddle; you should receive new questions 5 days befor..."
creator_oid:ObjectId("5ff9e50a206b1924dccd691e")
start:2021-02-26T07:00:59.999+00:00
end:2021-02-26T07:30:59.999+00:00
__v:0
Team:
_id:ObjectId("60350837c57b3a15a414d263")
weekly_schedule:1
status:"Live"
huddle_number:2
reminders:2
active:true
created_by:ObjectId("5ff9e50a206b1924dccd691e")
team_name:"tESTI"
create_date:2021-02-23T13:50:47.172+00:00
__v:0
This is just a guess since you don't have schema in your question. But it looks like your have some of your _ids mixed up. Where you are currently trying to $match events whose _id is equal to a team_oid. Rather than the event's team_oid field being equal to the current 'team' _id.
I'm pretty confident this will produce the correct output. If you post any schema or sample docs I will edit it.
https://mongoplayground.net/p/5i1w2Ii7KCR
let now = new Date()
let doc = await Team.aggregate([
{ $match: { created_by: mongoose.Types.ObjectId(req.params.user_oid)} },
{ $sort: { create_date: 1 } },
{ $lookup: {
from: 'events',
// Set tea_oid as the current team _id
let: { "team_oid": "$_id" },
pipeline: [
{ $match: {
$expr: {
$and: [
{ $gt: [ "$start", now ] },
// Match events whose 'team_oid' field matches the 'team' _id set above
{ $eq: [ "$team_oid", "$$team_oid" ] }
]
},
}
},
{
$sort: { start: 1 }
},
{
$limit: 1
}
],
as: 'events',
}},
{
$group: {
_id: "$_id",
team_name: { $first: "$team_name" },
status: { $first: "$status" },
invited: { $first: "$invited" },
uninvited: { $first: "$uninvited" },
events: { $first: "$events.action" },
dates: { $first: "$events.start" } ,
team_oid: { $first: "$events.team_oid" }
}
}])

Mongodb - aggregation $push if conditional

I am trying to aggregate a batch of documents. There are two fields in the documents I would like to $push. However, lets say they are "_id" and "A" fields, I only want $push "_id" and "A" if "A" is $gt 0.
I tried two approaches.
First one.
db.collection.aggregate([{
"$group":{
"field": {
"$push": {
"$cond":[
{"$gt":["$A", 0]},
{"id": "$_id", "A":"$A"},
null
]
}
},
"secondField":{"$push":"$B"}
}])
But this will push a null value to "field" and I don't want it.
Second one.
db.collection.aggregate([{
"$group":
"field": {
"$cond":[
{"$gt",["$A", 0]},
{"$push": {"id":"$_id", "A":"$A"}},
null
]
},
"secondField":{"$push":"$B"}
}])
The second one simply doesn't work...
Is there a way to skip the $push in else case?
ADDED:
Expected documents:
{
"_id":objectid(1),
"A":2,
"B":"One"
},
{
"_id":objectid(2),
"A":3,
"B":"Two"
},
{
"_id":objectid(3),
"B":"Three"
}
Expected Output:
{
"field":[
{
"A":"2",
"_id":objectid(1)
},
{
"A":"3",
"_id":objectid(2)
},
],
"secondField":["One", "Two", "Three"]
}
You can use "$$REMOVE":
This system variable was added in version 3.6 (mongodb docs)
db.collection.aggregate([{
$group:{
field: {
$push: {
$cond:[
{ $gt: ["$A", 0] },
{ id: "$_id", A:"$A" },
"$$REMOVE"
]
}
},
secondField:{ $push: "$B" }
}
])
In this way you don't have to filter nulls.
This is my answer to the question after reading the post suggested by #Veeram
db.collection.aggregate([{
"$group":{
"field": {
"$push": {
"$cond":[
{"$gt":["$A", 0]},
{"id": "$_id", "A":"$A"},
null
]
}
},
"secondField":{"$push":"$B"}
},
{
"$project": {
"A":{"$setDifference":["$A", [null]]},
"B":"$B"
}
}])
One more option is to use $filter operator:
db.collection.aggregate([
{
$group : {
_id: null,
field: { $push: { id: "$_id", A : "$A"}},
secondField:{ $push: "$B" }
}
},
{
$project: {
field: {
$filter: {
input: "$field",
as: "item",
cond: { $gt: [ "$$item.A", 0 ] }
}
},
secondField: "$secondField"
}
}])
On first step you combine your array and filter them on second step
$group: {
_id: '$_id',
tasks: {
$addToSet: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.id', ''],
},
'',
],
},
then: '$$REMOVE',
else: {
id: '$tasks.id',
description: '$tasks.description',
assignee: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.assignee._id', ''],
},
'',
],
},
then: undefined,
else: {
id: '$tasks.assignee._id',
name: '$tasks.assignee.name',
thumbnail: '$tasks.assignee.thumbnail',
status: '$tasks.assignee.status',
},
},
},
},
},
},
},
}