I have added a virtual environment for reviews but for some reason it doesn't appear in postman when i try to get it. If worked correctly it should display reviews with a value of null. I am fairly new to this but i did read through the mongoose documentation and other online sources but everything seems similar to what i wrote.
const mongoose = require('mongoose');
const slugify = require('slugify');
const tourSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name required'],
unique: true,
maxlength: [40, 'Less then or equal to 40 characters'],
minlength: [10, 'More then or equal to 10 characters'],
},
slug: String,
duration: {
type: Number,
required: [true, 'Duration required'],
},
maxGroupSize: {
type: Number,
required: [true, 'Group size required'],
},
difficulty: {
type: String,
required: [true, 'Difficulty required'],
enum: {
values: ['easy', 'medium', 'difficult'],
message: 'Difficulty is either easy, medium, difficult',
},
},
ratingsAverage: {
type: Number,
default: 4.5,
max: [5, 'Less than or equal to 5'],
min: [1, 'More than or equal to 1'],
},
ratingsQuantity: {
type: Number,
default: 0,
},
price: {
type: Number,
required: [true, 'Price required'],
},
priceDiscount: {
type: Number,
validate: {
validator: function (val) {
// This only points to current doc on New doc creating
return val < this.price ? true : false;
},
message: 'Discount should be less than the regular price',
},
},
summary: {
type: String,
trim: true,
required: [true, 'Summary required'],
},
description: {
type: String,
trim: true,
},
imageCover: {
type: String,
required: [true, 'Image required'],
},
images: [String],
createAt: {
type: Date,
default: Date.now(),
},
startDates: [Date],
secretTour: {
type: Boolean,
default: false,
},
startLocation: {
// GeoJSON (At least 2 field names e.g. type and coordinates)
type: {
type: String,
default: "Point",
enum: ["Point"]
},
coordinates: [Number],
address: String,
description: String,
},
locations: [{
type: {
type: String,
default: "Point",
enum: ["Point"],
},
coordinates: [Number],
address: String,
description: String,
day: Number
}],
guides: [{
type: mongoose.Schema.ObjectId,
ref: "User"
}]
}, {
toJSON: {
virtuals: true
},
toObject: {
virtuals: true
}
});
tourSchema.virtual('durationWeeks').get(function () {
return this.duration / 7;
});
// Virtual populate
tourSchema.virtual('reviews', {
ref: 'Review',
foreignField: 'tour',
localField: '_id',
});
Here is the controller for the tour
const Tour = require('../models/tourModel');
const APIFeatures = require('../utils/apiFeatures');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');
const {
deleteOne,
updateOne,
createOne
} = require("./handlerFactory")
const {
populate
} = require('../models/tourModel');
const aliasTopTours = catchAsync(async (req, res, next) => {
req.query.limit = '5';
req.query.sort = '-ratingAverage,price';
req.query.fields = 'name, price, ratingAverage, summary, difficulty';
next();
});
const getAllTours = catchAsync(async (req, res, next) => {
// Execute query
const features = new APIFeatures(Tour.find(), req.query)
.filter()
.sort()
.limit()
.pagination();
const tours = await features.query;
// Send response
res.status(200).json({
status: 'Success',
length: tours.length,
message: tours,
});
});
const getTour = catchAsync(async (req, res, next) => {
const tourId = await (await Tour.findById(req.params.Id)).populate('reviews');
if (!tourId) {
return next(new AppError('No tour found with that ID', 404));
}
res.status(200).json({
status: 'Success',
data: {
tourId,
},
});
});
const postTour = createOne(Tour)
const patchTour = updateOne(Tour)
const deleteTour = deleteOne(Tour)
const getTourStats = catchAsync(async (req, res, next) => {
const stats = await Tour.aggregate([{
$match: {
ratingAverage: {
$gte: 4.5,
},
},
},
{
$group: {
_id: '$difficulty',
aveRating: {
$avg: '$ratingAverage',
},
avePrice: {
$avg: '$price',
},
minPrice: {
$min: '$price',
},
maxPrice: {
$max: '$price',
},
totalRating: {
$sum: '$ratingQuantity',
},
totalTours: {
$sum: 1,
},
},
},
{
$sort: {
avePrice: 1,
},
},
]);
res.status(200).json({
status: 'Success',
message: stats,
});
});
const getMonthlyPlan = catchAsync(async (req, res, next) => {
const year = req.params.year * 1;
const plan = await Tour.aggregate([{
$unwind: '$startDates',
},
{
$match: {
startDates: {
$gte: new Date(`${year}-01-01`),
$lte: new Date(`${year}-12-31`),
},
},
},
{
$group: {
_id: {
$month: '$startDates',
},
numTourStarts: {
$sum: 1,
},
tours: {
$push: '$name',
},
},
},
{
$addFields: {
month: '$_id',
},
},
{
$project: {
_id: 0,
},
},
{
$sort: {
numTourStarts: -1,
},
},
{
$limit: 12,
},
]);
res.status(200).json({
status: 'Success',
length: plan.length,
message: plan,
});
});
module.exports = {
getAllTours,
getTour,
postTour,
patchTour,
deleteTour,
aliasTopTours,
getTourStats,
getMonthlyPlan,
};
const tourId = await (await Tour.findById(req.params.Id)).populate('reviews');
The problem is here, you need to remove one await here, no need to use 2 await in this line
Related
I'm working on a social media app and I'm having this problem, I have a collection for friends to store friend requests and the relationship between users and everything works fine, but I'm working on a post('save') middleware and aggrgate pipeline to count the number of friends per user per process which also worked But the problem is aggrgate pipeline counting friends before update collection eg if user accepts friend request it counts friends without this new friend.
Is there any solution to this or best way to do it?
Here is the code
const mongoose = require('mongoose');
const User = require('./userModel');
const FriendSchema = new mongoose.Schema({
sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
recipient: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
status: {
type: String,
enum: ['pending', 'accepted', 'cancelled'],
required: true,
},
createdAt: { type: Date, default: Date.now },
});
FriendSchema.statics.calcFriends = async function (senderID, recipientID) {
const senderIDstats = await this.aggregate([
{
$match: {
$or: [
{ sender: senderID, status: 'accepted' },
{ recipient: senderID, status: 'accepted' },
],
},
},
{
$group: {
_id: null,
count: { $sum: 1 },
},
},
]);
const recipientIDstats = await this.aggregate([
{
$match: {
$or: [
{ sender: recipientID, status: 'accepted' },
{ recipient: recipientID, status: 'accepted' },
],
},
},
{
$group: {
_id: null,
count: { $sum: 1 },
},
},
]);
await User.bulkWrite([
{
updateOne: {
filter: { _id: senderID },
update: {
friendsCount: senderIDstats.length > 0 ? senderIDstats[0]?.count : 0,
},
},
},
{
updateOne: {
filter: { _id: recipientID },
update: {
friendsCount:
recipientIDstats.length > 0 ? recipientIDstats[0]?.count : 0,
},
},
},
]);
};
FriendSchema.index({ sender: 1, recipient: 1 }, { unique: true });
FriendSchema.post('save', function () {
this.constructor.calcFriends(this.sender, this.recipient);
});
const Friend = mongoose.model('Friend', FriendSchema);
module.exports = Friend;
How can I add the "userid" (its a string) to the nested array "people_attending" in MongoDB ?
The problem is that I can not add to the array people_attending.
Here is my MongoDB schema:
const OrganizationSchema = new Schema({
name: {
type: String,
required: true,
unique: true,
},
register_date: {
type: Date,
default: Date.now,
},
teams: [
{
sport: {
type: String,
required: false,
},
events: [
{
date_time: {
type: Date,
offset: true,
},
opponent: {
type: String,
required: true,
},
people_attending: [String],
amenities: [String],
},
],
},
],
});
Here is my attempt:
I would like to find the event that equals to the given eventid and add the userid to the people_attending array.
router.put("/event/attend/:orgid/:teamid/:eventid/:userid", (req, res) => {
const userid = req.params.userid;
const eventid = req.params.eventid;
const teamid = req.params.teamid;
const orgid = req.params.orgid;
console.log(teamid, userid, eventid);
Organization.findOneAndUpdate(
{ _id: orgid, "teams._id": teamid, "teams.events._id": eventid },
{
$addToSet: {
"teams.$.events.$.people_attending": userid,
},
}
)
.then((team) => {
res.status(200).json(team);
})
.catch((err) => {
res.status(400).json(err);
});
});
Found the solution:
Organization.findOneAndUpdate(
{ _id: orgid, "teams._id": teamid, "teams.events._id": eventid },
{
$addToSet: {
"teams.$.events.$[].people_attending": userid,
},
},
{ new: true }
I am creating a search API and using the $regex operator of MongoDB to filter from search text. But I also want to add a category inside the $or operator. I have a category Id and this is an id of another collection whenever I want to get the category I have to populate the category id. But I want to use the same $regex operator on the category as given below.
Category Schema
const mongoose = require('mongoose');
const categorySchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
unique: true
},
type: {
type: String
},
categoryImage: {
type: String
},
parentId: {
type: String
}
}, {
timestamps: true
})
module.exports = mongoose.model('Category', categorySchema);
Product Shema
const mongoose = require("mongoose");
const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
slug: {
type: String,
required: true,
unique: true,
},
price: {
type: Number,
required: true,
},
quantity: {
type: Number,
required: true,
},
description: {
type: String,
required: true,
trim: true,
},
offer: {
type: Number,
},
productPictures: [{ img: { type: String } }],
views: {
type: Number,
min: 0,
default: 0,
required: true
},
ratings: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
rate: {
type: Number,
required: true,
max: 5,
},
reviewTitle: {
type: String,
},
review: {
type: String,
},
reviewImages: [
{
img: String,
},
],
date: {
type: Date,
required: true,
}
},
],
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category",
required: true,
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
},
{
timestamps: true,
}
);
module.exports = mongoose.model("Product", productSchema);
Search Product Controller
exports.searchProducts = async (req, res) => {
try {
const { page = 1, limit = 20 } = req.query;
const pagination = {
skip: (Number(page) - 1) * Number(limit),
limit: Number(limit),
};
const search = req?.query?.s?.split("+").join("") || "";
const fields = req?.query?.fields?.split(",").join(" ");
const sort = req?.query?.sort?.split(",")?.join(" ");
const { products, matchCount, pageNeeded, showingFrom, showingTo } =
await searchProductsService(search, fields, sort, pagination);
if (products.length === 0) {
return res.status(400).json({ error: "No products found" });
}
res
.status(200)
.json({ products, matchCount, pageNeeded, showingFrom, showingTo });
} catch (error) {
return res.status(400).json({ error: "Something Wen't wrong" });
}
};
Search Product Service (Where I am doing database opearations)
exports.searchProductsService = async (search, fields, sort, pagination) => {
await SearchQuery.updateOne(
{ title: search },
{ $inc: { searchedCount: 1 } },
{ upsert: true }
);
const query = {
$or: [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
],
};
const products = await Product.find(query)
.skip(pagination.skip)
.limit(pagination.limit)
.select(fields || "name productPictures price")
.sort(sort);
const matchCount = await Product.find(query).count();
const pageNeeded = Math.ceil(matchCount / pagination.limit);
const showingFrom = pagination.skip + 1;
const showingTo = pagination.skip + products.length;
return { products, matchCount, pageNeeded, showingFrom, showingTo };
};
I want to get a boolean value eg. true or false if a location coordinate is within the given radius is the $geoWithin query or $geoNear pipeline in mongoose aggregate function. If I user $geoNear in the mongoose aggregate pipeline then it only returns the filtered result. So far I have done the following,
The Model:
import * as mongoose from 'mongoose';
import { User } from 'src/user/user.model';
export const BlipSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
media: [
new mongoose.Schema(
{
fileName: String,
mediaType: {
type: String,
enum: ['image', 'video'],
},
},
{
toJSON: {
transform: function (doc, ret) {
delete ret._id;
},
},
},
),
],
likesCount: {
type: Number,
default: 0,
},
commentsCount: {
type: Number,
default: 0,
},
isLiked: Boolean,
status: {
type: String,
enum: ['public', 'private'],
default: 'public',
},
location: {
type: {
type: String,
default: 'Point',
enum: ['Point'],
},
coordinates: [Number],
address: String,
description: String,
},
lat: Number,
lng: Number,
address: String,
city: {
type: String,
default: '',
},
createdAt: {
type: Date,
default: Date.now,
},
},
{
toJSON: {
transform: function (doc, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.location;
delete ret.__v;
},
},
toObject: { virtuals: true },
},
);
export interface Media {
fileName: string;
mediaType: string;
}
export interface Location {
type: string;
coordinates: [number];
address: string;
description: string;
}
export interface Blip {
user: User;
media: [Media];
likesCount: number;
commentsCount: number;
isLiked: boolean;
status: string;
lat: number;
lng: number;
address: string;
city: string;
createdAt: string;
}
The function in my controller:
async getLocationBlips(user: User, query: any) {
const aggrea = [];
aggrea.push(
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user',
},
},
{
$unwind: '$user',
},
{ $set: { 'user.id': '$user._id' } },
{
$group: {
_id: { lat: '$lat', lng: '$lng' },
lat: { $first: '$lat' },
lng: { $first: '$lng' },
isViewable: { $first: false },
totalBlips: { $sum: 1 },
blips: {
$push: {
id: '$_id',
media: '$media',
user: '$user',
likesCount: '$likesCount',
commentsCount: '$commentsCount',
isLiked: '$isLiked',
status: '$status',
address: '$address',
city: '$city',
createdAt: '$createdAt',
},
},
},
},
{ $unset: 'blips.media._id' },
{ $unset: 'blips.user._id' },
{ $unset: 'blips.user.__v' },
{
$project: {
_id: 0,
lat: 1,
lng: 1,
totalBlips: 1,
isViewable: 1,
blips: 1,
},
},
{ $sort: { totalBlips: -1 } },
);
if (query.page !== undefined && query.limit !== undefined) {
const page = query.page * 1;
const limit = query.limit * 1;
const skip = (page - 1) * limit;
aggrea.push({ $skip: skip }, { $limit: parseInt(query.limit, 10) });
}
const blipLocations = await this.blipModel.aggregate(aggrea);
const total = blipLocations.length;
let viewableBlips = [];
if (query.lat && query.lng) {
const radius = query.distance / 3963.2;
viewableBlips = await this.blipModel.find({
location: {
$geoWithin: {
$centerSphere: [[query.lng, query.lat], radius],
},
},
});
}
await Promise.all(
blipLocations.map(async (blpL) => {
await Promise.all(
blpL.blips.map(async (blp) => {
const like = await this.likeModel.findOne({
user: user.id,
blip: blp.id,
});
if (like) {
blp.isLiked = true;
} else {
blp.isLiked = false;
}
if (query.lat && query.lng) {
viewableBlips.forEach((vBlp) => {
if (vBlp._id.toString() === blp.id.toString()) {
console.log(vBlp.id);
blpL.isViewable = true;
}
});
}
}),
);
}),
);
return {
status: true,
message: 'Data fetched successfully',
results: total,
blipLocations,
};
}
In the above snippet, I have a field called isViewable. Right now I am updating this field in runtime. But I want to update this field in the aggregation pipeline. Is there any way to check if the location coordinate is within the provided $geoWithin or $geoNear from the aggregation pipeline? Thanks.
If you're fine with using $geoNear (you need the 2dsphere index & it must be the first stage in the pipeline as noted in docs) you could add a distance field and then another field which will output a boolean based on it, like so:
this.blipModel.aggregate([
{
$geoNear: {
key: 'location',
near: {
type: 'Point',
coordinates: [query.lng, query.lat]
},
distanceField: 'distance',
spherical: true
}
},
{
$addFields: {
isViewable: {
$cond: {
if: {$lte: ["$distance", query.distance]},
then: true,
else: false
}
}
}
}
]);
And optionally unset the distance field in another stage if you don't need it.
I have two model one is productModel.js and another is reviewModel.js. I did child referencing in reviewSchema. product Schema has ratingsAverage and ratingsQuantity property. I want to manipulate these properties of properties inside from reviewModel.js. So that I have used statics method on reviewSchema.
reviewModel.js:
const Product = require("./productModel");
const { Schema } = require("mongoose");
const mongoose = require("mongoose");
const reviewSchema = new Schema({
review: {
type: String,
},
rating: {
type: Number,
min: 1,
max: 5,
},
createdAt: {
type: Date,
default: Date.now(),
},
product: {
type: mongoose.Schema.ObjectId,
ref: "Product",
required: [true, "A review must belong to a tour."],
},
user: {
type: mongoose.Schema.ObjectId,
ref: "User",
required: [true, "A review must belong to a user"],
},
});
// Preventing from duplication of review on same tour from same user
// Combined index
reviewSchema.index({ product: 1, user: 1 }, { unique: true });
// Creating calcAverageRating() static method for calculating average rating and number of ratings on product. It should work on creating, updating and deleting of review.
reviewSchema.statics.calcAverageRating = async function (productId) {
// "this" represents here model
const stats = await this.aggregate([
{
$match: { product: productId },
},
{
$group: {
_id: "$product",
nRating: { $sum: 1 },
avgRating: { $avg: "$rating" },
},
},
]);
console.log(stats, "rrrr");
// console.log(Product, "product");
// Problem is coming here i am not able to access productModel
// const doc = await Product.findByIdAndUpdate(
// productId,
// {
// ratingsAverage: stats[0],
// ratingsQuantity: stats[0],
// },
// {
// new: true,
// runValidators: true,
// }
// );
};
// As you know static methods only call on Model so we have to think about how to call calcAverageRating() static method. problem is that we have to get this product id which one is used to creating review so we only get this id on document middleware("post") for creating and for updating and deleting we will get from get from query middleware(/^findOne/).
// Document middleware("save")
reviewSchema.post("save", function () {
this.constructor.calcAverageRating(this.product);
});
reviewSchema.post(/^findOneAnd/, async function () {
// console.log(await this.findOne());
this.r = await this.clone().findOne();
console.log(this.r, "query");
console.log(Product, "testing");
});
reviewSchema.post(/^findOneAnd/, function () {
this.r.constructor.calcAverageRating(this.r.product);
});
// reviewSchema.pre(/^find/, function (next) {
// this.populate({
// path: "product",
// select: "name",
// });
// next();
// });
// CREATING MODEL
const Review = mongoose.model("Review", reviewSchema);
// We can call here static method but we will not get productId so we have to call this static method before creating of Model.
// Review.calcAverageRating();
// EXPORTING
module.exports = Review;
productModel.js:
const mongoose = require("mongoose");
const { Schema } = require("mongoose");
const Review = require("./reviewModel");
// CREATING SCHEMA
const productSchema = new Schema(
{
name: {
type: String,
required: [true, "Please enter product name"],
unique: true,
trim: true,
},
slug: String,
price: {
type: Number,
required: [true, "A product must have a price"],
maxlength: [8, "Price cannot exceeds 8 characters"],
},
priceDiscount: {
type: Number,
validate: {
validator: function (val) {
// this only points to current doc on NEW document creation
return val < this.price;
},
message: "Discount price ({VALUE}) should be below regular price",
},
},
stock: {
type: Number,
required: [true, "Please Enter product Stock"],
maxLength: [4, "Stock cannot exceed 4 characters"],
default: 1,
},
summary: {
type: String,
required: [true, "A product must have a summary"],
},
description: {
type: String,
required: [true, "A product have a description"],
},
images: [
{
public_id: {
type: String,
required: [true, "Images public id is required"],
},
url: {
type: String,
required: [true, "Images url is required"],
},
},
],
ratingsAverage: {
type: Number,
default: 0,
},
numOfReviews: {
type: Number,
default: 0,
},
reviews: [
{
user: {
type: mongoose.Schema.ObjectId,
ref: "User",
required: true,
},
name: {
type: String,
required: true,
},
rating: {
type: Number,
required: true,
},
comment: {
type: String,
required: true,
},
},
],
user: {
type: mongoose.Schema.ObjectId,
ref: "User",
required: true,
},
category: {
type: String,
required: [true, "A product must belong to a category"],
},
createdAt: {
type: Date,
default: Date.now(),
},
// ratingsAverage: {
// type: Number,
// default: 4.5,
// min: [1, "Rating must be above 1.0"],
// max: [5, "Rating must be below 5.0"],
// // Declaring custom function for do some opeartion from get value
// // set: (val) => Math.round(val * 10) / 10, // 4.666666, 46.6666, 47, 4.7
// set: (val) => Math.round(val * 10) / 10,
// },
// ratingsQuantity: {
// type: Number,
// default: 0,
// },
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
productSchema.index({ name: "text", description: "text" });
// VIRTUAL POPULATE
productSchema.virtual("reviews", {
ref: "Review",
foreignField: "product",
localField: "_id",
});
// CREATING MODEL
const Product = mongoose.model("Product", productSchema);
// EXPORTING
module.exports = Product;