Query userId for having one condition and NOT having other - mongodb

Can you please help me with this?
I have these documents:
{
userId: 123,
event: 'A',
properties: {
status: 'Sent'
}
}
{
userId: 123,
event: 'B',
properties: {
status: 'Opened'
}
}
{
userId: 123,
event: 'C',
properties: {
status: 'Clicked'
}
}
I need a query to match all userId == 'A', that also registered for event 'B', but has not done event 'C' with properties.status == "Clicked".
Here's what I did so far:
db.collection.aggregate([
{ $match: { "event": "A"} },
{
$lookup: {
from: "collection",
let: { "userId": "$userId" },
pipeline: [{
$match: {
$expr: { $eq: ["$userId", "$$userId"] }
}
}],
as: "events"
}
}
This gives me this unified output:
{
userId: 123,
event: "A",
properties: {
status: 'Sent'
},
events: [
{ event: 'A' /* and all the other fields from original document */ },
{ event: 'B' /* and all the other fields from original document */ },
{
event: 'C',
properties: {
status: 'Clicked'
}
},
]
I tried to do this next, but it didn't work:
{
$match: {
$expr: {
$not: {
$and: [
{ $eq: ["$events.event", "C"] },
{ $eq: ["$events.properties.status', 'Clicked'] }
]
}
}
}
}
My expectation is that this query does not brings me userId: 123 in this case, but it's on the results.
Can someone please help me with that?
Tks!

I ended up doing this by adding these steps after the lookup. Not sure if it's the best solution, but I couldn't achieve this in any other way I've tried.
{ $unwind: "$events"},
{
$project: {
"userId": 1, "events": "$events",
"exclude": {
$cond: { if: {
$and: [
{ $eq: ["$events.event", "C"] },
{ $eq: ['$events.properties.status', 'Clicked'] }
]
}, then: 1, else: 0 }
}
}
},
{
$group: {
_id: "$userId",
exclude: { $sum: "$exclude" }
}
},
{
$match: { "exclude": 0 }
}

Related

How to find and update a document in MongoDB

I am having a similar collection
db={
collectionA: [
{
"id": ObjectId("63b7c24c06ebe7a8fd11777b"),
"uniqueRefId": "UUID-2023-0001",
"products": [
{
"productIndex": 1,
"isProdApproved": false,
"productCategory": ObjectId("63b7c24c06ebe7a8fd11777b"),
"productOwners": [
{
_id: ObjectId("63b7c2fd06ebe7a8fd117781"),
iApproved: false
},
{
_id: ObjectId("63b7c2fd06ebe7a8fd117782"),
iApproved: false
}
]
},
{
"productIndex": 2,
"isProdApproved": false,
"productCategory": ObjectId("63b7c24c06ebe7a8fd11777b"),
"productOwners": [
{
_id: ObjectId("63b7c2fd06ebe7a8fd117781"),
iApproved: false
},
{
_id: ObjectId("63b7c2fd06ebe7a8fd117783"),
iApproved: false
}
]
},
{
"productIndex": 3,
"productCategory": "",
"productOwners": ""
}
]
}
]
}
I want to find the productOwner whose _id is 63b7c2fd06ebe7a8fd117781 in the productOwners and update the isApproved and isprodApproved to true. Other data will remain as it is.
I have tried this but it is only updating the first occurance
db.collectionA.update(
{
_id: ObjectId('63b7c24c06ebe7a8fd11777b'),
'products.productOwners._id': ObjectId('63b7c2fd06ebe7a8fd117781'),
},
{ $set: { 'products.$.productOwners.$[x].isApproved': true } },
{ arrayFilters: [{ 'x._id': ObjectId('63b7c2fd06ebe7a8fd117781') }] }
);
This one should work:
db.collection.updateMany({},
[
{
$set: {
products: {
$map: {
input: "$products",
as: "product",
in: {
$cond: {
if: { $eq: [{ $type: "$$product.productOwners" }, "array"] },
then: {
$mergeObjects: [
"$$product",
{ isProdApproved: { $in: [ObjectId("63b7c2fd06ebe7a8fd117781"), "$$product.productOwners._id"] } },
{
productOwners: {
$map: {
input: "$$product.productOwners",
as: 'owner',
in: {
$mergeObjects: [
"$$owner",
{ iApproved: { $eq: ["$$owner._id", ObjectId("63b7c2fd06ebe7a8fd117781")] } }
]
}
}
}
}
]
},
else: "$$product"
}
}
}
}
}
}
]
)
However, the data seem to be redundant. Better update only products.productOwners.iApproved and then derive products.isProdApproved from nested elements:
db.collection.aggregate([
{
$set: {
products: {
$map: {
input: "$products",
as: "product",
in: {
$cond: {
if: { $eq: [{ $type: "$$product.productOwners" }, "array"] },
then: {
$mergeObjects: [
"$$product",
{ isProdApproved: { $anyElementTrue: ["$$product.productOwners.iApproved"] } },
]
},
else: "$$product"
}
}
}
}
}
}
])

if condtion MongoDB aggregation

I need to display the currentLocation of which the process is completed,which are presented in four different collections. It's a chain process, in the order of labtest, inwards, goods, production. Each of them is different collections. I want the result in which, if labtest status is completed it goes to inwards, if inwards status also completed, then it goes and checks on goods. If goods status is In-Progress, it should return Inwards as currentLocation.
But, I am getting a response as Production....it seems like me if the condition is not working. Please help me to solve this issue
async function getGrnDetails(userParam) {
var user = User.findOne({ grnNo: userParam.grnNo })
var userss = User.aggregate([
{
$match:
{
grnNo: userParam.grnNo
}
}, {
$lookup: {
from: "goods",
let: { grnNo: "$grnNo" },
pipeline: [
{
$match: {
$expr:
{
$eq: ["$grnNo", '$$grnNo']
}
}
},
],
as: "Goods",
},
},
{
$lookup: {
from: "inwards",
let: { grnNo: "$grnNo" },
pipeline: [
{
$match: {
$expr:
{
$eq: ["$grnNo", '$$grnNo']
}
}
}
],
as: "Inwards",
}
},
{
$lookup: {
from: "productions",
let: { grnNo: "$grnNo" },
pipeline: [
{
$match: {
$expr:
{
$eq: ["$grnNo", '$$grnNo']
}
}
},
],
as: "Productions",
}
},
{
$unwind: {
"path": "$Productions"
}
},{
$unwind: {
"path": "$status"
}
},{
$unwind: {
"path": "$Goods"
}
},
{
$unwind: {
"path": "$Inwards"
}
},
{
$group: {
"_id": {
beforeHeadSettingArray: '$beforeHeadSettingArray',
beforeWashingArray: '$beforeWashingArray', id: '$id',
status: '$status', defaultAccountId: '$defaultAccountId',
beforeStenterArray: '$beforeStenterArray',
beforeCompactingArray: '$beforeCompactingArray',
afterCompactingArray: '$afterCompactingArray',
status: "$status",
Goods: '$Goods.processArray', Inwards: '$Inwards.fabricArray', Production: '$Productions.operationDisplay', currentLocation: {
$cond: {
if: {
$strcasecmp: ["$status", "Completed"]
}, then: {
$cond: {
if: {
$strcasecmp: ["$Inwards.status", "Completed"]
},
then: {
$cond: {
if: {
$strcasecmp: ["$Goods.status", "Completed"]
},
then:
{
$cond: {
if: {
$strcasecmp: ["$Productions.status", "Completed"]
},
then: "Productions",
else: "Goods"
}
},
else: "Inwards"
}
},
else: "LabTest"
}
},
else: "Yet To be Started"
}
}
},
"ProgressArray": {
$addToSet: {
"Inwards": "$Inwards.status",
"Goods": "$Goods.status",
"Productions": "$Productions.status",
"LabTest": "$status"
},
}
}
} ,{ $sort: { _id: 1 } },
{ $limit: 1 }
]);
return await userss
}
The Problem is I used strcasecmp, when I changed to eq it worked perfectly
currentLocation: {
$cond: {
if: {
$eq: ["$status", "Completed"]
}, then: {
$cond: {
if: {
$eq: ["$Inwards.status", "Completed"]
},
then: {
$cond: {
if: {
$eq: ["$Goods.status", "Completed"]
},
then:
{
$cond: {
if: {
$eq: ["$Productions.status", "Completed"]
},
then: "Finished",
else: "Productions"
}
},
else: "Goods"
}
},
else: "Inwards"
}
},
else: "LabTest"
}
}
,

Returning a document with two fields from the same array in MongoDB

Given documents such as
{
_id: 'abcd',
userId: '12345',
activities: [
{ status: 'login', timestamp: '10000001' },
{ status: 'logout', timestamp: '10000002' },
{ status: 'login', timestamp: '10000003' },
{ status: 'logout', timestamp: '10000004' },
]
}
I am trying to create a pipeline such as all users that have their latest login/logout activities recorded between two timestamps will be returned. For example, if the two timestamp values are between 10000002 and 10000003, the expected document should be
{
_id: 'abcd',
userId: '12345',
login: '10000003',
logout: '10000002'
}
Of if the two timestamp values are between -1 and 10000001, the expected document should be :
{
_id: 'abcd',
userId: '12345',
login: '10000001',
logout: null
}
Etc.
I know it has to do with aggregations, and I need to $unwind, etc., but I'm not sure about the rest, namely evaluating two fields from the same document array
You can try below aggregation:
db.col.aggregate([
{
$unwind: "$activities"
},
{
$match: {
$and: [
{ "activities.timestamp": { $gte: "10000001" } },
{ "activities.timestamp": { $lte: "10000002" } }
]
}
},
{
$sort: {
"activities.timestamp": -1
}
},
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
activities: { $push: "$activities" }
}
},
{
$addFields: {
login: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "login" ] } } } , 0 ] },
logout: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "logout" ] } } } , 0 ] }
}
},
{
$project: {
_id: 1,
userId: 1,
login: { $ifNull: [ "$login.timestamp", null ] },
logout: { $ifNull: [ "$logout.timestamp", null ] }
}
}
])
We need to use $unwind + $sort + $group to make sure that our activities will be sorted by timestamp. After $unwind you can use $match to apply filtering condition. Then you can use $filter with $arrayElemAt to get first (latest) value of filtered array. In the last $project you can explicitly use $ifNull (otherwise JSON key will be skipped if there's no value)
You can use below aggregation
Instead of $unwind use $lte and $gte with the $fitler aggregation.
db.collection.aggregate([
{ "$project": {
"userId": 1,
"login": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "login"] }
]
}
}
}
},
"logout": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "logout"] }
]
}
}
}
}
}}
])

Using document field as JS object field-name in aggregation pipeline

I have an JS object called tasksCountMap:
const tasksCountMap = {
'Freshman': 46,
'Senior': 10
}
and I need get count of task for each user type in my aggregation pipe, 'Freshman', 'Senior' it's document field called gradeLevel. I'm trying do it like this:
status: {
$let: {
vars: {
tasksCount: tasksCountMap['$gradeLevel'],
completedTasksCount: '$completedTasksCount[0].count'
},
in: {
$cond: {
if: { $or: [
{ $eq: ['$$tasksCount', '$$completedTasksCount'] },
{ $lte: ['$$tasksCount', '$$completedTasksCount'] },
]},
then: 'On track',
else: 'High priority'
}
}
}
}
Also '$completedTasksCount[0].count' doesn't work to...
Can someone show right way to do this?
All pipeline:
{
$match: {
type: 'student',
counselorUserName: username
},
$project: {
username: '$username',
email: '$email',
phone: '$phone',
fullName: '$fullName',
gradeLevel: {
$switch: {
branches: [{
case: {
$eq: ['$gradeLevel', '9']
},
then: 'Freshman'
},
{
case: {
$eq: ['$gradeLevel', '10']
},
then: 'Sophomore'
},
{
case: {
$eq: ['$gradeLevel', '11']
},
then: 'Junior'
},
{
case: {
$eq: ['$gradeLevel', '12']
},
then: 'Senior'
}
],
default: "Freshman"
}
}
},
$lookup: {
from: 'RoadmapTasksCompleted',
let: {
username: '$username',
gradeLevel: '$gradeLevel'
},
pipeline: [{
$match: {
monthToComplete: {
$in: prevMonthsNames
},
$expr: {
$and: [{
$eq: ['$username', '$$username']
},
{
$eq: ['$gradeLevel', '$$gradeLevel']
}
]
}
}
},
{
$count: 'count'
}
],
as: 'completedTasksCount'
},
$project: {
username: '$username',
email: '$email',
phone: '$phone',
fullName: '$fullName',
completedTask: { $arrayElemAt: ['$completedTasksCount', 0] },
status: {
$let: {
vars: {
tasksCount: tasksCountMap['$gradeLevel'],
completedTasksCount: '$completedTasksCount[0].count'
},
in: {
$cond: {
if: { $or: [
{ $eq: ['$$tasksCount', '$$completedTasksCount'] },
{ $lte: ['$$tasksCount', '$$completedTasksCount'] },
]},
then: '$$tasksCount',
else:'$$tasksCount'
}
}
}
}
}
$limit: 10,
$skip: 10,
}
You have to move the js map into the aggregation pipeline for you to be able to access the map.
I split $project stage into two and took the liberty to clean up the status calculation.
Something like
{"$project":{
"username":"$username",
"email":"$email",
"phone":"$phone",
"fullName":"$fullName",
"completedTask":{"$arrayElemAt":["$completedTasksCount",0]},
"tasksCountMap":[{"k":"Freshman","v":46},{"k":"Senior","v":10}]
}},
{"$addFields":{
"status":{
"$let":{
"vars":{
"tasksCount":{
"$arrayElemAt":[
"$tasksCountMap.v",
{"$indexOfArray":["$tasksCountMap.k","$gradeLevel"]}
]
},
"completedTasksCount":"$completedTask.count"
},
"in":{
"$cond":{
"if":{"$lte":["$$tasksCount","$$completedTasksCount"]},
"then":"$$tasksCount",
"else":"$$completedTasksCount"
}
}
}
}
}}

Grouping and counting across documents?

I have a collection with documents similar to the following format:
{
departure:{name: "abe"},
arrival:{name: "tom"}
},
{
departure:{name: "bob"},
arrival:{name: "abe"}
}
And to get output like so:
{
name: "abe",
departureCount: 1,
arrivalCount: 1
},
{
name: "bob",
departureCount: 1,
arrivalCount: 0
},
{
name: "tom",
departureCount: 0,
arrivalCount: 1
}
I'm able to get the counts individually by doing a query for the specific data like so:
db.sched.aggregate([
{
"$group":{
_id: "$departure.name",
departureCount: {$sum: 1}
}
}
])
But I haven't figured out how to merge the arrival and departure name into one document along with counts for both. Any suggestions on how to accomplish this?
You should use a $map to split your doc into 2, then $unwind and $group..
[
{
$project: {
dep: '$departure.name',
arr: '$arrival.name'
}
},
{
$project: {
f: {
$map: {
input: {
$literal: ['dep', 'arr']
},
as: 'el',
in : {
type: '$$el',
name: {
$cond: [{
$eq: ['$$el', 'dep']
}, '$dep', '$arr']
}
}
}
}
}
},
{
$unwind: '$f'
}, {
$group: {
_id: {
'name': '$f.name'
},
departureCount: {
$sum: {
$cond: [{
$eq: ['$f.type', 'dep']
}, 1, 0]
}
},
arrivalCount: {
$sum: {
$cond: [{
$eq: ['$f.type', 'arr']
}, 1, 0]
}
}
}
}, {
$project: {
_id: 0,
name: '$_id.name',
departureCount: 1,
arrivalCount: 1
}
}
]