Mongoose group by an lookup with nested arrays - javascript

I have two schema's , first one is Quiz and the other one is quizResults, I want to get aggregated data from the quizResult while doing the lookup in quiz Schema. Below is my Quiz schema:
vidId: {type: Number, required: true},
status: {type: Number, enum: [statusType.quizStatusEnums.LIVE, statusType.quizStatusEnums.DELETED], default: statusType.quizStatusEnums.LIVE},
questions: [
{
questionNum: { type: Number },
questionName: { type: String },
answers: [
{
answerId: { type: String, default: uuid.v4() },
answerName: { type: String },
isCorrect: { type: Boolean },
answerType: { type: Number, enum: [statusType.quizTypeEnums.QUIZ, statusType.quizTypeEnums.SURVEY], default: statusType.quizTypeEnums.QUIZ},
hotspotId: { type: Number },
overlayId: {type: Number},
panelId: {type: String}
}
]
}
],
The Second one is QuizResults: The aggregation query needs to be performed on this collection.
created: {
type: Date,
},
vidId: {
type: Number,
required: true,
},
viewerId: {
type: String,
required: true,
},
quizId: {
type: Schema.Types.ObjectId,
ref: 'quiz'
},
questionId: {
type: Schema.Types.ObjectId,
ref: 'quiz'
},
answerId: {
type: String,
required: true
},
isCorrect: {
type: Boolean,
default: false,
},
whenAnswered: {
type: Date
},
I want the final aggregated result like that:
[
{
"questionNum": 2,
"questionName": "Which is the best selling record in history ?",
"correct": 10,
"incorrect": 20,
"totalAnswers": 30,
"answers": [
{
"answerId": "123abc",
"answerName": "Thriller Michel Jackson",
"numResponses": 10
},
{
"answerId": "234d",
"answerName": "A kind of Magic Queen",
"numResponses": 10
},
{
"answerId": "432e",
"answerName": "help The Beatles",
"numResponses": 10
}
]
},
{
"questionNum": 1,
"questionName": "What value has the number PI?",
"correct": 5,
"incorrect": 3,
"totalAnswers": 8,
"answers": [
{
"answerId": "111",
"answerName": "3.12",
"numResponses": 0
},
{
"answerId": "222",
"answerName": "3.14",
"numResponses": 5
},
{
"answerId": "333",
"answerName": "3.16",
"numResponses": 3
}
]
}
]
What I tried is :
aggregate([
{ "$match": { "vidId": 8225342, } },
{
"$group": {
"_id": "$questionId",
"Correct": {
"$sum": {
"$cond": [
{ "$eq": ["$isCorrect", true] },
1,
0
]
},
},
"Incorrect": {
"$sum": {
"$cond": [
{ "$eq": ["$isCorrect", false] },
1,
0
]
}
},
}
},
{
"$lookup": {
"from": "quiz",
"let": { "id": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$id", "$questions._id"] } } },
{ "$unwind": "$questions" },
{ "$match": { "$expr": { "$eq": ["$questions._id", "$$id"] } } },
],
"as": "quizData"
}
},
{ $unwind: '$quizData' },
{ "$project": {
"questionName": "$quizData.questions.questionName",
"questionNum": "$quizData.questions.questionNum",
"Correct": "$Correct",
"Incorrect": "$Incorrect",
"answers": "$quizData.questions.answers" } },
])
I got the results something like that:
{
"_id": "611632305bd3910929b95552",
"questionName": "Which is the best selling record in history?",
"questionNum": 5,
"Correct": 3,
"Incorrect": 0,
"answers": [
{
"answerId": "078f441b-373f-40e9-89e1-04fca0a9fc5d",
"answerType": 0,
"_id": "611632305bd3910929b95553",
"answerName": "Thriller Michel Jackson",
"isCorrect": true,
"hotspotId": 470114,
"overlayId": 3,
"panelId": "12abc"
},
{
"answerId": "644b80fe-5778-46fa-b3a6-1eff5989cdee",
"answerType": 0,
"_id": "611632305bd3910929b95554",
"answerName": "A kind of Magic Queen",
"isCorrect": false,
"hotspotId": 470113,
"overlayId": 4,
"panelId": "12345abc"
},
{
"answerId": "5bde2682-66fe-4c79-a728-aea67f6842a8",
"answerType": 0,
"_id": "611632305bd3910929b95555",
"answerName": "help The Beatles",
"isCorrect": false,
"hotspotId": 470112,
"overlayId": 3,
"panelId": "12abc"
}
]
},
how I can get the Answers array like that:
answers: [
{
answerId: "123abc",
answerName: "Thriller Michel Jackson",
numResponses: 10
},
{
answerId: "234d",
answerName: "A kind of Magic Queen",
numResponses: 10
},
{
answerId: "432e",
answerName: "help The Beatles",
numResponses: 10
}
]
         

You can try to approach it the other way around. Use $lookup to get quizResults filtered and aggregated and then run $map along with $filter to get matching statistics for each answer:
db.quiz.aggregate([
{
$match: { "vidId": 8225342 }
},
{
$lookup: {
from: "quizResults",
pipeline: [
{ $match: { "vidId": 8225342 } },
{
$group: {
_id: "$answerId",
count: { $sum: 1 }
}
}
],
as: "quizResults"
}
},
{
$project: {
_id: 1,
questions: {
$map: {
input: "$questions",
as: "q",
in: {
_id: "$$q._id",
questionName: "$$q.questionName",
questionNum: "$$q.questionNum",
answers: {
$map: {
input: "$$q.answers",
as: "a",
in: {
$mergeObjects: [
"$$a",
{
$let: {
vars: {
fst: {
$first: {
$filter: { input: "$quizResults", cond: { $eq: [ "$$this._id", "$$a._id" ] } }
}
}
},
in: { numResponses: "$$fst.count" }
}
}
]
}
}
}
}
}
}
}
}
])
Mongo Playground

Related

How to access nested elements of json object in node js and mongodb

const mongoose = require("mongoose");
const productSchema = mongoose.Schema({
product_name: {
type: String,
required: [true, "Must Enter Product Name"],
},
product_brand: {
type: String,
},
category: {
type: String,
required: [true, "Must Enter Product Catagorey"],
},
reviews: [
{
name: {
type: String,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
comment: {
type: String,
required: true,
},
rating: {
type: Number,
// required: true,
},
},
{ timestamps: true },
],
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
});
module.exports = mongoose.model("Product", productSchema);
Here I want to access the values of commet for Appling filters
Here is is the output of my products
[
{
"_id": "63c4ee520ca7722674d007d5",
"product_name": "Headphones",
"product_description": "xvy",
"product_price": 8000,
"product_brand": "Apple",
"product_color": "White",
"product_stoke": 3,
"category": "Headphones",
"product_image": "public\\images\\uploaded\\products\\167385045002297820433-5f2f-43d1-972a-4b33785393ee.0ad829318c5599f429d2a6e625f8bde3.jpeg",
"product_sku": "#1",
"owner": "63c4ed720ca7722674d007ab",
"reviews": [
{
"name": "Daniyal Alam",
"user": "63c43d54cf582066929c8c46",
"comment": "Good Mobbile Phone",
"rating": 4,
"_id": "63cbce2cd52bf9ecfdf29323"
}
],
"__v": 1
},
{
"_id": "63c4e3530ca7722674d006b6",
"product_name": "Air bud Two.",
"product_description": "In the busy world which is fil.",
"product_price": 50000,
"product_brand": "Apple",
"product_color": "White",
"product_stoke": 5,
"category": "Headphones",
"product_image": "public\\images\\uploaded\\products\\167384763581197820433-5f2f-43d1-972a-4b33785393ee.0ad829318c5599f429d2a6e625f8bde3.jpeg",
"product_sku": "33",
"owner": "63c4d8ac0ca7722674d00529",
"reviews": [],
"__v": 0
},
]
I want to access my comments I tried the following code but it only returns reviews
exports.commentProduct = async (req, res, next) => {
const newproduct = await Product.find({});
try {
const reviews = newproduct?.map((p) => {
if (typeof p.reviews != "undefined") {
console.log(p.reviews);
}
});
const com = reviews?.map((c) => {
// console.log(comments);
});
// const comments = reviews.comment;
return res.status(200).json({
success: true,
reviews,
});
} catch (error) {
return res.status(500).json({
success: false,
message: error.message,
});
}
};
the Console Prints products having reviews but after applying dot notation on reviews I got undefined
[
{
name: 'Daniyal Alam',
user: new ObjectId("63c43d54cf582066929c8c46"),
comment: 'BEst One',
rating: 5,
_id: new ObjectId("63cbce44d52bf9ecfdf29743")
}
]
[]
[]
[]
[]
[]
[]
[]
[]
[
{
name: 'Daniyal Alam',
user: new ObjectId("63c43d54cf582066929c8c46"),
comment: 'pp',
rating: 4,
_id: new ObjectId("63c4f76e0ca7722674d040ae")
}
]
[]
[
{
name: 'Daniyal Alam',
user: new ObjectId("63c43d54cf582066929c8c46"),
comment: 'Good Mobbile Phone',
rating: 4,
_id: new ObjectId("63cbce2cd52bf9ecfdf29323")
}
]
Working with your example, you can map each item, then map the reviews, and get the comment out of each like using the following code
const data = [{
"_id": "63c4ee520ca7722674d007d5",
"product_name": "Headphones",
"product_description": "xvy",
"product_price": 8000,
"product_brand": "Apple",
"product_color": "White",
"product_stoke": 3,
"category": "Headphones",
"product_image": "public\\images\\uploaded\\products\\167385045002297820433-5f2f-43d1-972a-4b33785393ee.0ad829318c5599f429d2a6e625f8bde3.jpeg",
"product_sku": "#1",
"owner": "63c4ed720ca7722674d007ab",
"reviews": [{
"name": "Daniyal Alam",
"user": "63c43d54cf582066929c8c46",
"comment": "Good Mobbile Phone",
"rating": 4,
"_id": "63cbce2cd52bf9ecfdf29323"
}],
"__v": 1
}]
data.map((dat) => {
dat["reviews"].map((rev) => {
document.querySelector('div').innerHTML +=rev["comment"]
})
})
<div></div>

how do i simplify aggregation pipeline result

i am trying to return aggregation result based on user's role and active status
sample doc
{
"email": "doe#gmail.com",
"firstName": "doe",
"lastName": "bghh",
"phone": "+919016703350",
"password":"$.5GIV3q9JqzRqY/lP2",
"status": "verified",
"role": "admin",
"isActive": true
}
const user = await User.aggregate([
{ $project: { isActive: 1, role: 1 } },
{ $group: { _id: { role: '$role', isActive: '$isActive' }, all: { $sum: 1 } } },
])
above query result below
{
"user": [
{
"_id": {
"role": "user",
"isActive": false
},
"all": 1
},
{
"_id": {
"role": "vendor",
"isActive": true
},
"all": 1
},
{
"_id": {
"role": "user",
"isActive": true
},
"all": 2
},
{
"_id": {
"role": "vendor",
"isActive": false
},
"all": 1
},
{
"_id": {
"role": "admin",
"isActive": true
},
"all": 1
}
]
}
expecting result like, help to write query to achieve below result
{ role:{user:{all:3, active:2 }, admin:{all:1, active:1}, vendor:{all:2, active:2}}}
Try this one:
db.collection.aggregate([
{ $project: { isActive: 1, role: 1 } },
{
$group: {
_id: '$role',
all: { $sum: 1 },
active: { $sum: { $cond: ['$isActive', 1, 0] } }
}
},
{
$group: {
_id: null,
role: { $push: "$$ROOT" }
}
},
{
$set: {
role: {
$map: {
input: "$role",
in: { k: "$$this._id", v: { all: "$$this.all", active: "$$this.active" } }
}
}
}
},
{ $set: { role: { $arrayToObject: "$role" } } },
{ $replaceWith: { role: "$role" } }
])
Mongo Playground

Exclude specific fields in all nested document in mongoose query

I have a ride Collection with trips as a field, trips is a map where the keys are different years. I want to query the collection but exclude the passengers field in each trip
const ride = new Schema(
{
boat_operator: {
type: Schema.Types.ObjectId,
required: true,
ref: 'User'
},
trips: {
type: Map,
of: {
passengers: [{ type: Schema.Types.ObjectId, ref: 'User' }],
available_seats: { type: Number, required: true }
},
default: new Map()
}
}
)
I tried this
const rides = await Ride.find({ status: 'waiting' }).select("-trips.*.passengers")
I tried to select all the items in value then remove the corresponding passengers field in each
It had no effect
this is what the response looks like
[
{
"_id": "632a1669279c86f4ab3a4bf5",
"boat_operator": "6328c434a98212a7f57c4edc",
"trips": {
"2019": {
"passengers": [],
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bfe"
},
"2020": {
"passengers": [],
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bfc"
},
"2021": {
"passengers": [],
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bfa"
},
"2022": {
"passengers": [],
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bf8"
}
}
}
]
I want to exclude the passengers field in the returned document
This would solve it
const rides = await Ride.aggregate([
{ "$match": { "status": "waiting" } },
{ $project: { "trips": { $objectToArray: '$trips' } } },
{ $project: { 'trips.v.passengers': 0 } },
{ $project: { "trips": { $arrayToObject: '$trips' } } }
]);
nodejs javascript mongodb mongoose
Here's the returned document
{
"_id": "632a1669279c86f4ab3a4bf5",
"trips": {
"2019": {
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bfe"
},
"2020": {
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bfc"
},
"2021": {
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bfa"
},
"2022": {
"available_seats": 5,
"_id": "632a1669279c86f4ab3a4bf8"
}
}
}

How to merge duplicates in an array of objects and sum a specific property in MongoDB?

I am building an URL shortener. So I want to track datewise click count when a URL is visited using the short URL. For example: on 30th January if the URL was visited 3 times by using the short URL it will show the click count 5 but my document looks like this -
{
"_id":{"$oid":"61f6322cd3e3484d97a25e7c"},
"dayWiseClicks":
[{"date":"01/30/2022","dailyClicks":1,"_id":{"$oid":"61f66ff95fadc3e9f01b0d34"}},
{"date":"01/30/2022","dailyClicks":1,"_id":{"$oid":"61f66ffd5fadc3e9f01b0d38"}},
{"date":"01/30/2022","dailyClicks":1,"_id":{"$oid":"61f66fff5fadc3e9f01b0d3c"}}]
}
I want my document to look like this:
{
"_id":{"$oid":"61f6322cd3e3484d97a25e7c"},
"dayWiseClicks":
[{"date":"01/30/2022","dailyClicks":3,"_id":{"$oid":"61f66ff95fadc3e9f01b0d34"}}]
}
How can I achieve this using the MongoDB aggregation pipeline?
UPDATE
Here's the full schema:
{
merchant_name: String,
store_id: String,
original_url: String,
url_hash: String,
subdomain: String,
// password: String,
totalClicks: {
type: Number,
default: 0,
},
igClicks: {
type: Number,
default: 0,
},
fbClicks: {
type: Number,
default: 0,
},
directClicks: {
type: Number,
default: 0,
},
dayWiseClicks: [
{
date: {
type: String,
default: moment(new Date()).format("L"),
},
dailyClicks: {
type: Number,
default: 0,
},
},
],
desktopClicks: {
device: { type: String, default: "Desktop" },
clicks: { type: Number, default: 0 },
},
mobileClicks: {
device: { type: String, default: "Mobile" },
clicks: { type: Number, default: 0 },
},
},
{ timestamps: true }
After the aggregation is done I want my document to look like this:
{
"_id": {
"$oid": "61f7a3b83dcebb77b05bd180"
},
"desktopClicks": {
"device": "Desktop",
"clicks": 5
},
"mobileClicks": {
"device": "Mobile",
"clicks": 0
},
"totalClicks": 5,
"igClicks": 0,
"fbClicks": 0,
"directClicks": 5,
"dayWiseClicks": [
{
"date": "01/31/2022",
"dailyClicks": 5,
"_id": {
"$oid": "61f7a3fe5bd4f779cc53f697"
}
}
],
"merchant_name": "Akash DTH",
"store_id": "3333",
"url_hash": "OGZhYTI",
"subdomain": "",
"original_url": "https://akashdth.com/",
"createdAt": {
"$date": "2022-01-31T08:54:16.472Z"
},
"updatedAt": {
"$date": "2022-01-31T09:03:55.925Z"
},
"__v": 0
}
try
https://mongoplayground.net/p/osWlBuYy0NZ
db.collection.aggregate([
{
$unwind: {
"path": "$dayWiseClicks"
}
},
{
$group: {
_id: {
"oid": "$_id",
"date": "$dayWiseClicks.date"
},
"dailyClicks": {
$sum: "$dayWiseClicks.dailyClicks"
},
"originalDayWiseClicks": {
$push: "$dayWiseClicks"
}
}
},
{
"$addFields": {
"dayWiseClicks": [
{
"date": "$_id.date",
"dailyClicks": "$dailyClicks",
"_id": {
"$first": "$originalDayWiseClicks._id"
}
}
]
}
},
{
"$project": {
_id: "$_id.oid",
"dayWiseClicks": 1
}
}
])
If you don't need the dayWiseClicks as nested or the first dayWiseClicks._id
It can be simplified to this:
https://mongoplayground.net/p/Zc9whjiLWdt
db.collection.aggregate([
{
$unwind: {
"path": "$dayWiseClicks"
}
},
{
$group: {
_id: {
"oid": "$_id",
"date": "$dayWiseClicks.date"
},
"dailyClicks": {
$sum: "$dayWiseClicks.dailyClicks"
}
}
},
{
$addFields: {
"_id": "$_id.oid",
"date": "$_id.date",
}
}
])

Mongoose update subdocument by key

for the following collection:
id: Number
name:[
new Schema({
language: { type: String, enum: ['en-US', 'fr-CA'] },
text: String,
},{ _id: false }
);
],
isActive: Boolean
and sample data like the following:
{
id:1,
"name": [
{"language": "en-US", "text": "Book"},
{"language": "fr-CA", "text": "livre"}
],
isActive:true
// and so many other fields
},
{
id:2,
"name": [
{"language": "en-US", "text": "Pen"}
],
isActive:true
// and so many other fields
}
I would like to update the document by Id:1 and change the text only for french, I tried by:
const input={ "id":"1", "isActive": false}
const name= { "name": [{"language": "fr-CA", "text": "Nouvel article"}]}
await langs.findByIdAndUpdate(
{ _id: input.id },
{ ...input, $addToSet: { name } },
{ new: true, upsert: true }
);
but the result is: (added another french item)
{
id:1,
"name": [
{"language": "en-US", "text": "Book"},
{"language": "fr-CA", "text": "livre"},
{"language": "fr-CA", "text": "Nouvel article"}
],
isActive:false
},
This is based on Brit comment:
https://mongoplayground.net/p/atlw5ZKoYiI
Please advise.
As long as that attribute already exists on the document, you can do something like:
Collection.findOneAndUpdate({id: 1}, {
$set: {
"name.$[elem]": name
}
},
{
arrayFilters: [ { "elem.language": name.language } ],
upsert: true,
new: true
})

Categories