Grouping in mongoose aggregation according to condition - node.js

I am building a chat app based on the MERN stack and need to fetch all chats for a specific user.
This is my chat messages schema:
const message = mongoose.Schema({
from: {
type: Schema.Types.ObjectId,
ref: "users",
required: true,
},
to: {
type: Schema.Types.ObjectId,
ref: "users",
required: true,
},
text: { type: String, required: false },
imgLink: { type: String, required: false },
sentAt: { type: Date, required: true },
deliveredAt: { type: Date, required: false },
readAt: { type: Date, required: false },
});
Is there a way to fetch all chats for a user and group them by who sent/recieved these chats.
I used this aggregation pipeline
const fetchChatPipeline = (_id) => {
return [
{
$match: {
$or: [
{
to: mongoose.Types.ObjectId(_id),
},
{
from: mongoose.Types.ObjectId(_id),
},
],
},
},
{
$sort: {
sentAt: 1,
},
},
{
$group: {
_id: "$from",
from: { $first: "$from" },
to: { $first: "$to" },
messages: {
$push: {
text: "$text",
imgLink: "$imgLink",
sentAt: "$sentAt",
deliveredAt: "$deliveredAt",
readAt: "$readAt",
},
},
},
},
{
$lookup: {
from: "users",
localField: "from",
foreignField: "_id",
as: "from",
},
},
{
$unwind: {
path: "$from",
preserveNullAndEmptyArrays: false,
},
},
{
$lookup: {
from: "users",
localField: "to",
foreignField: "_id",
as: "to",
},
},
{
$unwind: {
path: "$to",
preserveNullAndEmptyArrays: false,
},
},
{
$project: {
to: {
_id: 1,
name: 1,
},
from: {
_id: 1,
name: 1,
},
messages: 1,
},
},
];
};
and got the following result:
{
"chats": [
{
"_id": "60368ee8a4e8494c74ccbeec",
"from": {
"_id": "60368ee8a4e8494c74ccbeec",
"name": "Jaivardhan Singh"
},
"to": {
"_id": "603a637ab356a309dcca9099",
"name": "Jai Singh "
},
"messages": [
{
"text": "hello",
"sentAt": "2021-02-27T16:33:40.335Z"
},
{
"text": "What you upto",
"sentAt": "2021-02-27T16:34:16.852Z"
}
]
},
{
"_id": "603a637ab356a309dcca9099",
"from": {
"_id": "603a637ab356a309dcca9099",
"name": "Jai Singh "
},
"to": {
"_id": "60368ee8a4e8494c74ccbeec",
"name": "Jaivardhan Singh"
},
"messages": [
{
"text": "Hi",
"sentAt": "2021-02-27T16:35:32.343Z"
},
{
"text": "Nothing just chilling",
"sentAt": "2021-02-27T16:35:41.720Z"
},
{
"text": "What you upto",
"sentAt": "2021-02-27T16:35:49.662Z"
}
]
}
]
}
Is there a way i can merge these two chats in the results as they have common participants. Thanks.

Just change your $match stage as below and $group it by null
{
$match: {
$or: [
{ to: mongoose.Types.ObjectId(_id) },
{ from: mongoose.Types.ObjectId(_id) }
],
to: { $ne: mongoose.Types.ObjectId(_id) }
}
}

Related

Aggregation: Return documents based on fields in a subdocument

I’m using an aggregation to return data via a lookup to build the links between documents.
At the moment, the linking is working when User A creates links between their own assets to navigate.
But if User A is viewing an asset that’s been shared with them by User B and navigates to one that has a link to an asset that hasn’t been shared with them, those are the documents I need to exclude from the results.
So, I need the documents for assets that have a document in attributes that contains my userId, or — as in the $match — the $_id of an attribute that's in the attributes array in assets. When an asset is shared with someone, a document in attributes is created.
The data for a Link is:
{
"_id": {
"$oid": "63769c377615fe4cdb4995a6"
},
"userId": "620920aa9ddac2074a50472f",
"toAsset": {
"$oid": "63769c117615fe4cdb499515"
},
"fromAsset": {
"$oid": "63769c067615fe4cdb4994d9"
},
"comment": "<p>Linking of Note 0001 to Note 0002.</p>",
"createdAt": {
"$date": {
"$numberLong": "1668717623761"
}
},
"updatedAt": {
"$date": {
"$numberLong": "1668717623761"
}
},
"isEmbedded": false,
"isActive": true,
"__v": 0
}
The data for an Asset, as in toAsset and fromAsset, is:
{
"_id": {
"$oid": "6377a8d834671794449f0dca"
},
"userId": "636b73f31527830f7bd7a47e",
"folderId": "636b73f31527830f7bd7a482",
"title": "Note that hasn't been shared",
"note": "<p>Here's a Note that hasn't been shared.</p>",
"typeOfAsset": "note",
"createdAt": {
"$date": {
"$numberLong": "1668786392389"
}
},
"updatedAt": {
"$date": {
"$numberLong": "1668786392389"
}
},
"isActive": 3,
"meta": [...],
"preferences": [...],
"sequence": 1,
"tags": [],
"attributes": [
{
"$oid": "6377a8d834671794449f0dc8"
}
],
"__v": 0
}
I’m using attributes to manage what assets have been shared with whom, and the data is:
{
"_id": {
"$oid": "6377a8d834671794449f0dc8"
},
"userId": "636b73f31527830f7bd7a47e",
"numberOfViews": 2,
"isFavourite": false,
"isToRead": false,
"typeOfAccess": "isOwner",
"sharing": {
"typeOfShare": "withUsers",
"sharedWith": [],
"segementsForUrl": []
},
"__v": 0
}
Now, the task here is to somehow how return the assets that have been shared, but after a bunch of different attempts (as per the code that’s been commented out), I’ve so far failed.
The code is:
const match = {
$match: {
[args.directionOfLink]: new mongoose.Types.ObjectId(args.assetId)
}
}
const project = {
$project: {
_id: 0,
id: '$_id',
userId: 1,
[directionOfLink]: 1,
comment: 1,
createdAt: 1,
updatedAt: 1,
isActive: 1,
score: {
$meta: 'searchScore'
}
}
}
const lookup = {
$lookup: {
from: 'assets',
localField: directionOfLink,
foreignField: '_id',
let: { attributesInAsset: '$attributes' },
pipeline: [
{
$lookup: {
from: 'attributes',
as: 'attributes',
pipeline: [{
$match: {
$expr: {
$in: [ '$_id', '$$attributesInAsset' ]
// $and: [
// { $eq: [ '$userId', context.body.variables.userId ] },
// { $in: [ '$typeOfAccess', ['isOwner', 'asAuthor', 'asReader'] ] },
// ]
}
}
}]
}
},
{
$project: {
_id: 1,
userId: 1,
folderId: 1,
title: 1,
typeOfAsset: 1,
attributes: 1,
createdAt: 1,
updatedAt: 1,
isActive: 1
}
}
],
as: directionOfLink
}
}
Here, directionOfLink is either "toAsset" or "fromAsset".
Any thoughts would be appreciated.
As a non expert in MongoDB, it's possible this isn't the most performant approach, but at least it works:
const lookup = {
$lookup: {
from: 'assets',
localField: directionOfLink,
foreignField: '_id',
as: directionOfLink,
pipeline: [
{
$lookup: {
from: 'assets_attributes',
as: 'attributesInAssets',
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ '$userId', context.body.variables.userId ] },
{ $in: [ '$typeOfAccess', ['isOwner', 'asAuthor', 'asReader'] ] },
]
}
}
}
]
}
},
{
$unwind: '$attributesInAssets'
},
{
$match: {
$expr: {
$in: [ '$attributesInAssets._id', '$attributes' ]
}
}
},
{
$group: {
_id: '$_id',
userId: { $first: '$userId' },
folderId: { $first: '$folderId' },
title: { $first: '$title' },
typeOfAsset: { $first: '$typeOfAsset' },
createdAt: { $first: '$createdAt' },
updatedAt: { $first: '$updatedAt' },
isActive: { $first: '$isActive' },
attributes: { $first: '$attributes' },
attributesInAssets: {
$push: '$attributesInAssets._id'
}
}
},
{
$project: {
_id: 1,
userId: 1,
folderId: 1,
title: 1,
typeOfAsset: 1,
attributes: 1,
attributesInAssets: 1,
createdAt: 1,
updatedAt: 1,
isActive: 1
}
}
]
}
}
const redact = {
$redact: {
$cond: {
if: {
$gt: [ {
$size: `$${directionOfLink}`
}, 0 ]
},
then: '$$KEEP',
else: '$$PRUNE'
}
}
}

How to add field in array with aggregate mongoose from another array coming from previous stage

I have two mongoose schema one for keywords :
const keywordSchema = new Schema<keywordI>(
{
sentence: { type: String, required: true },
example: { type: String, required: true },
translate: { type: String, required: true },
imageUrl: { type: String, required: true },
audioUrl: { type: String, required: true },
deletedAt: { type: Date, select: false },
},
{ timestamps: true }
);
and one for keywordsUser :
const keywordUserSchema = new Schema<keywordUserI>(
{
keywordId: {
type: mongoose.Types.ObjectId,
ref: "Keyword",
required: true,
},
userId: {
type: mongoose.Types.ObjectId,
ref: "User",
required: true,
},
isBookMarked: { type: Boolean, default: false },
correctAnswer: { type: Number, default: 0 },
wrongAnswer: { type: Number, default: 0 },
},
{ timestamps: true }
);
I want to get keywords by user grouped by type which I calculate from wrongAnswer and correctAnswer
I try to do this using aggregate
return KeyWordUser.aggregate([
{
$match: { userId },
},
{
$addFields: {
type: {
$function: {
body: getType,
args: ["$correctAnswer", "$wrongAnswer"],
lang: "js",
},
},
},
},
{
$group: {
_id: "$type",
words: {
$push: "$keywordId",
},
isBookMarked: {
$push: { isBookMarked: "$isBookMarked", keywordId: "$keywordId" },
},
},
},
{
$lookup: {
localField: "words",
from: "keywords",
foreignField: "_id",
as: "props",
},
},
{
$addFields: { type: "$_id" },
},
{
$project: {
_id: 0,
words: 0,
},
},
]);
I have this result from aggregate
returned data from. postman
I want to add new aggregate stage to get this result
previous result
[
{
"isBookMarked": [
{
"keywordId": "62e2a0ad3113b8234511cd72"
},
{
"isBookMarked": false,
"keywordId": "62e2a0ba3113b8234511cd74"
}
],
"props": [
{
"_id": "62e2a0ad3113b8234511cd72",
"sentence": "hello",
"example": "Hello , what's your name",
"translate": "مرحبا ما اسمك",
"imageUrl": "src/img/keywords/keyword-A7H_M-1659019437698.png",
"audioUrl": "src/audio/keywords/keyword-YyhUp-1659019437700.mpeg",
"createdAt": "2022-07-28T14:43:57.708Z",
"updatedAt": "2022-07-28T14:43:57.708Z",
"__v": 0
},
{
"_id": "62e2a0ba3113b8234511cd74",
"sentence": "hello 2 ",
"example": "Hello , what's your name 2 ",
"translate": "مرحبا ما اسمك 2",
"imageUrl": "src/img/keywords/keyword-2JO7D-1659019450396.png",
"audioUrl": "src/audio/keywords/keyword-kt5Tc-1659019450397.mpeg",
"createdAt": "2022-07-28T14:44:10.400Z",
"updatedAt": "2022-07-28T14:44:10.400Z",
"__v": 0
}
],
"type": "strong"
}
]
and i want this result :
[
{
"props": [
{
"_id": "62e2a0ad3113b8234511cd72",
"sentence": "hello",
"example": "Hello , what's your name",
"translate": "مرحبا ما اسمك",
"imageUrl": "src/img/keywords/keyword-A7H_M-1659019437698.png",
"audioUrl": "src/audio/keywords/keyword-YyhUp-1659019437700.mpeg",
"createdAt": "2022-07-28T14:43:57.708Z",
"updatedAt": "2022-07-28T14:43:57.708Z",
"__v": 0
},
{
"_id": "62e2a0ba3113b8234511cd74",
"isBookMarked" : false
"sentence": "hello 2 ",
"example": "Hello , what's your name 2 ",
"translate": "مرحبا ما اسمك 2",
"imageUrl": "src/img/keywords/keyword-2JO7D-1659019450396.png",
"audioUrl": "src/audio/keywords/keyword-kt5Tc-1659019450397.mpeg",
"createdAt": "2022-07-28T14:44:10.400Z",
"updatedAt": "2022-07-28T14:44:10.400Z",
"__v": 0
}
],
"type": "strong"
}
]
You can just populate with the keywordId not with aggregate

mongodb - left join with conditions

I am attempting an left antijoin on these two collections.
I want all users where department is equal to 'IT' that aren't in a meeting that had an endAt time > 175. Either as a creator or receiver.
So essentially whoever hasn't been in a meeting in the last xxx time.
Based on below collections:
John would be retrieved because he is apart of department IT and has not been a receiver or creator after '175'.
Jane has an endAt time after 175 and is in IT so wouldn't be retrieved
Bill is apart of finance so even though he hasn't been it doesn't matter
Bob has an endAt time after 175 and is in IT so wouldn't be retrieved
Mary is in IT and has not been in any meetings so she is retrieved.
Users Collection:
[
{
_id: ObjectId("1"),
name: "john",
department: 'IT'
},
{
_id: ObjectId("2"),
name: "jane",
department: 'IT'
},
{
_id: ObjectId("3"),
name: "bill",
department: 'finance'
},
{
_id: ObjectId("4"),
name: "Bob",
department: 'IT'
},
{
_id: ObjectId("5"),
name: "Mary",
department: 'IT'
}
]
Meetings Collection:
[
{
_id: ObjectId("a"),
endedAt: 100,
creator_id: ObjectId("1"),
receiver_id: ObjectId("2")
},
{
_id: ObjectId("b"),
endedAt: 150,
creator_id: ObjectId("1"),
receiver_id: ObjectId("3")
},
{
_id: ObjectId("c"),
endedAt: 200,
creator_id: ObjectId("4"),
receiver_id: ObjectId("2")
},
{
_id: ObjectId("d"),
endedAt: 250,
creator_id: ObjectId("2"),
receiver_id:
}
]
Output:
[
{
_id: ObjectId("1"),
name: "john",
department: 'IT'
},
{
_id: ObjectId("5"),
name: "Mary",
department: 'IT'
}
]
My approach:
db.users.aggregate([
{
$match:
{
type: 'IT'
}
},
{
$lookup:
{
from: "meetings",
let:
{
userid: "$_id",
},
pipeline: [
{ $match:
{ $expr:
{
$and:[
{
$or: [
{ $eq: ["$receiver_id", "$$userid"] },
{ $eq: ["$creator_id", "$$userid"] },
]
},
{ $gt: ["$endAt", 175] }
]
}
}
}
],
as: "result"
}
},
{
$unwind:
{
path: "$result",
preserveNullAndEmptyArrays: true
}
},
{
$match:
{
result: {$exists:false}
}
}
])
Query
match "IT"
join if >175 AND (userid in any of the 2 (creator/receiver))
*its lookup pipeline, because multiple join creteria
reject those that are joined
Test code here
db.users.aggregate([
{
"$match": {
"department": {
"$eq": "IT"
}
}
},
{
"$lookup": {
"from": "meetings",
"let": {
"userid": "$_id"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$gt": [
"$endedAt",
175
]
},
{
"$or": [
{
"$eq": [
"$$userid",
"$creator_id"
]
},
{
"$eq": [
"$$userid",
"$receiver_id"
]
}
]
}
]
}
}
},
{
"$project": {
"_id": 1
}
}
],
"as": "meetings"
}
},
{
"$match": {
"$expr": {
"$eq": [
"$meetings",
[]
]
}
}
},
{
"$unset": [
"meetings"
]
}
])
aggregate
db.users.aggregate([
{
"$match": {
department: "IT"
}
},
{
"$lookup": {
"from": "meeting",
"localField": "_id",
"foreignField": "creator_id",
"as": "meeting_creator"
}
},
{
"$lookup": {
"from": "meeting",
"localField": "_id",
"foreignField": "receiver_id",
"as": "meeting_receiver"
}
},
{
"$match": {
"$and": [
{
"meeting_creator.endedAt": {
"$not": {
"$gt": 175
}
}
},
{
"meeting_receiver.endedAt": {
"$not": {
"$gt": 175
}
}
}
]
}
},
{
"$project": {
_id: 1,
name: 1,
department: 1
}
}
])
data
db={
"users": [
{
_id: "1",
name: "john",
department: "IT"
},
{
_id: "2",
name: "jane",
department: "IT"
},
{
_id: "3",
name: "bill",
department: "finance"
},
{
_id: "4",
name: "Bob",
department: "IT"
},
{
_id: "5",
name: "Mary",
department: "IT"
}
],
"meeting": [
{
_id: "a",
endedAt: 100,
creator_id: "1",
receiver_id: "2"
},
{
_id: "b",
endedAt: 150,
creator_id: "1",
receiver_id: "3"
},
{
_id: "c",
endedAt: 200,
creator_id: "4",
receiver_id: "2"
},
{
_id: "d",
endedAt: 250,
creator_id: "2",
receiver_id: ""
}
]
}
result
[
{
"_id": "1",
"department": "IT",
"name": "john"
},
{
"_id": "5",
"department": "IT",
"name": "Mary"
}
]
mongoplayground
This is the solution I came up with that ended up working, does anyone have any details what would be the most efficient?
db.users.aggregate([
{
$match:
{
type: 'IT'
}
},
{
$lookup:
{
from: "meetings",
let:
{
userid: "$_id",
},
pipeline: [
{ $match:
{ $expr:
{
$and:[
{
$or: [
{ $eq: ["$receiver_id", "$$userid"] },
{ $eq: ["$creator_id", "$$userid"] },
]
},
{ $gt: ["$endAt", 175] }
]
}
}
}
],
as: "result"
}
},
{
$unwind:
{
path: "$result",
preserveNullAndEmptyArrays: true
}
},
{
$match:
{
result: {$exists:false}
}
}
])

Get true or false ( only value not array or object ) from mongodb lookup aggregation

I am trying to make a function that returns if a user is a favorite or not.
I am using MongoDB aggregate on User model with lookout with favourites model ( in which all favourite user has saved ) with pipeline and $project
here is the MongoDB aggregate function query
await User.aggregate([
{
$match: {
_id: ObjectId(_id),
},
},
{
$lookup: {
from: 'userportfolios',
localField: '_id',
foreignField: 'user',
as: 'user_portfolios',
},
},
{
$lookup: {
from: 'favourites',
pipeline: [
{
$match: {
user: ObjectId(data.user._id),
favourites: {
$elemMatch: {
$eq: ObjectId(_id),
},
},
},
},
{
$project: {
_id: 0,
is_favourite: {
$cond: [
{ $ifNull: ['$is_favourite', false] },
{ $literal: true },
{ $literal: false },
],
},
},
},
],
as: 'is_favourite',
},
},
]);
and I am getting this result
[
{
"_id": "602b5078e8eb5339e0134351",
"is_active": true,
"is_admin": false,
"is_block": false,
"user_type": "company",
"firstName": "Sagar",
"lastName": "Davara",
"mobileNumber": "*******",
"email": "s*********#gmail.com",
"password": "b7370338e54b1cb051be0dd54eba2f7f",
"verificationCode": "210768",
"createdAt": "2021-02-16T04:56:24.091Z",
"updatedAt": "2021-02-16T04:56:24.091Z",
"user_portfolios": [],
"is_favourite": [
{
"is_favourite": false
}
]
}
]
but I want to get is_favourite: true instead of an array of object
I need this output from mongodb
[
{
"_id": "602b5078e8eb5339e0134351",
"is_active": true,
"is_admin": false,
"is_block": false,
"user_type": "company",
"firstName": "Sagar",
"lastName": "Davara",
"mobileNumber": "6353764283",
"email": "sagardspeed3#gmail.com",
"password": "b7370338e54b1cb051be0dd54eba2f7f",
"verificationCode": "210768",
"createdAt": "2021-02-16T04:56:24.091Z",
"updatedAt": "2021-02-16T04:56:24.091Z",
"user_portfolios": [],
"is_favourite": false
}
]
Just add below stage after $lookup stage:
{
$addFields: {
is_favourite: { $arrayElemAt: ["$is_favourite.is_favourite", 0] }
}
}

Mongo Aggregation Returns [{}] for one of it's key:value

I am use NodeJS, ExpressJS with Mongoose Mongo ORM.
My Schema is as follows.
const chatMessageSchema = new Schema({
_id: {
type: String,
default: () => (uuidV1().replace(/-/g, ''))
},
roomid: String,
message: Schema.Types.Mixed,
type: String, // text, card, qrt, attachment
notes: [notesSchema],
posted_by_user: String,
}, {
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
},
collection: 'ChatMessages'
});
The nested sub schema for notes is as follows;
const notesSchema = new Schema({
_id: {
type: String,
default: () => (uuidV1().replace(/-/g, ''))
},
roomid: String,
messageid: String,
message: Schema.Types.Mixed,
type: String, // text, attachment
posted_by_user: String,
}, {
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
}});
Initially when a user creates a chatMessage the notes is default kept as an empty array initially [].
The user can explicitly create a note message after the message has been created, the notes is an array sub documents.
Now what happens is when I get a list of messages. I need to show posted_by_user which is an id of string. Against that id of string i perform a $lookup which gets the user profile. The aggregation is as follows.
return this.aggregate([
{ $match : {roomid: roomid} },
{ $sort: {created_at: -1} },
{
$lookup: {
from: "RefUserProfiles",
localField: "posted_by_user",
foreignField: "_id",
as: "posted_by_user"
}
},
{ $unwind: "$posted_by_user" },
{ $skip: pageOptions.page * pageOptions.limit },
{ $limit: pageOptions.limit },
{ $sort: {created_at: 1} },
{
$unwind: { path: "$notes", preserveNullAndEmptyArrays: true }
},
{
$lookup: {
from: "RefUserProfiles",
localField: "notes.posted_by_user",
foreignField: "_id",
as: "notes.posted_by_user"
}
},
{
$unwind: { path: "$notes.posted_by_user", preserveNullAndEmptyArrays: true }
},
{
$group: {
_id: "$_id",
created_at: { $last: "$created_at" },
roomid: { $last: "$roomid" },
message: { $last: "$message" },
notes: { $addToSet: "$notes" },
type: { $last: "$type" },
posted_by_user: { $last: "$posted_by_user" },
read_by_recipients: { $last: "$read_by_recipients" },
}
}
]);
Now since in some cases the notes for some messages is an empty array, so for that very particular case the object I get in responses is like [{}]. Is this how it is suppose to be. or is there a better use case involved.
For example the response I get based on this query is as follows.
"conversation": [
{
"_id": "7cdbf630e6ec11e79166559abfb987ac",
"created_at": "2017-12-22T07:48:23.956Z",
"roomid": "aacc6b70e68211e79166559abfb987ac",
"message": {
"text": "message 1"
},
"notes": [{}],
"type": "text",
"posted_by_user": {
"id": "4d6fd5a95d57436d944586a1d5b2b2ff",
"name": "Adeel Imran",
"profilePicture": "",
"phone": "",
"email": "",
"designation": "",
"type": "user"
},
},
{
"_id": "80d91470e6ec11e79166559abfb987ac",
"created_at": "2017-12-22T07:48:30.648Z",
"roomid": "aacc6b70e68211e79166559abfb987ac",
"message": {
"text": "message 4"
},
"notes": [
{
"posted_by_user": {
"id": "86049b3d28f640a8a6722c57b73a292e",
"name": "Paul Johnson",
"profilePicture": "",
"phone": "",
"email": "",
"designation": "",
"type": "sp"
},
"type": "text",
"message": {
"text": "yeah "
},
"messageid": "26401d30e5a511e7acc4c734d11adb25",
"roomid": "27d3ed80e0cc11e78318b3cd036890f2",
"updated_at": "2017-12-20T16:45:44.132Z",
"created_at": "2017-12-20T16:45:44.132Z",
"_id": "38ad3750e5a511e7acc4c734d11adb25"
}
],
"type": "text",
"posted_by_user": {
"id": "4d6fd5a95d57436d944586a1d5b2b2ff",
"name": "Adeel Imran",
"profilePicture":"",
"phone": "",
"email": "",
"designation": "",
"type": "user"
},
}
],
Any suggestions or directions for this will be highly appreciated. Thank you for taking the time out to review this question.
Cheers.

Resources