MongoDB add $lookup result to array - mongodb

I have two collections
users:
{
{ _id: 1, name: 'John' },
{ _id: 2, name: 'Sarah' },
{ _id: 3, name: 'Mike' }
}
services:
{
{ _id: 1,
payment: [
{ uid: 1, paid: true },
{ uid: 2, paid: false }
]
},
{ _id: 2,
payment: [
{ uid: 3, paid: true }
]
}
}
I need result like this (from services):
{
{ _id: 1,
payment: [
{ uid: 1, paid: true, user: { _id: 1, name: 'John' } },
{ uid: 2, paid: false, user: { _id: 2, name: 'Sarah' } }
]
},
{ _id: 2,
payment: [
{ uid: 3, paid: true, user: { _id: 3, name: 'Mike' } }
]
}
}
I can $lookup by uid field, but how to add "paid" field to each item in lookup result? I know that it's must be really simple... but not for me now ;)
Thanks in advance!

db.getCollection('services').aggregate([
{ "$match": {} },
{ "$unwind": '$payment' },
{ "$lookup": {
"from": "users",
"localField": "payment.cid",
"foreignField": "_id",
"as": "user_data"
}},
{ "$unwind": '$user_data' },
{ "$addFields": {
"payment.user.name": "$user_data.name",
}},
{ "$group": {
"_id": "$_id",
"payment": { "$push": "$payment" }
}}
])

Related

MongoDB query to find top store from list of orders

I'm pretty new to Mongo. I have two collections that look as follows.
Order collection
[
{
id: 1,
price: 249,
store: 1,
status: true
},
{
id: 2,
price: 230,
store: 1,
status: true
},
{
id: 3,
price: 240,
store: 1,
status: true
},
{
id: 4,
price: 100,
store: 2,
status: true
},
{
id: 5,
price: 150,
store: 2,
status: true
},
{
id: 6,
price: 500,
store: 3,
status: true
},
{
id: 7,
price: 70,
store: 4,
status: true
},
]
Store Collection
[
{
id: 1,
name: "Store A",
status: true
},
{
id: 2,
name: "Store B",
status: true
},
{
id: 3,
name: "Store C",
status: true
},
{
id: 4,
name: "Store D",
status: false
}
]
How to find the top store from the list of orders, which should be based on the total sales in each store.
I have tried the following
db.order.aggregate([
{
"$match": {
status: true
}
},
{
"$group": {
"_id": "$store",
"totalSale": {
"$sum": "$price"
}
}
},
{
$sort: {
totoalSale: -1
}
}
])
I got the sorted list of stores from the above snippets. But I want to add store details along with total sales.
For more: https://mongoplayground.net/p/V3UH1r6YRnS
Expected Output
[
{
id: 1,
name: "Store A",
status: true,
totalSale: 719
},
{
id: 1,
name: "Store c",
status: true,
totalSale: 500
},
{
_id: 2,
id: 1,
name: "Store B",
status: true,
totalSale: 250
},
{
_id: 4,
name: "Store D",
status: true,
totalSale: 70
}
]
$lookup - store collection joins order collection and generate new field store_orders.
$set - Filter order with status: true from store_orders.
$set - totalSale field sum for store_orders.price.
$sort - Sort totalSale by descending.
$unset - Remove store_orders field.
db.store.aggregate([
{
$lookup: {
from: "order",
localField: "id",
foreignField: "store",
as: "store_orders"
}
},
{
$set: {
"store_orders": {
$filter: {
input: "$store_orders",
as: "order",
cond: {
$eq: [
"$$order.status",
true
]
}
}
}
}
},
{
$set: {
"totalSale": {
"$sum": "$store_orders.price"
}
}
},
{
$sort: {
totalSale: -1
}
},
{
$unset: "store_orders"
}
])
Sample Mongo Playground
You can start from store collection, $lookup the order collection, $sum the totalSales, then wrangle to your expected form
db.store.aggregate([
{
"$lookup": {
"from": "order",
let: {
id: "$id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$$id",
"$store"
]
}
}
},
{
$group: {
_id: null,
totalSale: {
$sum: "$price"
}
}
}
],
"as": "totalSale"
}
},
{
$unwind: "$totalSale"
},
{
$addFields: {
totalSale: "$totalSale.totalSale"
}
},
{
$sort: {
totalSale: -1
}
}
])
Here is the Mongo playground for youre reference.

Mongodb aggregate to return result only if the lookup field has length

I have two collections users and profiles. I am implementing a search with the following query:
User.aggregate(
[
{
$match: {
_id: { $ne: req.user.id },
isDogSitter: { $eq: true },
profileId: { $exists: true }
}},
{
$project: {
firstName: 1,
lastName: 1,
email: 1,
isDogSitter: 1,
profileId: 1,
}},
{
$lookup: {
from: "profiles",
pipeline: [
{
$project: {
__v: 0,
availableDays: 0,
}},
{
$match: {
city: search
}}
],
as: "profileId",
}}
],
(error, result) => {
console.log("RESULT ", result);
}
);
What this does is that its searches for the city in the profiles collection and when there is not search match then profileId becomes an empty array. What I really want is that if the profileId is an empty array then I don't want to return the other fields in the documents too. It should empty the array. Below is my current returned result.
RESULT [
{
_id: 60cabe38e26d8b3e50a9db21,
isDogSitter: true,
firstName: 'Test',
lastName: 'Sitter',
email: 'test#user.com',
profileId: []
}
]
Add $match pipeline stage after the $lookup pipeline stage and
add the empty array condition check over there.
User.aggregate(
[
{
$match: {
_id: { $ne: req.user.id },
isDogSitter: { $eq: true },
profileId: { $exists: true }
}},
{
$project: {
firstName: 1,
lastName: 1,
email: 1,
isDogSitter: 1,
profileId: 1,
}},
{
$lookup: {
from: "profiles",
pipeline: [
{
$project: {
__v: 0,
availableDays: 0,
}},
{
$match: {
city: search
}}
],
as: "profileId",
}}
{
$match: { // <-- Newly added $match condition
"profileId": {"$ne": []}
},
},
],
(error, result) => {
console.log("RESULT ", result);
}
);

Do an aggregate with a populate

I'm having troubles with the following. I wonder if it's possible to do it with a single query.
So I have the following model :
const Analytics = new Schema({
createdAt: {
type: Date,
default: Moment(new Date()).format('YYYY-MM-DD')
},
loginTrack: [
{
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Users',
}
}
]
}, { collection: 'analytics' });
And the user model :
const UserSchema = new mongoose.Schema(
{
nickname: {
type: String,
required: true,
unique: true
},
instance: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Instances',
default: null
}}, {collection: 'users'});
I want to get the connected users for a specific instance at a specific date.
AnalyticsModel.aggregate([
{
$match: {
createdAt: { "$gte": moment(args.startDate).format('YYYY-MM-DD'), "$lt": moment(args.endDate).format('YYYY-MM-DD')}
}
},
{
"$project": {
users: { $size: "$loginTrack" },
"createdAt": 1,
"_id": 0
}
}, {
"$group": {
"_id": "$createdAt",
"count": { "$sum": "$users" }
}
}
This gets me
[ { _id: '2019-02-11', count: 3 },
{ _id: '2019-02-08', count: 6 },
{ _id: '2019-02-07', count: 19 },
{ _id: '2019-02-06', count: 16 } ]
The results expected will be the same but I want to filter on users that belongs to a specific instance
Is it possible to do it with a single query or I need to do a populate first before the aggregation ?
UPDATE
I did some progress on it, I needed to add a lookup and I think it's ok :
AnalyticsModel.aggregate([
{"$unwind": "$loginTrack"},
{
$lookup:
{
from: 'users',
localField:'loginTrack.user_id',
foreignField: '_id',
as: '_users'
}
},
{
$match: {
createdAt: { "$gte": new Date(args.startDate), "$lt": new Date(args.endDate)}
}
},
{
$project: {
_users: {
$filter: {
input: '$_users',
as: 'item',
cond: {
$and: [
{ $eq: ["$$item.instance", new ObjectId(args.instance_id)] }
]
}
}
},
"createdAt": 1,
"_id": 0
}
},
{
"$group": {
"_id": "$createdAt",
"count": { "$sum": { "$size": "$_users" } }
}
}
Also the dates were in string in the model.
The output is now :
[ { _id: 2019-02-11T00:00:00.000Z, count: 2 } ]

Mongoose: $sum in $project return only zero

I have a query using $lookup that "join" two models and $project to select all fields that i need only, and in that $project I need to $sum a value called totalValue but only return zero:
My query
User.aggregate([{
$match: {
storeKey: req.body.store,
}
},
{
$group: {
_id: {
id: "$_id",
name: "$name",
cpf: "$cpf",
phone: "$phone",
email: "$email",
birthday: "$birthday",
lastName: "$lastname"
},
totalServices: {
$sum: "$services"
},
}
},
{
$lookup: {
from: "schedules",
"let": { "id": "$_id.phone" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$customer.phone", "$$id"] }}},
{ "$project": { "scheduleStart": 1, "scheduleEnd": 1 }}
],
"as": "user_detail"
}
},
{
$project: {
_id: 1,
name: 1,
name: 1,
cpf: 1,
phone: 1,
email: 1,
birthday: 1,
totalServices: 1,
totalValue: { $sum : "$user_detail.value" }, // here only return zero
count: {
$sum: 1
},
user_detail: 1
}
},
You need to $project your value field in the user_details projection to get it in the next aggregation stage
{ "$project": { "scheduleStart": 1, "scheduleEnd": 1, "value": 1 }}

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
}
}
]