mongoose move ref document to main document as DTO - node.js

I wanna to move sub document to main documents, and return single DTO without any nested document, below is my sample data.js
data.js
const mongoose = require('mongoose');
//city
const citySchema = new mongoose.Schema({
cityName: { type: String, required: true, unique: true },
});
const City = mongoose.model('City', citySchema);
//country
const countrySchema = new mongoose.Schema({
countryName: { type: String, required: true, unique: true },
});
const Country = mongoose.model('Country', countrySchema);
//user
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
city: {
type: mongoose.Schema.Types.ObjectId,
ref: 'City',
required: true,
},
country: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Country',
required: true,
},
createdAt: { type: Date, required: true },
updatedAt: { type: Date, required: true },
});
const User = mongoose.model('User', userSchema);
function getUser(id) {
return User.findById(id)
.populate('city')
.populate('country')
.exec();
};
Current return JSON Response for User:
{
"_id": "6321ac3d14a57c2716f7f4a0",
"name": "David",
"city": {
"_id": "63218ce557336b03540c9ce9",
"cityName": "New York",
"__v": 0
},
"country": {
"_id": "632185bbe499d5505cafdcbc",
"countryName": "USA",
"__v": 0
},
"createdAt": "2022-09-14T10:26:05.000Z",
"__v": 0
}
How do I move the cityName and countryName to main model, and response JSON as below format?
{
"_id": "6321ac3d14a57c2716f7f4a0",
"username": "David",
"cityName": "New York",
"countryName": "USA",
"createdAt": "2022-09-14T10:26:05.000Z",
}

Using aggregation you can try something like this:
db.user.aggregate([
{
"$match": {
_id: "6321ac3d14a57c2716f7f4a0"
}
},
{
"$lookup": {
"from": "city",
"localField": "city",
"foreignField": "_id",
"as": "city"
}
},
{
"$lookup": {
"from": "country",
"localField": "country",
"foreignField": "_id",
"as": "country"
}
},
{
"$addFields": {
"country": {
"$arrayElemAt": [
"$country",
0
]
},
"city": {
"$arrayElemAt": [
"$city",
0
]
}
}
},
{
"$addFields": {
"countryName": "$country.countryName",
"cityName": "$city.cityName"
}
},
{
"$unset": [
"country",
"city"
]
}
])
Here's the playground link.
The other probably simpler way of doing this would be, modify your function like this:
function getUser(id) {
const user = User.findById(id)
.populate('city')
.populate('country')
.exec();
if(user.country) {
user.countryName = user.country.countryName;
}
if(user.city) {
user.cityName = user.city.cityName;
}
delete user.country;
delete user.city;
return user
};

Related

populate collection is returning an empty array in mongoose using express

This is my how my Like model schema looks like.
//create likes schema
const likes = mongoose.Schema({
liked: {
type: Boolean,
default: false
},
tweet: {
type: Schema.Types.ObjectId,
ref: "Tweet"
},
author: {
type: Schema.Types.ObjectId,
ref: "User"
}
});
module.exports = mongoose.model('Like', likes);
and this is my an overview of my Tweet Schema:
const tweets = mongoose.Schema({
content: {
type: String,
required: true,
},
author: {
type: Schema.Types.ObjectId,
ref: "User"
},
likes: [{
type: Schema.Types.ObjectId,
ref: "Like"
}]
});
module.exports = mongoose.model('Tweet', tweets);
I am testing on based on the following data from
const likes = await Like.find().populate("author", "_id name email").populate("tweet", "_id content").exec()
res.json(likes)
[
{
"_id": "63921e53deb31c60249901e4",
"liked": true,
"tweet": {
"_id": "63921e50deb31c60249901e1",
"content": "tweet 1"
},
"author": {
"_id": "63921e2ddeb31c60249901dd",
"name": "Dave",
"email": "something#gmail.com"
},
"createdAt": "2022-12-08T17:26:43.650Z",
"updatedAt": "2022-12-08T17:26:43.650Z",
"__v": 0
}
]
And this is how I am using the populate method to fetch the likes of a tweet.
const tweets = await Tweet.find()
.populate("author", "_id name email")
.populate("likes", "_id")
.sort({updatedAt: "desc"})
.exec()
res.status(200).json(tweets)
but I am getting an empty array in likes collection (of objects).
[
{
"_id": "6393701aa62997f3454e81e1",
"content": "My tweet",
"author": "63936ffaa62997f3454e81dd",
"likes": [],
"createdAt": "2022-12-09T17:27:54.146Z",
"updatedAt": "2022-12-09T17:27:54.146Z",
"__v": 0
}
]
Followed this documentation
this is the data from likes schema
[
{
"_id": "63937140df6222756bd84ede",
"liked": true,
"tweet": {
"_id": "6393701aa62997f3454e81e1",
"content": "My tweet"
},
"author": {
"_id": "63936ffaa62997f3454e81dd",
"name": "Dave",
"email": "admin#gmail.com"
},
"createdAt": "2022-12-09T17:32:48.251Z",
"updatedAt": "2022-12-09T17:32:48.251Z",
"__v": 0
}
]
As i understand, you want to populate nested objects
Mongoose populate syntax is:
populate({ path: 'refKey'}).
I would suggest you use aggregate.
const tweets = await Tweet.aggregate([
{
$lookup: {
from: "user",//your schema name in mongoose db
localField: "author",//field name from Tweet which contains the id of user(auther)
foreignField: "_id",//_id of user(auther) model
pipeline: [
{
$project: {
"_id": 1,
"name": 1,
"email": 1,
}
}
],
as: "author"
}
},
{
$lookup: {
from: "likes",//your schema name in mongoose db
localField: "likes",// or likes._id
foreignField: "_id",
pipeline: [
{
$project: {
"_id": 1,
}
}
],
as: "likes"
}
},
{
$sort: {
updatedAt: -1
}
}
])

$lookup with condition in mongoose

I have 2 schemas, this is parent collection schema:
const TimesheetSchema = Schema({
managersComment: {
type: String,
},
weekNum: {
type: Number,
},
year: {
type: Number,
},
user: { type: Schema.Types.ObjectId, ref: userModel },
status: {
type: String,
enum: ["Saved", "Submitted", "Approved", "Rejected"],
},
data: [{ type: Schema.Types.ObjectId, ref: TimesheetIndividualData }]
});
This is child collection schema
const TimesheetDataSchema = new Schema(
{
workingDate: {
type: Date,
},
dayVal: {
type: Number,
},
user: { type: Schema.Types.ObjectId, ref: userModel },
parentId: { type: String }
},
{ timestamps: true }
);
In TimesheetDataSchema parentId is basically the _id from TimesheetSchema.
Now i need to run a query which return docs from TimesheetDataSchema, but only the docs in which parentId(ObjectId) of TimesheetSchema has status Approved.
I am trying to do $lookup, but currently no success. Please help.
EDIT: Based upon #ashh suggestion tried this: but getting empty array.
const result = await TimesheetIndividualData.aggregate([
{
"$lookup": {
"from": "timesheetModel",
"let": { "parentId": "$parentId" },
"pipeline": [
{ "$match": { "status": "Approved", "$expr": { "$eq": ["$weekNum", "$parentId"] } } },
],
"as": "timesheet"
}
},
{ "$match": { "timesheet": { "$ne": [] } } }
])
You can use below aggregation
const result = await db.TimesheetDataSchema.aggregate([
{ "$lookup": {
"from": "TimesheetSchema",
"let": { "parentId": "$parentId" },
"pipeline": [
{ "$match": { "status": "approved", "$expr": { "$eq": ["$_id", "$$parentId"] }}},
],
"as": "timesheet"
}},
{ "$match": { "timesheet": { "$ne": [] }} }
])
But I would prefer two queries for better performance here
const timesheets = (await db.TimesheetSchema.find({ status: "approved" }, { _id: 1 })).map(({ _id }) => _id)
const result = await db.TimesheetDataSchema.find({ parentId: { $in: timesheets } })

aggregate nested array of objects using mongoose

I have the following model and I want to query a specific user on _id field and populate the inbox.messages array with the necessary data that matches the corresponding _id field in the users model and more importantly i also want to group each message by the 'from' field and return that result
const UserSchema = new Schema({
username: {
type: String,
required: true,
},
blockedUsers: {
users: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
}
]
},
favorites: {
users: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
}
]
},
profileViews: {
views: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
date: {type: Date}
}
]
},
inbox: {
messages: [
{
messageId: {type: Schema.Types.ObjectId},
from: {type: Schema.Types.ObjectId, ref: 'User', required: true },
content: {type: String, required: true},
date: {type: Date}
}
]
},
images: {
"imagePaths": [
{
imageId: {type: Schema.Types.ObjectId},
path: { type: String, required: true},
date: {type: Date}
}
],
}
})
what I have so far
let incomingId = '5e29fd75fdfd5320d0e42bc4';
let myUser = await User.aggregate([
{ $match: {"_id": mongoose.Types.ObjectId(incomingId) }},
{ $lookup: { }}
])
Not sure exactly what to put in the $lookup field or if this is even correct.
As a sample I would like the documents to look like:
[
{
"from": "5e240f7480a24e07d832c7bd",
"username":"hable0",
"images": {
imagePaths: [
'images/2020-09-24-Z_34234342_12.jpg'
],
},
"inbox": {
"messages": [
{
"messageId": "5e2a110a21c64d63f451e39e",
"content": "Message content",
"date": "2020-01-23T21:32:58.126Z"
},
{
"messageId": "5e2a111321c64d63f451e3a0",
"content": "Message content",
"date": "2020-01-23T21:33:07.378Z"
},
{
"messageId": "5e2a112321c64d63f451e3a2",
"content": "Message content",
"date": "2020-01-23T21:33:23.036Z"
}
]
}
}
]
You could try the following pipeline with aggregate().
Find the document that matches the id
Unwind inbox.messages
Group by from field
Perform a $lookup to get another document
Perform a $unwind to destruct the array
Specify fields to be included in the output
let myUser = await User.aggregate([
{
$match: { "_id": mongoose.Types.ObjectId(incomingId) }
},
{
$unwind: "$inbox.messages"
},
{
$group: {
_id: { from: "$inbox.messages.from" },
messages: {
$push: {
messageId: "$inbox.messages.messageId"
// Add more info of the message here as needed
}
}
},
},
{
$lookup: {
from: "User",
localField: "_id.from",
foreignField: "_id",
as: "extraUserInfo"
}
},
{
$unwind: "$extraUserInfo"
},
{
$project: {
_id: 0,
from: "$_id.from",
inbox: { messages: "$messages" },
username: "$extraUserInfo.username",
images: "$extraUserInfo.images"
}
}
]);
Sample output:
{
"from": "user1",
"inbox": {
"messages": [{
"messageId": "message1-from-user1"
}]
},
"username": "user1-username",
"images": {
"imagePaths": ["image-path-user1"]
}
} {
"from": "user2",
"inbox": {
"messages": [{
"messageId": "message1-from-user2"
}, {
"messageId": "message2-from-user2"
}, {
"messageId": "message3-from-user2"
}]
},
"username": "user2-username",
"images": {
"imagePaths": ["image-path-user2"]
}
} {
"from": "user3",
"inbox": {
"messages": [{
"messageId": "message1-from-user3"
}, {
"messageId": "message2-from-user3"
}]
},
"username": "user3-username",
"images": {
"imagePaths": ["image-path-user3"]
}
}
Hope this answers part of your question. Though I'm not very clear how you would like to populate the messages array with the user info who sent the messages. But you can perform a $lookup() with a pipeline after $group() operation to attach additional info from the sender to the result.
Read more about $unwind, $group, $project and $lookup.

fetch 3 schema value using mongoose node JS

I have 3 collection schema CategorySchema, SubCategorySchema, ProductSchema like below.
var CategorySchema = new mongoose.Schema({
catgory_name: {
type: String,
required: [true, "Catgory name is required"]
},
modified_date: {
type: Date
}
});
module.exports = mongoose.model("Category", CategorySchema);
var SubCategorySchema = new Schema({
subcatgory_name: {
type: String,
required: [true, "subcategory name is required"]
},
category_id: {
type: Schema.Types.ObjectId,
ref: "Category",
required: [true, "category id is required"]
},
modified_date: {
type: Date
},
is_active: {
type: Boolean,
default: 1
}
});
module.exports = mongoose.model("SubCategories", SubCategorySchema);
const ProductSchema = new Schema({
product_name: {
type: String,
required: [true, "Product name is required"]
},
product_image: {
type: String,
required: [true, "Product image is required"]
},
category_id: {
type: Schema.Types.ObjectId,
ref: "Category",
required: [true, "category is required"]
},
subcategory_id: {
type: Schema.Types.ObjectId,
ref: "Subcategory",
required: [true, "Subcategory is required"]
},
modified_date: {
type: Date
},
is_active: {
type: Boolean,
default: 1
}
});
module.exports = mongoose.model("Products", ProductSchema);
Here i want to take all the active products (is_active = 1) with the corresponding categories and active subcategories (is_active = 1). No need to check is_active condition for categories but need to check active condition for subcategories and products
I tried with the below code in node JS controller
router.get("/list", (req, res, next) => {
products
.find({ is_active: true })
.populate("category_id")
.populate("subcategory_id", null, SubCategory, {
match: { is_active: true }
})
//.where("subcategory_id", !null)
.then(products => res.json({ status: 200, data: products }))
.catch(err => res.json(err));
});
But even subcategories are inactive it returns the product data
You can query using mongodb aggregation framework still using mongoose.
router.get("/list", (req, res, next) => {
products
.aggregate([
{
$match: {
is_active: true
}
},
{
$lookup: {
from: "subcategories",
localField: "subcategory_id",
foreignField: "_id",
as: "subcategories"
}
},
{
$unwind: "$subcategories"
},
{
$match: {
"subcategories.is_active": true
}
},
{
$lookup: {
from: "categories",
localField: "category_id",
foreignField: "_id",
as: "category"
}
},
{
$addFields: {
category: {
$arrayElemAt: ["$category", 0]
}
}
}
])
.then(products => res.json({ status: 200, data: products }))
.catch(err => res.status(500).json(err));
});
Playground
Let's have these sample documents:
db={
"products": [
{
"modified_date": "2020-01-08T09:06:51.544Z",
"is_active": true,
"_id": "5e159ca1bd95457404b22bc3",
"product_name": "Product1 Name",
"product_image": "Product1 Image",
"category_id": "5e159b77a746036404b5f0ae",
"subcategory_id": "5e159befbd95457404b22bc2"
},
{
"modified_date": "2020-01-08T09:06:51.544Z",
"is_active": false,
"_id": "5e159cb8bd95457404b22bc4",
"product_name": "Product2 Name",
"product_image": "Product2 Image",
"category_id": "5e159b77a746036404b5f0ae",
"subcategory_id": "5e159befbd95457404b22bc2"
},
{
"modified_date": "2020-01-08T09:06:51.544Z",
"is_active": true,
"_id": "5e159d3abd95457404b22bc6",
"product_name": "Product3 Name",
"product_image": "Product3 Image",
"category_id": "5e159b77a746036404b5f0ae",
"subcategory_id": "5e159ce0bd95457404b22bc5"
}
],
"categories": [
{
"modified_date": "2020-01-08T09:04:18.003Z",
"_id": "5e159b77a746036404b5f0ae",
"catgory_name": "Main Category 1"
}
],
"subcategories": [
{
"modified_date": "2020-01-08T09:06:51.544Z",
"is_active": true,
"_id": "5e159befbd95457404b22bc2",
"subcatgory_name": "Sub Category 1",
"category_id": "5e159b77a746036404b5f0ae"
},
{
"modified_date": "2020-01-08T09:06:51.544Z",
"is_active": false,
"_id": "5e159ce0bd95457404b22bc5",
"subcatgory_name": "Sub Category 2",
"category_id": "5e159b77a746036404b5f0ae"
}
]
}
The result will be:
[
{
"_id": "5e159ca1bd95457404b22bc3",
"category": {
"_id": "5e159b77a746036404b5f0ae",
"catgory_name": "Main Category 1",
"modified_date": "2020-01-08T09:04:18.003Z"
},
"category_id": "5e159b77a746036404b5f0ae",
"is_active": true,
"modified_date": "2020-01-08T09:06:51.544Z",
"product_image": "Product1 Image",
"product_name": "Product1 Name",
"subcategories": {
"_id": "5e159befbd95457404b22bc2",
"category_id": "5e159b77a746036404b5f0ae",
"is_active": true,
"modified_date": "2020-01-08T09:06:51.544Z",
"subcatgory_name": "Sub Category 1"
},
"subcategory_id": "5e159befbd95457404b22bc2"
}
]
As you see, even the Product 3 is active, it hasn't been retrieved because its subcategory 5e159ce0bd95457404b22bc5 is not active.

mongoose get count of relation with condition

i have two schema
vehicle schema :
const VehicleSchema = new Schema({
title: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
);
VehicleSchema.virtual('booking', {
ref: 'Booking',
localField: '_id',
foreignField: 'vehicle',
options: {sort: {created_at: 1}}
});
export default mongoose.model('Vehicle', VehicleSchema);
Booking Schema :
const BookingSchema = new Schema({
start_at:{
type:Date,
required:true
},
end_at:{
type:Date,
required:true
},
status: {
type: String,
enum: ["APPROVED", "REJECTED",],
default: "REJECTED"
},
vehicle:{
type: Schema.Types.ObjectId,
ref: 'Vehicle'
},
});
export default mongoose.model('Booking', BookingSchema);
every vehicle have multi booking
i need to get all Vehicles with counts of rejected and approved status :
[
{
"title":"vehicle_1",
"price":2500,
"rejected_count":10
"approved_count":55
},{
"title":"vehicle_2",
"price":2500,
"rejected_count":15
"approved_count":5
},{
"title":"vehicle_3",
"price":2500,
"rejected_count":1
"approved_count":30
},{
"title":"vehicle_4",
"price":2500,
"rejected_count":5
"approved_count":15
},
]
You can use below aggregation
Vehicle.aggregate([
{ "$lookup": {
"from": Booking.collection.name,
"let": { "vehicle": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$vehicle", "$$vehicle" ] },
"status": "APPROVED"
}}
],
"as": "approved"
}},
{ "$lookup": {
"from": Booking.collection.name,
"let": { "vehicle": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$vehicle", "$$vehicle" ] },
"status": "REJECTED"
}}
],
"as": "rejected"
}},
{ "$project": {
"rejected_count": { "$size": "$rejected" },
"approved_count": { "$size": "$approved" },
"title": 1,
"price": 1
}}
])

Resources