mongo use $lookup after $group - node.js

I have a visits collection where I successfully count the number of visits per location
Visits model:
{
"_id": {
"$oid": "5a3969e2f4ea3e33ac5a523d"
},
"locationId": "5a395ccf210a1d35d0df4a58"
}
locationId above is of type 'Object' as I learned lookup localField and foreignField must be of same type
nodejs code =>
let sort = { "count": -1, "locationId": 1 };
Visit.aggregate([
{
$match:{
$and: [
{ accountId: req.session.passport.user },
{
'details.dateTimeIn': {
$gte: new Date(dateFrom), $lte: new Date(dateTo)
}
}
]
}
},
{
"$group": {
//_id: name,
_id: "$locationId",
count: { $sum: 1 }
}
},
{ $sort: sort }
])
Output is half ok:
[
{
"_id":"5a395ccf210a1d35d0df4a58",
"count":20
}
]
Instead of showing location id id like to show location name. Schema for locations collection is:
{
"_id": {
"$oid": "5a395ccf210a1d35d0df4a58"
"name": "Tower A",
"__v": 0
}
}
Research suggests I need to use $lookup to get that JOIN effect
So I tried
{
"$lookup": {
from: "locations",
localField: "_id",
foreignField: "_id",
as: "locationdetails"
}
}
but the match seems broken. The closest I got was a list of all locations in 'locationdetails'
But with code above here is the empty locationdetails
[
{
"_id":"5a395ddf1d221918d0041313",
"count":20,
"locationdetails":[
]
}
]
What am I missing ?

Related

How get all objects from an array of Id's in mongoose with express nodejs?

I'm developing a Chat application where I'm saving the conversation list in the DB like this;
Conversations Collection:
{
members: [ "123", "456" ]
...rest
}
and User Collection:
{
_id: ObjectId( "123" ), name: "anyone"
},
{
_id: ObjectId( "456" ), name: "someone"
}
I want the result to be like:
{
members: [
{_id: ObjectId( "123" ), name: "anyone"},
{_id: ObjectId( "456" ), name: someone"}
]
}
I know aggregation with lookup is the rescue but can't find a way how to fetch all ids from that member's array because in the future it can be 20 to 30 ids. If it is a simple field then I'm able to fetch but with an array, I can't.
I have tried this
db.conversations.aggregate([
{
"$lookup": {
"from": "users",
"localField": "members",
"foreignField": "_id",
"as": "members_details"
}
}
])
but it returns members_details: [ ]
You could use the following query to accomplish what you want. You'll need to use $lookup since you are wanting to gather data from a different collection then the one you are currently querying.
You can check out a live demo here
Here is an updated live demo
Database
db={
"conversations": [
{
members: [
123,
456
]
}
],
"users": [
{
"_id": 123,
"name": "foo"
},
{
"_id": 456,
"name": "bar"
}
]
}
Query
db.conversations.aggregate([
{
"$lookup": {
"from": "users",
"localField": "members",
"foreignField": "_id",
"as": "members"
}
},
{
$project: {
_id: 0,
members: 1
}
}
])
Result
[
{
"members": [
{
"_id": 123,
"name": "foo"
},
{
"_id": 456,
"name": "bar"
}
]
}
]
Update
See new live demo here
New Query
db.conversations.aggregate([
{
$unwind: "$members"
},
{
"$lookup": {
"from": "users",
"as": "membersFlat",
"let": {
memberObjectId: {
"$toObjectId": "$members"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$memberObjectId",
"$_id"
]
}
}
}
]
}
},
{
$group: {
_id: null,
members: {
$push: {
"_id": {
$first: "$membersFlat._id"
},
"name": {
$first: "$membersFlat.name"
}
}
}
}
},
{
$project: {
_id: 0
}
}
])
New Result
[
{
"members": [
{
"_id": ObjectId("124578987898787845658574"),
"name": "foo"
},
{
"_id": ObjectId("124578986532124578986532"),
"name": "bar"
}
]
}
]
Since your members id is string in conversation collection, you can convert it to object id and the join to users collection,
$addFields to update members array
$map to iterate loop of members array
$toObjectId to convert string object id type to object id type
db.conversations.aggregate([
{
$addFields: {
members: {
$map: {
input: "$members",
in: {
$toObjectId: "$$this"
}
}
}
}
},
{
$lookup: {
from: "users",
localField: "members",
foreignField: "_id",
as: "members"
}
}
])
Playground
You can use a normal .find() like this:
const members = [
ObjectId('4ed3ede8844f0f351100000c'),
ObjectId('4ed3f117a844e0471100000d'),
ObjectId('4ed3f18132f50c491100000e')
]
const docs = await model.find({
'_id': { $in: members }
}).exec()
Install mongoose-autopopulate. And in your model code
import {Schema, model} from 'mongoose';
const modelSchema = new Schema({
...
chats: [{type: Schema.Types.ObjectId, ref: "Chat", autopopulate: true}]
})
modelSchema.plugin(require('mongoose-autopopulate'));
...

I will like to return the previous object and the next object of a matching object id in node

Please i am new to node js and MongoDB.
When i want to retrieve a post by id, i want to be able to retrieve the previous post and next post also.
this is my post, it only retrieves the current post by id.
Post.findById(req.params.postId)
.then((existingpost) => {
console.log(Post.find(req.params.postId))
if (existingpost) {
res.send(existingpost);
}
return res.status(404).send({
message: "Post does not exist with id " + req.params.postId,
});
})
.catch((err) => {
if (err.kind === "ObjectId") {
return res.status(404).send({
message: "Post does not exist with id " + req.params.postId,
});
}
return res.status(500).send({
message:
"Some error occurred while retrieving the post with postId " +
req.params.postId,
});
});
};
I currently receive the object with the id like this which is fine.
{
"_id": "6009f3e294d8a033402a76e7",
"title": "Covid 19 in Italy",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
}
But i will love to receive the object of the current id, the previous object and the next object.
something like this.
[{
"_id": "3230g5e382d8a033402a76e7",
"title": "Effect of Covid on the Economy",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
},
{
"_id": "6009f3e294d8a033402a76e7",
"title": "Covid 19 in Italy",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
},
{
"_id": "4567hye294d8a033402a76e7",
"title": "Life after Covid",
"author": "John Doe",
"createdAt": "2021-01-21T21:36:34.514Z",
"updatedAt": "2021-01-21T21:36:34.514Z",
"__v": 0
}]
Since its a UUID, this approach might help you..
$sort to sort the documents by asc
$group and $unwind to get the index
$facet to categorize the incoming data into current and allDocs
We know current is only one object, so we do $unwind to deconstruct the array
We already know the index, so we use $filter to get prev, current and next using index
$unwind to deconstruct the array
$replaceRoot to make the objects to the root
Here is the script
db.collection.aggregate([
$sort: { createdAt: 1 } },
{
$group: {
_id: null,
data: { $push: "$$ROOT"}
}
},
{ $unwind: { path: "$data", includeArrayIndex: "index" } },
{
$facet: {
current: [
{ $match: { "data._id": "3230g5e382d8a033402a76e7" } }
],
allDocs: [
{ $match: {} }
]
}
},
{
$unwind: "$current"
},
{
$project: {
docs: {
$filter: {
input: "$allDocs",
cond: {
$or: [
{ $eq: [ "$$this.index", { $subtract: [ "$current.index", 1 ] } ] },
{ $eq: [ "$$this.index", "$current.index" ] },
{ $eq: [ "$$this.index", { $add: [ "$current.index", 1 ] } ] }
]
}
}
}
}
},
{ "$unwind": "$docs" },
{ "$replaceRoot": { "newRoot": "$docs.data" } }
])
Working Mongo playground
There are many ways to do this, this is one of the way. If you feel you have a lot of document, then try to avoid $unwind which is expensive, in that case you can try using createdDate instead of index
I am not sure is there any straight way to do this, you can try aggregation,
Using UUID and CreatedAt:
$facet to get all documents in all after sorting in ascending order by createdAt
$let to define vars states with start and total documents,
$cond check condition if index of input uuid is zero then return start: 0 and total: 2 documents we have to slice from all array, else get current index and subtract minus 1 and total: 3
in to return slice documents on the base of start and total
Post.aggregate([
{ $facet: { all: [{ $sort: { createdAt: 1 } }] } },
{
$project: {
result: {
$let: {
vars: {
states: {
$cond: [
{ $eq: [{ $indexOfArray: ["$all._id", req.params.postId] }, 0] },
{ start: 0, total: 2 },
{
start: {
$subtract: [{ $indexOfArray: ["$all._id", req.params.postId] }, 1]
},
total: 3
}
]
}
},
in: { $slice: ["$all", "$$states.start", "$$states.total"] }
}
}
}
}
])
Playground
Using ObjectID:
convert your string input id req.params.postId to object id using mongoose.Types.ObjectId
$facet to separate result,
first, $match to get current and next documents, $sort _id in descending order, $limit 2
second, $match to get previous document, $sort _id in descending order, $limit 1
$project to get result after concat both array first and second using $concatArrays
req.params.postId = mongoose.Types.ObjectId(req.params.postId);
Post.aggregate([
{
$facet: {
first: [
{ $match: { _id: { $gte: req.params.postId } } },
{ $sort: { _id: 1 } },
{ $limit: 2 }
],
second: [
{ $match: { _id: { $lt: req.params.postId } } },
{ $sort: { _id: -1 } },
{ $limit: 1 }
]
}
},
{ $project: { result: { $concatArrays: ["$first", "$second"] } } }
])
Playground

I want to display only one product image

This is Code in node js
const result = await OrderDB.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(id) } },
{
$lookup: {
from: 'products',
localField: 'product',
foreignField: '_id',
as: 'productDetail',
},
},
{
$project: {
productDetail: {
name: 1,
price: 1,
productImage: 1,
},
},
},
])
This is the response of code
{
"message": "Get Order Successfully",
"result": [
{
"_id": "5ff47348db5f5917f81871aa",
"productDetail": [
{
"name": "Camera",
"productImage": [
{
"_id": "5fe9b8a26720f728b814e246",
"img": "uploads\\product\\7Rq1v-app-7.jpg"
},
{
"_id": "5fe9b8a26720f728b814e247",
"img": "uploads\\product\\FRuVb-app-8.jpg"
}
],
"price": 550
}
]
}
]
}
I want to display only one productImage from the response using nodejs and mongoose
This is using in aggregate projection
I was use $arrayElemAt but it is don't work
I also use $first but it is don't work
so projection method I use to display only one *productImage*
Data looks like multi level nested.
You have array of results, each result contains array of productDetails
play
You need to unwind the data to get the first productImage
db.collection.aggregate([
{
"$unwind": "$result"
},
{
"$unwind": "$result.productDetail"
},
{
$project: {
pImage: {
"$first": "$result.productDetail.productImage"
}
}
}
])
With the above response
db.collection.aggregate([
{
"$project": {
productDetails: {
$map: {
input: "$productDetail",
in: {
"$mergeObjects": [
"$$this",
{
productImage: {
"$arrayElemAt": [
"$$this.productImage",
0
]
}
}
]
}
}
}
}
}
])
Working Mongo playground

Mongodb lookup array of elements with combined result

So these are my two documents
Order document:
{
"_id":"02a33b9a-284c-4869-885e-d46981fdd679",
"context":{
"products":[
{
"id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00"
}
],
},
}
Product document:
{
"_id":"e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"context":{
"name": "My Product",
"image": "someimage"
},
}
So I'm trying to do a lookup for a products in order document, but the result should contain combined fields, like so:
"products":[
{
"_id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00",
"name": "My Product",
"image": "someimage"
}
],
Not sure how to do this, should I do it outside of the lookup, or inside? This is my aggregation
Orders.aggregate([
{
"$lookup":{
"from":"products",
"let":{
"products":"$context.products"
},
"pipeline":[
{
"$match":{
"$expr":{
"$in":[
"$_id",
"$$products.id"
]
}
}
},
{
"$project":{
"_id":0,
"id":1,
"name":"$context.name"
}
}
],
"as":"mergedProducts"
}
},
{
"$project":{
"context":"$context",
"mergedProducts":"$mergedProducts"
}
},
]);
You need to run that mapping outside of $lookup by running $map along with $arrayElemAt to get single pair from both arrays and then apply $mergeObjects to get one object as a result:
db.Order.aggregate([
{
$lookup: {
from: "products",
localField: "context.products.id",
foreignField: "_id",
as: "productDetails"
}
},
{
$addFields: {
productDetails: {
$map: {
input: "$productDetails",
in: {
_id: "$$this._id",
name: "$$this.context.name"
}
}
}
}
},
{
$project: {
_id: 1,
"context.products": {
$map: {
input: "$context.products",
as: "prod",
in: {
$mergeObjects: [
"$$prod",
{ $arrayElemAt: [ { $filter: { input: "$productDetails", cond: { $eq: [ "$$this._id", "$$prod.id" ] } } }, 0 ] }
]
}
}
}
}
}
])
Mongo Playground
The goals of the last step is to take take two arrays: products and productDetails (the output of $lookup) and find matches between them. We know there's always one match so we can get only one item $arrayElemAt 0. As an output of $map there will be single array containing "merged" documents.

Mongodb - Find count of distinct items after applying aggregate and match

Trying to figure out something from Mongo using mongoose in optimal way.
I have following documents
Regions
{
"_id" : ObjectId("5cf21263ff605c49cd6d8016"),
"name" : "Asia"
}
Countries can be part of multiple regions
{
"_id" : ObjectId("5d10a4ad80a93a1d7cd56cc6"),
"regions" : [
ObjectId("5d10a50080a93a1d7cd56cc7"),
ObjectId("5cf2126bff605c49cd6d8017")
],
"name" : "India"
}
Places belongs to one country
{
"_id" : ObjectId("5d11bb8180a93a1d7cd56d26"),
"name" : "Delhi",
"country" : ObjectId("5d136e7a4e480863a51c4056"),
}
Programs each in dayshows array represents one day. On a day show can cover multiple places.
{
"_id" : ObjectId("5d11cc9480a93a1d7cd56d31"),
"dayshows" : [
{
"_id" : ObjectId("5d11cc9480a93a1d7cd56d41"),
"places" : [
ObjectId("5d11bb8180a93a1d7cd56d26")
],
},
{
"_id" : ObjectId("5d11cc9480a93a1d7cd56d3c"),
"places" : [
ObjectId("5d11bb8180a93a1d7cd56d26"),
ObjectId("5d11bc7c80a93a1d7cd56d2e")
]
}
]
}
What am I trying to figure out?
For a given region, for each country in region which all places are covered and count of programs for each place. Using nodejs and mongoose.
Example
Input - Asia
Output
India
- Delhi (3)
- Mumbai (5)
Thailand
- Pattaya (2)
- Bangkok (5)
New to mongo.
You need to use $lookup to cross different collections.
Pipeline:
Stages 1-6 serves to get all related data.
(Optional) Stages 7-10 serves to transform aggregated data into key:pair object.
ASSUMPTION
Programs to visit 2 places counted as is (Place1: +1, Place2: +1)
You know how to execute MongoDB aggregation in node.js
db.Regions.aggregate([
{
$match: {
name: "Asia"
}
},
{
$lookup: {
from: "Countries",
let: {
region: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$$region",
"$regions"
]
}
}
},
{
$lookup: {
from: "Places",
localField: "_id",
foreignField: "country",
as: "Places"
}
}
],
as: "Countries"
}
},
{
$unwind: "$Countries"
},
{
$unwind: "$Countries.Places"
},
{
$lookup: {
from: "Programs",
localField: "Countries.Places._id",
foreignField: "dayshows.places",
as: "Countries.Places.Programs"
}
},
{
$project: {
"name": 1,
"Countries.name": 1,
"Countries.Places.name": 1,
"Countries.Places.Programs": {
$size: "$Countries.Places.Programs"
}
}
},
{
$group: {
_id: {
name: "$name",
Countries: "$Countries.name"
},
Places: {
$push: {
k: "$Countries.Places.name",
v: "$Countries.Places.Programs"
}
}
}
},
{
$project: {
_id: 1,
Places: {
$arrayToObject: "$Places"
}
}
},
{
$group: {
_id: "$_id.name",
Countries: {
$push: {
k: "$_id.Countries",
v: "$Places"
}
}
}
},
{
$project: {
_id: 0,
name: "$_id",
Countries: {
$arrayToObject: "$Countries"
}
}
}
])
MongoPlayground

Resources