Getting last message from each user - Mongoose - javascript

I'm creating an app in which I want to include chat. The last thing that I have to do is showing last message from each conversation (including those sent by me).
My Db looks like this
[
{
_id: 5f9b06cdb5d5eb3b94f066e3
from: "5f860655e3fc7f43709cc408"
to: "5f8b0a941efeb42a24f1f926"
message: "123"
createdAt: 2020-10-29T18:15:41.424+00:00
updatedAt: 2020-10-29T18:15:41.424+00:00
__v: 0
},
{
_id: 5f9b0761b5d5eb3b94f066e5
from: "5f860655e3fc7f43709cc408"
to: "5f8b0a941efeb42a24f1f926"
message: "321"
createdAt: 2020-10-29T18:15:41.424+00:00
updatedAt: 2020-10-29T18:15:41.424+00:00
__v: 0
}
]
Results that I want to achieve:
[
{
message: '123',
createdAt: 2020-10-29T18:15:41.424+00:00,
from: "5f860655e3fc7f43709cc408"
}
]

Chat.findOne({_id: 0, message: 1, createdAt: 1, from: 1}).sort({createdAt: -1})

You need something like this I think:
db.collection.aggregate([
{
"$sort": {
"createdAt": -1
}
},
{
"$project": {
"_id": 0,
"message": 1,
"createdAt": 1,
"from": 1
}
},
{
"$limit": 1
}
])
sort is to sort by date. In this case, the newest date first.
project is to use only these fields.
limit to get only the first.
Mongo PlayGround example here
Edit:
If you want to query by conversation (with your model I don't know how to do that...) you can use $match and something like: $match {"id_conver": "..."}
For example, querying matching "from" and "to".
db.collection.aggregate([
{
"$match": {
"from": "5f860655e3fc7f43709cc408",
"to": "5f8b0a941efeb42a24f1f926"
}
},
{
"$sort": {
"createdAt": -1
}
},
{
"$project": {
"_id": 0,
"message": 1,
"createdAt": 1,
"from": 1
}
},
{
"$limit": 1
}
])

Related

Group Array of Objects by key and sum values from mongoose aggregation result

I have a query result from a mongoose aggregation query that I need to further process, or at best do it in the aggregation itself.
The aggregation looks like this
result = await TokenBalance.aggregate([
{
$match: {
$and: [
{ ethervalue: { $gte: minBalance } },
{
ethervalue: { $lte: maxBalance }
}
]
}
},
{ $limit:limit }
])
This returns an array of Objects of this format
{
"_id": "61013d6dda7d7c0015af5ccf",
"balances": [
{
"address": "0x1fc3ddeb035310930a444c0fa59c01618d5902af",
"symbol": "HBTC",
"balance": 5.21419339e-10,
"usdvalue": 0.000020969961637162402
},
{
"address": "0x1fc3ddeb035310930a444c0fa59c01618d5902af",
"symbol": "NSBT",
"balance": 1.258566,
"usdvalue": 27.427343477595258
},
{
"address": "0x1fc3ddeb035310930a444c0fa59c01618d5902af",
"symbol": "CRV",
"balance": 517.985955847106,
"usdvalue": 806.7017064052314
},
{
"address": "0x1fc3ddeb035310930a444c0fa59c01618d5902af",
"symbol": "USDT",
"balance": 0.003469,
"usdvalue": 0.003470159747979122
}
],
"address": "0x1fc3ddeb035310930a444c0fa59c01618d5902af",
"ethervalue": 0.7604598621232733,
"createdAt": "2021-07-28T11:20:13.927Z",
"updatedAt": "2021-07-28T11:20:13.927Z",
"__v": 0
},
What I need, is the "balances" property to be processed as grouped by symbol and for each of these symbols sum the balance and usdvalue fields.
I would prefer this do be done in the aggregation if possible, but I can not seem to get it right, even not in pure nodejs.
I want the result to be like this:
[
{
symbol: USDC, balance: xxx, usdvalue: yyy
},
{
symbol: USDT, balance: zzz, usdvalue: jjj
}
]
You can use the below approach,
$unwind to deconstruct the balances array
$group by symbol and sum balance and usdvalue
$addFields to rename _id field to symbol and and remove _id field
result = await TokenBalance.aggregate([
{
$match: {
$and: [
{ ethervalue: { $gte: minBalance } },
{ ethervalue: { $lte: maxBalance } }
]
}
},
{ $unwind: "$balances" },
{
$group: {
_id: "$balances.symbol",
balance: { $sum: "$balances.balance" },
usdvalue: { $sum: "$balances.usdvalue" }
}
},
{
$addFields: {
symbol: "$_id",
_id: "$$REMOVE"
}
},
{ $limit:limit }
])
Playground

how to group two elements using Mongoose?

I need to create a query using mongoose, which allows me to use the group, to count the elements that are in the STARTED or NOT INITIATED state, and then return the count of the elements that are in each of the states.
This is the json object that I am working with.
[
{
status: "INITIATED",
_id: "6057e0013a3dec1d44a7d152",
group: "dairy",
location: "001",
total_items: 30,
__v: 0
},
{
status: "INITIATED",
_id: "6057e0013a3dec1d44a7d153",
group: "dairy",
location: "002",
total_items: 45,
__v: 0
},
{
status: "INITIATED",
_id: "6057e0013a3dec1d44a7d154",
group: "dairy",
location: "003",
total_items: 12,
__v: 0
},
{
status: "NOT INITIATED",
_id: "6057e0013a3dec1d44a7d155",
group: "dairy",
location: "004",
total_items: 50,
__v: 0
},
{
status: "NOT INITIATED",
_id: "6057e0013a3dec1d44a7d156",
group: "drugs",
location: "005",
total_items: 23,
__v: 0
},
{
status: "NOT INITIATED",
_id: "6057e0013a3dec1d44a7d157",
group: "drugs",
location: "006",
total_items: 76,
__v: 0
}]
For example, from the dairy group I need to return a json with the following structure.
{
'id': null,
'group': 'dairy',
'INITIATED': 3,
'NO INITIATED': 1
}
Because of the dairy group 3 of the elements are in the STARTED state and one of the elements is in the NOT INITIATED state. Another example would be with the group of drugs that should return the following json.
{
'id': null,
'group': 'medications',
'STARTED': 0,
'NOT INITIATED': 2
}
In order to do this I am trying to get the following code to work.
db.collection.aggregate ([
{
"$ group": {
_id: {
source: "$ group",
status: "$ status"
},
count: {
$ sum: 1
}
}
}
])
But I am not very clear on how to use the group to generate the response as I need it.
Ok, so this was trickier than I thought at first, but here's how you could do it:
First, you group your documents by the group field, and sum up the counts for the status field. I'm using the $cond and $eq operators to make sure that the status matches the count field. Finally, you apply $project to get the desired output without the _id field:
db.collection.aggregate([
{ "$group": {
"_id": "$group",
"NOT INITIATED": {
"$sum": {
"$cond": [ { "$eq": [ "$status", "NOT INITIATED" ] }, 1, 0]
}
},
"INITIATED": {
"$sum": {
"$cond": [ { "$eq": [ "$status", "INITIATED" ] }, 1, 0]
}
},
} },
{ "$project": {
"_id": 0,
"group": "$_id",
"INITIATED":1,
"NOT INITIATED":1
} }])
I've created an example on mongoplayground for you: https://mongoplayground.net/p/H-72VzWoeoN

Change aggregate result

Im currently having an almost working aggregate query, that would get the users array, and order the objects there by their score.
But not getting the expected output, for some reason the entire family data is beeing printed again.
How do i fix this?
Executed code:
return Family.aggregate([
// Initial document match (uses index, if a suitable one is available)
{ $match: {name: 'Management'}},
// Expand the scores array into a stream of documents
{ $unwind: '$users' },
// Sort in descending order
{ $sort: {
'users.score': -1
}}]
Current result:
{ _id: 5c8e5c79e55ef42ce4923e0b,
name: 'Management',
time_started: 1552833657354,
location: 1,
isFamily: true,
last_member: 0,
score: 0,
users:
{ userid: '5c852292d1bd911abc4957dc',
joined_date: 1552839246371,
permission: 5,
upgrade: 0,
score: 141,
_id: 5c8e724e6e5e6512447c1a61 },
__v: 0 },
{ _id: 5c8e5c79e55ef42ce4923e0b,
name: 'Management',
time_started: 1552833657354,
location: 1,
isFamily: true,
last_member: 0,
score: 0,
users:
{ userid: '5c8522a96bcca9268c0753fe',
joined_date: 1552833657354,
permission: 6,
upgrade: 0,
score: 32,
_id: 5c8e5c79e55ef42ce4923e0c },
__v: 0 } ]
wanted result:
{
name: 'Management',
time_started: 1552833657354,
location: 1,
isFamily: true,
last_member: 0,
score: 0,
users:
[{ userid: '5c852292d1bd911abc4957dc',
joined_date: 1552839246371,
permission: 5,
upgrade: 0,
score: 141,
_id: 5c8e724e6e5e6512447c1a61 },
__v: 0 },
{ userid: '5c8522a96bcca9268c0753fe',
joined_date: 1552833657354,
permission: 6,
upgrade: 0,
score: 32,
_id: 5c8e5c79e55ef42ce4923e0c },
__v: 0 }
]}
You need to use one more $group stage to reshape the splited array into its original form after $unwind
Family.aggregate([
{ "$match": { "name": "Management" }},
{ "$unwind": "$users" },
{ "$sort": { "users.score": -1 }},
{ "$group": {
"_id": "$_id",
"users": { "$push": "$users" },
"name": { "$first": "$name" },
"time_started": { "$first": "$time_started" },
"isFamily": { "$first": "$isFamily" },
"last_member": { "$first": "$last_member" },
"score": { "$first": "$score" },
}}
])

How to improve this aggregate with many $projects

I have created an aggregate function and I feel it's pretty long and non-DRY. I'm wondering what ways I can improve it.
My Thread model has a sub-document called revisions. The function tries to get the most recent revision that has the status of APPROVED.
Here is the full model.
{
"_id": ObjectId("56dc750769faa2393a8eb656"),
"slug": "my-thread",
"title": "my-thread",
"created": 1457249482555.0,
"user": ObjectId("56d70a491128bb612c6c9220"),
"revisions": [
{
"body": "This is the body!",
"status": "APPROVED",
"_id": ObjectId("56dc750769faa2393a8eb657"),
"comments": [
],
"title": "my-thread"
}
]
}
And here is the aggregate function I want to improve.
Thread.aggregate([
{ $match: {
slug: thread
} },
{ $project: {
user: '$user',
created: '$created',
slug: '$slug',
revisions: {
$filter: {
input: '$revisions',
as: 'revision',
cond: { $eq: [ '$$revision.status', 'APPROVED' ] }
}
}
} },
{ $sort: { 'revisions.created': -1 } },
{ $project: {
user: '$user',
created: '$created',
slug: '$slug',
revisions: { $slice: ["$revisions", 0, 1] }
} },
{ $unwind: '$revisions'},
{ $project: {
body: '$revisions.body',
title: '$revisions.title',
user: '$user',
slug: '$slug',
created: '$created'
}}
])
Well you cannot really since there are $sort and $unwind stages in between on purpose. It's also basically "wrong", since the $sort cannot re-order the array until you $unwind it first.
Then it is better to use $group and $first instead, to just get the first element from the sort in each document:
Thread.aggregate([
{ "$match": {
"slug": thread
} },
{ "$project": {
"user": 1,
"created": 1,
"slug": 1,
"revisions": {
"$filter": {
"input": "$revisions",
"as": "revision",
"cond": { "$eq": [ "$$revision.status", "APPROVED" ] }
}
}
} },
// Cannot sort until you $unwind
{ "$unwind": "$revisions" },
// Now that will sort the elements
{ "$sort": { "_id": 1, "revisions.created": -1 } },
// And just grab the $first boundary for everything
{ "$group": {
"_id": "$_id",
"body": { "$first": "$revisions.body" },
"title": { "$first": "$revisions.title" },
"user": { "$first": "$user" },
"slug": { "$first": "$slug" },
"created": { "$first": "$created" }
}}
])
You could always reform the array with $push and then apply $arrayElemAt instead of the $slice to yield just a single element, but it's kind of superflous considering it would need another $project after the $group in the first place.
So even though there are "some" operations you can do without using $unwind, unfortunately "sorting" the arrays generated out of functions like $filter is not something that can be presently done, until you $unwind the array first.
If you didn't "need" the $sort on the "revisions.created" ( notably missing from your sample document ) then you can instead just use normal projection instead:
Thread.find(
{ "slug": slug, "revisions.status": "APPROVED" },
{ "revisions.$": 1 },
)
Only when sorting array elements would you need anything else, since the $ positional operator will just return the first matched element anyway.

Mongoose find with multiple matches

I'm new to this technology and working with Node and Express server that uses Mongoose. I have following schema for a document collection.
var empSchema = new mongoose.Schema({
_id: String,
orgName: {type: String, required: true},
locName: {type: String, required: true},
empName: {type: String, required: true}
});
Here I get a list of location names like "NewYork", "London", "Paris" etc... in a request and needs to return the documents in the response as following....
{
result:[{locName:"NewYork",
empList:[
{orgName:"abc", empName:"emp1"},
{orgName:"xyz", empName:"emp2"}]
},
{locName:"London",
empList:[
{orgName:"pkq", empName:"emp13"},
{orgName:"mns", empName:"emp23"}]
}]
}
What would be the best way to use mongoose from Node. I think making multiple queries (each one with a location) to mongodb is a bad idea.
Is there a way to get the expected json response with single call to mongoose? Thanks.
Yes, use the aggregation framework to get the desired output. The aggregation pipeline will consist of a $group operator pipeline stage which groups the documents by the locName field and the $addToSet accumulator operator to add the orgName and empName fields to an array empList. The last pipeline stage $project operator then replaces the _id field from the previous aggregation stream with a new field locName.
To demonstrate this concept, suppose you have a sample collection which you insert with mongo shell:
db.employees.insert([
{
_id: "1",
orgName: "abc",
locName: "New York",
empName: "emp1"
},
{
_id: "2",
orgName: "xyz",
locName: "New York",
empName: "emp2"
},
{
_id: "3",
orgName: "pkq",
locName: "London",
empName: "emp13"
},
{
_id: "4",
orgName: "mns",
locName: "London",
empName: "emp23"
}
])
The following aggregation produces the desired result:
db.employees.aggregate([
{
"$group": {
"_id": "$locName",
"empList": {
"$addToSet": {
"orgName": "$orgName",
"empName": "$empName"
}
}
}
},
{
"$project": {
"_id": 0,
"locName": "$_id",
"empList": 1
}
}
])
Output:
/* 0 */
{
"result" : [
{
"empList" : [
{
"orgName" : "mns",
"empName" : "emp23"
},
{
"orgName" : "pkq",
"empName" : "emp13"
}
],
"locName" : "London"
},
{
"empList" : [
{
"orgName" : "xyz",
"empName" : "emp2"
},
{
"orgName" : "abc",
"empName" : "emp1"
}
],
"locName" : "New York"
}
],
"ok" : 1
}
In Mongoose, you can use the aggregation pipeline builder like this:
Employee.aggregate()
.group({
"_id": "$locName",
"empList": {
"$addToSet": {
"orgName": "$orgName",
"empName": "$empName"
}
}
})
.project({
"_id": 0,
"locName": "$_id",
"empList": 1
})
.exec(function (err, res) {
if (err) return handleError(err);
console.log(res);
});
// Or the simple aggregate method
var pipeline = [
{
"$group": {
"_id": "$locName",
"empList": {
"$addToSet": {
"orgName": "$orgName",
"empName": "$empName"
}
}
}
},
{
"$project": {
"_id": 0,
"locName": "$_id",
"empList": 1
}
}
]
Employee.aggregate(pipeline, function (err, res) {
if (err) return handleError(err);
console.log(res);
});
All queries, when you need to group by sum values called aggregate. You can read about it in the mongo docs and same methods have model in Mongoose. To produce your query, you can use code like this:
Employee
.aggregate()
.group({ _id: '$locName', empList: { $push: "$$ROOT" }})
.exec(function (err, res) {
});
If you need not to query all table, there is also have a match method.

Categories