Keep non related object in mongodb aggregation - javascript

I am trying to make an aggregation on users collection and than make a $lookup on another collection usersurveystatuses to find if this user has completed surveys. This works fine but it projects only users who have matching surveys. How can i keep all the users even if a survey doesn't relate.
Code:
db.collection('users')
.aggregate([
{
$match: { roles: { $in: ['user'] } },
},
{
$lookup: {
from: 'usersurveystatuses',
localField: '_id',
foreignField: 'user_id',
as: 'completedSurveys',
},
},
{
$unwind: {
path: '$completedSurveys',
},
},
{
$match: {
'completedSurveys.status': 'completed',
},
},
{
$group: {
_id: '$_id',
name: { $first: '$firstname' },
surname: { $first: '$lastname' },
credits: { $first: '$credits' },
completedSurveys: { $push: '$completedSurveys' },
},
},
{
$project: {
_id: 1,
name: 1,
surname: 1,
credits: 1,
completedSurveys: 1,
},
},
])
.toArray()
.then(users => {
console.log(users)
})
Result only returning the user who has completed surveys.
[ { _id: 5be98fa0b02eed1b2a17ca54,
name: 'User',
surname: 'User',
credits: 0,
completedSurveys: [ [Object], [Object] ] } ]

Related

Mongo Aggregation pipeline update or push

I have a MongoDB Model which consist of array of members as obejcts.
const guestSchema = new mongoose.Schema({
salutation: {
type: String,
},
members: [membersSchema],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
},
});
Members Schema:
const membersSchema = new mongoose.Schema({
name: String,
status: {
type: String,
enum: ['regular', 'helper'],
default: 'regular',
},
});
I want to achieve of doing an update in case documet with given ID exist or push to an array in case ID with document in array does not exist. I use aggregation pipeline, however I am not able to achieve pushing new document to array. Why can't I use push after else statement like this.
const subDocumentToUpsert = { 'name': mem.name, 'status': mem.status, '_id': ObjectId(mem.id)}
const subDocumentNoID = { 'name': mem.name, 'status': mem.status}
await Guest.findOneAndUpdate(
{ "_id": req.params.id },
[
{
$set: {
members: {
$cond: {
if: { $in: [subDocumentToUpsert._id, '$members._id'] },
then: {
$map: {
input: '$members',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document._id', subDocumentToUpsert._id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: {
$push: {
subDocumentNoID
},
},
},
},
},
},
},
]);
What is the best way of doing so? Thank you
You can do as follow:
db.collection.update({
_id: {
$in: [
1,
2
]
}
},
[
{
$set: {
members: {
$cond: {
if: {
$in: [
5,
"$members._id"
]
},
then: {
$map: {
input: "$members",
as: "sub",
in: {
$cond: {
if: {
$eq: [
"$$sub._id",
5
]
},
then: {
_id: 5,
status: "regular_updated",
name: "Negan_updated"
},
else: "$$sub"
},
},
},
},
else: {
$concatArrays: [
"$members",
[
{
_id: 5,
status: "regular_upserted",
name: "Negan_upserted"
}
]
]
}
}
}
}
}
}
],
{
multi: true
})
Explained:
Check if _id:5 exist in the subobject and update via $map/$cond only the object that has the _id:5.
In case there is no _id:5 add the new object to the array with $concatArrays.
Playground

Querying mongo array of embedded documents in aggregation pipeline $eq diff value

I have an array of mogoDB embedded documents
const ArticleSchema = new mongoose.Schema({
title: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
group: { type: mongoose.Schema.Types.ObjectId, ref: "ArticleGroup" },
});
// ArticleGroup
const ArticleGroupSchema = new mongoose.Schema({
name: { type: String, required: true },
desc: { type: String, default: "" },
state: { type: String, required: true, default: "public" },
members: [
{
desc: String,
role: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
},
],
});
// User
const userSchema = new mongoose.Schema(
{
email: { type: String, unique: true },
password: String,
},
);
test data :
{
_id: "6131aa5b367318e2df14b988",
title: "功能更新清单",
author: ObjectId("607edeb4b1e1bea9acb5af38"),
group: ObjectId("612d00a43c52975d4ade10d4")
}
I want to add a new field 'can_edit' to the document by comparing 'author._id == group.members. User' in 'aggregate' pipeline.
Article.aggregate([
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "author",
},
},
{
$lookup: {
from: "articlegroups",
localField: "group",
foreignField: "_id",
as: "group"
}
},
{
$addFields: {
can_edit: {
$eq: ["$group.members.user", "$author._id"],
},
}
}
])
However, the 'can_edit' I got was always' false ', and I'm sure that the 'author._id' value in my test data is the same as' group.members.
I want to get the data:
{
_id: "6131aa5b367318e2df14b988",
can_edit: true,
title: "功能更新清单",
author: {
_id: "607edeb4b1e1bea9acb5af38",
email: "frmachao#126.com",
},
group: {
_id: "612d00a43c52975d4ade10d4",
desc: "开发",
state: "public",
name: "开发小组",
members: [
{
_id: "612d00a43c52975d4ade10d5",
role: "admin",
user: "607edeb4b1e1bea9acb5af38",
}
]
}
}
Is there any way I can meet my needs? Does anyone know how to do that? I would grateful to you 🙏.
Just now, I try :
Article.aggregate([
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "author",
},
},
{
$lookup: {
from: "articlegroups",
localField: "group",
foreignField: "_id",
as: "group"
}
},
{
$addFields: {
can_edit: {
$in: [
"$author._id",
"$group.members.user"
],
},
},
}
])
However, the 'can_edit' I got was always' false '
With $in (aggregation),
Returns a boolean indicating whether a specified value is in an array.
$in: [
"$author._id",
"$group.members.user"
]
Updated:
Since Post Owner added the issue:
$in requires an array as a second argument
which means $group.members potentially is null and not an array.
Add condition checking for checking $group.members must be an array.
{
case: {
$ne: [
{
"$type": "$group.members"
},
"array"
]
},
then: false
},
$ne - Not equal check
$type - Return the BSON type of field
Complete query:
db.collection.aggregate([
{
$addFields: {
can_edit: {
$switch: {
branches: [
{
case: {
$ne: [
{
"$type": "$group.members"
},
"array"
]
},
then: false
},
{
case: {
$ne: [
{
"$type": "$group.members"
},
"array"
]
},
then: false
},
{
case: {
$in: [
"$author._id",
"$group.members.user"
]
},
then: true
}
],
default: false
}
}
}
}
])
Sample 1 in Mongo playground
Alternatively, the query can be shortened with $switch not needed as $in returns the same boolean result.
$and - Logical AND of multiple expressions
Complete query:
db.collection.aggregate([
{
$addFields: {
can_edit: {
$and: [
{
$eq: [
{
"$type": "$group.members"
},
"array"
]
},
{
$in: [
"$author._id",
"$group.members.user"
]
}
]
}
}
}
])
Sample 2 in Mongo Playground

Compare arrays and filter, using MongoDB aggregation

For my DB, I wrote the following pipeline:
let orders = await Order.aggregate(
{
$unwind: "$candidate",
},
{
$lookup: {
from: "groups",
localField: "candidate.groupId",
foreignField: "_id",
as: "groupData",
},
},
{
$lookup: {
from: "users",
let: {
id: "$candidate.groupId",
},
pipeline: [
{ $match: { groupId: { $ne: null } } },
{
$match: {
$expr: { $in: ["$$id", "$groupId"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "members",
},
},
{ $match: { "members._id": new ObjectId(req.userId) } },
{
$lookup: {
from: "users",
let: { ids: "$candidate.autonomousId" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "candidate",
},
},
{
$project: {
groupData: 1,
members: 1,
candidate: 1,
stillAvailable: 1,
_id: 0,
},
}
).toArray();
The output was the expected...
{ candidate:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' } ],
groupData:
[ { _id: 606632403fffb851b8c41d12,
name: 'Giraia' } ],
members:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' },
{ _id: 60182cbb2b654330d2458f89,
name: 'Jonas'} ] }
The last step in the pipeline would be to compare the arrays, filter which members were not candidates and add them to the array stillAvailable. I tried in many ways but I couldn't achieve my goal with aggregation. The only solution I could find was to process the result of the incomplete pipeline on my backend. The code is:
orders.forEach(
(order) =>
(order.stillAvailable = order.members.filter(
(autonomous) =>
!order.candidate.some((el) => {
return el._id.toString() === autonomous._id.toString();
})
))
);
With that, I reach the expected output...
{ candidate:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' } ],
groupData:
[ { _id: 606632403fffb851b8c41d12,
name: 'Giraia' ],
members:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' },
{ _id: 60182cbb2b654330d2458f89,
name: 'Jonas' ],
stillAvailable:
[ { _id: 60182cbb2b654330d2458f89,
name: 'Jonas' ] }
The problem is to better compartmentalize my code, it would be necessary to realize the last step (done with javascript on my backend) as one more step on the pipeline. Does anyone have an idea how to reach that?
After I wrote the question here, somehow the idea was better structured and I achieved the result, using $map and one more level of $lookup! I left the answer documented here in case someone falls into the same issue.
let orders = await Order.aggregate(
{
$unwind: "$candidate",
},
{
$lookup: {
from: "groups",
localField: "candidate.groupId",
foreignField: "_id",
as: "groupData",
},
},
{
$lookup: {
from: "users",
let: {
id: "$candidate.groupId",
},
pipeline: [
{ $match: { groupId: { $ne: null } } },
{
$match: {
$expr: { $in: ["$$id", "$groupId"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "members",
},
},
{ $match: { "members._id": new ObjectId(req.userId) } },
{
$lookup: {
from: "users",
let: { ids: "$candidate.autonomousId" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "candidate",
},
},
{
$project: {
groupData: 1,
members: 1,
candidate: 1,
_id: 0,
stillAvailable: {
$setDifference: [
{
$map: {
input: "$members",
as: "member",
in: "$$member._id",
},
},
{
$map: {
input: "$candidate",
as: "el",
in: "$$el._id",
},
},
],
},
},
},
{
$lookup: {
from: "users",
let: {
ids: "$stillAvailable",
},
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "stillAvailable",
},
}
).toArray();

MongoDB aggregation: flatten array property into root array

I'm working on a feature that recursively looks up a thread of comments using $graphLookUp, and I almost have it. (albeit in a somewhat convoluted way but it is working!)
The last step I need is the following:
instead of having the nested posteriorThread as a property of the root array ($$ROOT), just merge it onto the root itself.
AGGREGATION CODE:
const posteriorThread = await Comment.aggregate([
{
$match: {
_id: post.threadDescendant
}
},
{
$graphLookup: {
from: 'baseposts',
startWith: '$threadDescendant',
connectFromField: 'threadDescendant',
connectToField: '_id',
as: 'posteriorThread'
}
},
{
$unwind: '$posteriorThread'
},
{
$sort: { 'posteriorThread.depth': 1 }
},
{
$group: {
_id: '$_id',
posteriorThread: { $push: '$posteriorThread' },
root: { $first: '$$ROOT' }
}
},
{
$project: {
'root.posteriorThread': 0
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
{
posteriorThread: '$posteriorThread'
},
'$root'
]
}
}
}
]);
CURRENT OUTPUT
OUTPUT: posteriorThread
[
{
_id: '5f7eab40575e6fc56ee07604',
onModel: 'BasePost',
depth: 1,
user: '5f5da45245c07cc06e51b09f',
text: 'thread 0',
isThread: true,
threadDescendant: '5f7eabad575e6fc56ee07607',
posteriorThread: [
{
_id: '5f7eabad575e6fc56ee07607',
onModel: 'Comment',
depth: 2,
user: '5f5da45245c07cc06e51b09f',
text: 'thread 1',
isThread: true,
threadDescendant: '5f7eac82575e6fc56ee07609'
},
{
_id: '5f7eac82575e6fc56ee07609',
onModel: 'Comment',
depth: 3,
user: '5f5da45245c07cc06e51b09f',
text: 'thread 2',
isThread: true
}
]
}
];
DESIRED OUTPUT
OUTPUT: posteriorThread
[
{
_id: '5f7eab40575e6fc56ee07604',
onModel: 'BasePost',
depth: 1,
user: '5f5da45245c07cc06e51b09f',
text: 'thread 0',
isThread: true,
threadDescendant: '5f7eabad575e6fc56ee07607'
},
{
_id: '5f7eabad575e6fc56ee07607',
onModel: 'Comment',
depth: 2,
user: '5f5da45245c07cc06e51b09f',
text: 'thread 1',
isThread: true,
threadDescendant: '5f7eac82575e6fc56ee07609'
},
{
_id: '5f7eac82575e6fc56ee07609',
onModel: 'Comment',
depth: 3,
user: '5f5da45245c07cc06e51b09f',
text: 'thread 2',
isThread: true
}
];
I could accomplish this after the aggregation in regular js, but I would prefer to do it all in the aggregation.
The part that needs to be replaced is the mergeObjects bit and replaced with something else or the group aggregation and taking a different strategy, but I'm not sure what to put in it's place.
Also, if you have any other suggestions to make this cleaner, I'm all ears.
Thanks in advance.
Its really challenging. Atleast for me. And really very interesting case. Lets try my solution. Hope it works..
db.test.aggregate([
// PREVIOSU STEPS YOU ALREADY DID
{
$group: {
_id: "$_id",
items: {$push: "$$ROOT"},
subItems: {$first: "$posteriorThread"}
}
},
{
$project: {
"items.posteriorThread": 0
}
},
{
$addFields: {
allItems: {
$concatArrays: ["$items", "$subItems"]
}
}
},
{
$group: {
_id: null,
mergedItems: {$push: "$allItems"}
}
},
{
$unwind: "$mergedItems"
},
{
$unwind: "$mergedItems"
},
{
$replaceRoot: {
newRoot: "$mergedItems"
}
}
])
Thanks to #Sunil K Samanta for steering me in the right direction. It's not the prettiest solution, but it does give me the right solution.
const posteriorThread = await Comment.aggregate([
{
$match: {
_id: post.threadDescendant
}
},
{
$graphLookup: {
from: 'baseposts',
startWith: '$threadDescendant',
connectFromField: 'threadDescendant',
connectToField: '_id',
as: 'posteriorThread'
}
},
{
$unwind: '$posteriorThread'
},
{
$sort: { 'posteriorThread.depth': 1 }
},
{
$group: {
_id: '$_id',
items: { $push: '$$ROOT.posteriorThread' },
root: { $push: '$$ROOT' },
},
},
{
$project: {
items: 1,
root: { $slice: ['$$ROOT.root', 0, 1] },
},
},
{
$project: {
'root.posteriorThread': 0,
},
},
{
$addFields: {
allItems: {
$concatArrays: ['$root', '$items'],
},
},
},
{
$replaceRoot: {
newRoot: {
full_posterior: '$$ROOT.allItems',
},
},
},
])
)[0].full_posterior;

MongoDB aggregate function not working as expected

I am trying to display a list of the last messages between User A and any other User. Here is what I have so far, but let me explain what wrong with the below query:
If User A messages 'Test1' to User B, and User B replies with 'Test2', and then User A replies with 'Test3', then only the Test2 message will show up.
The 'Test1' message will show up first, as it's the first message in the conversation, but from there on, it will only show User B's messages.
The behavior I want is for the query to always return 'Test3', in this case. Here is what I have:
My Model:
const MostRecentMessageSchema = new Schema({
to: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
from: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
conversation: {
type: mongoose.Schema.Types.ObjectId,
ref: "conversation"
},
deletedBy: {
type: [String]
},
date: {
type: Date,
default: Date.now
}
});
await MostRecentMessages.aggregate(
[
{
$match: {
$or: [
{ from: mongoose.Types.ObjectId(userId) },
{ to: mongoose.Types.ObjectId(userId) }
],
deletedBy: { $ne: mongoose.Types.ObjectId(userId) },
_id: { $ne: filteredIds }
}
},
{ $project: { _id: 1, from: 1, to: 1, conversation: 1, date: 1 } },
{ $sort: { date: -1 } },
{
$group: {
_id: {
userConcerned: {
$cond: {
if: {
$eq: ["$to", mongoose.Types.ObjectId(userId)]
},
then: "$to",
else: "$from"
}
},
interlocutor: {
$cond: {
if: {
$eq: ["$to", mongoose.Types.ObjectId(userId)]
},
then: "$from",
else: "$to"
}
}
},
from: { $first: "$from" },
to: { $first: "$to" },
date: { $first: "$date" },
conversation: { $first: "$conversation" }
}
},
{
$lookup: {
from: "conversations",
localField: "conversation",
foreignField: "_id",
as: "conversation"
}
},
{ $unwind: { path: "$conversation" } },
{
$lookup: {
from: "users",
localField: "to",
foreignField: "_id",
as: "to"
}
},
{ $unwind: { path: "$to" } },
{
$lookup: {
from: "users",
localField: "from",
foreignField: "_id",
as: "from"
}
},
{ $unwind: { path: "$from" } }
],
function(err, mostRecentMessages) {
if (err) {
console.log("Error fetching most recent messages", err);
} else {
if (connectedUsersAllMessages[userId]) {
allMessagesSocket.sockets.connected[
connectedUsersAllMessages[userId].socketId
].emit("response", {
mostRecentMessages
});
}
}
}
);
}, 5000);
}
What am I doing wrong here? Here is a POC. It should return
{
from: "A",
to: "C",
number: 3,
message: "A-C Test 4"
},
but it returns,
{
from: "C",
to: "A",
number: 3,
message: "A-C Test 3"
},

Categories

Resources