using "or" operator when using .populate on a query? - node.js

I have my invoices model which has a "sales" field ,which is array of sales. In my sales model I have item field which either can come from Vehicles schema or AfterSale schema.
export const Sales = new Schema<ISale>(
{
customer: { type: Schema.Types.ObjectId, ref: "Customer", required: true },
category: { type: String, enum: enu, required: true },
item: {
type: Schema.Types.ObjectId,
required: true,
ref: (doc) => doc.category,
},
total: { type: Number, required: true },
discount: { type: Number },
purchaseDate: { type: Date, required: true },
branch: { type: Schema.Types.ObjectId, ref: "Branch", required: true },
},
{ timestamps: true },
);
I want to populate the sales.item on my invoice schema, but the problem is , since item can reference to multiple schemas, I can only populate one of them.
async findAll(req: any, query: SharedPreferences): Promise<InvoiceClass[]> {
const invoices = this.invoiceModel
.find(req.searchObj)
.sort({ [query.sort]: query.orderBy === "desc" ? -1 : 1 })
.populate([
"customer",
"sales",
{
path: "sales",
populate: [
{
path: "customer",
model: "Customer",
},
{
path: "branch",
model: "Branch",
},
{
path: "item",
model: "Vehicle",
},
{
path: "item",
model: "AfterSale", //in this case only AfterSale will be populated since
// its last and has same path
}
],
},
]);
my perfect solution would have been using the or operator , but that doesnt work
async findAll(req: any, query: SharedPreferences): Promise<InvoiceClass[]> {
const invoices = this.invoiceModel
.find(req.searchObj)
.sort({ [query.sort]: query.orderBy === "desc" ? -1 : 1 })
.populate([
"customer",
"sales",
{
path: "sales",
populate: [
{
path: "customer",
model: "Customer",
},
{
path: "branch",
model: "Branch",
},
{
path: "item",
model: { $or: ["Vehicle", "AfterSale"] },
},
],
},
]);
any reference/help will be appreciated, thanks.

Related

Mongoose calculating average from another collection

I am new to MongoDB/Mongoose. I am trying to do a search based on a key string for a 'Resource' which will return a list of resources based on average of ratings for that resource. I am having a hard time calculating and returning the average. This is my schema.
Resource Schema:
const ResourceSchema = mongoose.Schema({
title: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
createdDate: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model("Resource", ResourceSchema);
Rating Schema:
const RatingSchema = mongoose.Schema({
resourceId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Resource",
},
createdDate: {
type: Date,
default: Date.now,
},
rating: {
type: Number,
required: true,
min: 1,
max: 5,
},
review: {
type: String,
required: true,
},
});
module.exports = mongoose.model("Rating", RatingSchema);
Each Resource will have multiple Ratings. I am trying to calculate the average of ratings in my list of fetched Resources and add it to the search results.
This is what I have for my search:
Resource.find({
$or: [
{ title: { $regex: req.params.searchStr.toLowerCase(), $options: "i" } },
{ url: { $regex: req.params.searchStr.toLowerCase(), $options: "i" } },
],
})
Here's one way you could do it.
db.resources.aggregate([
{ // filter resources
"$match": {
"title": {
"$regex": "passenger",
"$options": "i"
},
"url": {
"$regex": "https",
"$options": "i"
}
}
},
{ // get ratings for resource
"$lookup": {
"from": "ratings",
"localField": "_id",
"foreignField": "resourceId",
"pipeline": [
{
"$project": {
"_id": 0,
"rating": 1
}
}
],
"as": "ratings"
}
},
{ // calculate average
"$set": {
"avgRating": { "$avg": "$ratings.rating" }
}
},
{ // don't need ratings array anymore
"$unset": "ratings"
}
])
Try it on mongoplayground.net.

populate a field in aggregation result

const commentSchema = new mongoose.Schema({
name: {
type: String,
default: "anonymous",
},
comment: {
type: String,
required: true,
},
post: {
type: mongoose.Schema.Types.ObjectId,
ref: "Post",
required: true,
},
}, {
timestamps: true
});
const trending = await Comment.aggregate([{
$group: {
_id: "$post",
total: {
$sum: 1
},
},
}, ]);
Result of aggregation
[{
"_id": "61eb55808551961dc737c00c",
"total": 5
},
{
"_id": "61eb4a490a894ac62bd833ab",
"total": 2
}
]
hello, please is there a way i can populate the field _id after using aggregation, i want to get all the fields of post not just the _id?

Getting items that referenced an object id

// PRODUCT MODEL
const productSchema = new mongoose.Schema(
{
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category",
},
name: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
default: 0,
},
colors: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Color",
},
],
sizes: {
type: Array,
default: [
{ id: 1, size: "XS" },
{ id: 2, size: "S" },
{ id: 3, size: "M" },
{ id: 4, size: "L" },
{ id: 5, size: "XL" },
{ id: 6, size: "XXL" },
],
},
},
{ timestamps: true }
);
const Product = mongoose.model("Product", productSchema);
export default Product;
// CATEGORY MODEL
const CategorySchema = new mongoose.Schema(
{
name: String,
},
{ timestamps: true, toJSON: true, toObject: true }
);
CategorySchema.virtual("items").get(async function () {
return await CategorySchema.aggregate([
{
$lookup: {
from: "Product",
localField: "category",
foreignField: "_id",
as: "items",
},
},
]);
});
const Category = mongoose.model("Category", CategorySchema);
export default Category;
Hello, please i'm trying to get list of items that referenced each categoryid on the product model, i want to add the result as a virtual field called "count" from the category model and not the product model. i'm getting this error "TypeError: Cannot use 'in' operator to search for 'transform' in true".
i want to get the result the way it was done in this example from mongodb doc about inventory and orders but i'm getting "items: {}". docs.mongodb.com/manual/reference/operator/aggregation/lookup
router.get(
"/counts",
catchAsync(async (req, res) => {
const stats = await Category.aggregate([
{
$lookup: {
from: "products",
localField: "_id",
foreignField: "category",
as: "count",
},
},
{
$project: {
name: 1,
image: 1,
count: {
$cond: {
if: { $isArray: "$count" },
then: { $size: "$count" },
else: "NA",
},
},
},
},
]);
if (!stats) {
return res.status(404).json("No data found");
}
res.status(200).json(stats);
})
);
For anyone that is trying to achieve the result i wanted, i used Category.aggregate on the category route instead of creating a virtual field from the category model.

How to return count array of subdocuments mongoose

I want to get the total amount of comments from Place models and I couldn't find a way to get it because I don't want to populate the comment with GET /places.
This is my place model
const placeSchema = mongoose.Schema(
{
type: { type: String },
english: { type: String },
province_code: { type: String },
district_code: { type: String, required: true },
commune_code: { type: String },
village_code: { type: String },
lat: { type: Number },
lon: { type: Number },
body: { type: String },
images: [{ type: String }],
comments: [{ type: mongoose.Types.ObjectId, ref: Comment }],
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}
)
I use this query
data = await Place
.find()
.limit(5)
.skip(0)
.populate('comments')
.exec()
I want to get the response like this
{
"data": [
{
"images": [],
"comments": 6,
"type": "place",
"english": "99Boko",
"province_code": "23",
"district_code": "2302",
"commune_code": "230202",
"village_code": "23020202",
"lat": 135.2039,
"lon": 104.01734762756038,
"body": "<b>This place is really good</b>",
"created_at": "2021-07-20T17:41:52.202Z",
"updated_at": "2021-07-20T17:41:52.202Z",
"id": "60f70ae08e54941530d14c4c"
},
]}
Does anybody know the solution to get this kind of response ?
I have figured out to get the comment length is to use virtual count
placeSchema.virtual('comment_length', {
ref: Comment, // model to use for matching
localField: 'comments', // from `localField` i.e., Place
foreignField: '_id', // is equal to `foreignField` of Comment schema
count: true, //only get the number of docs
})
placeSchema.set('toObject', { virtuals: true })
placeSchema.set('toJSON', { virtuals: true })
and use this query
data = await Place
.find().populate({ path: 'comment_length', count: true })
.exec()

Mongoose sub field aggregation with full text search and project

I have a Mongoose model called Session with a field named course (Course model) and I want to perform full text search on sessions with full text search, also I wanna aggregate results using fields from course sub field and to select some fields like course, date, etc.
I tried the following:
Session.aggregate(
[
{
$match: { $text: { $search: 'web' } }
},
{ $unwind: '$course' },
{
$project: {
course: '$course',
date: '$date',
address: '$address',
available: '$available'
}
},
{
$group: {
_id: { title: '$course.title', category: '$course.courseCategory', language: '$course.language' }
}
}
],
function(err, result) {
if (err) {
console.error(err);
} else {
Session.deepPopulate(result, 'course course.trainer
course.courseCategory', function(err, sessions) {
res.json(sessions);
});
}
}
);
My models:
Session
schema = new mongoose.Schema(
{
date: {
type: Date,
required: true
},
course: {
type: mongoose.Schema.Types.ObjectId,
ref: 'course',
required: true
},
palnning: {
type: [Schedule]
},
attachments: {
type: [Attachment]
},
topics: {
type: [Topic]
},
trainer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'trainer'
},
trainingCompany: {
type: mongoose.Schema.Types.ObjectId,
ref: 'training-company'
},
address: {
type: Address
},
quizzes: {
type: [mongoose.Schema.Types.ObjectId],
ref: 'quiz'
},
path: {
type: String
},
limitPlaces: {
type: Number
},
status: {
type: String
},
available: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: new Date()
},
updatedAt: {
type: Date
}
},
{
versionKey: false
}
);
Course
let schema = new mongoose.Schema(
{
title: {
type: String,
required: true
},
description: {
type: String
},
shortDescription: {
type: String
},
duration: {
type: Duration
},
slug: {
type: String
},
slugs: {
type: [String]
},
program: {
content: {
type: String
},
file: {
type: String
}
},
audience: [String],
requirements: [String],
language: {
type: String,
enum: languages
},
price: {
type: Number
},
sections: [Section],
attachments: {
type: [Attachment]
},
tags: [String],
courseCategory: {
type: mongoose.Schema.Types.ObjectId,
ref: 'course-category',
required: true
},
trainer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'trainer'
},
trainingCompany: {
type: mongoose.Schema.Types.ObjectId,
ref: 'training-company'
},
status: {
type: String,
default: 'draft',
enum: courseStatus
},
path: {
type: String
},
cover: {
type: String,
required: true
},
duration: {
type: Number,
min: 1
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date
}
},
{ versionKey: false }
);
I am not sure if what I tried is gonna bring me what I want and I am getting this error concerning the $unwind operator:
MongoError: exception: Value at end of $unwind field path '$course'
must be an Array, but is a OID
Any kind of help will be really appreciated.
You can try below aggregation.
You are missing $lookup required to pull course document by joining on course object id from session document to id in the course document.
$project stage to keep the desired fields in the output.
Session.aggregate([
{
"$match": {
"$text": {
"$search": "web"
}
}
},
{
"$lookup": {
"from": "courses",
"localField": "course",
"foreignField": "_id",
"as": "course"
}
},
{
"$project": {
"course": 1,
"date": 1,
"address": 1,
"available": 1
}
}
])
Course is an array with one course document. You can use the $arrayElemAt to project the document.
"course": {"$arrayElemAt":["$course", 0]}

Resources