I am having a little trouble getting my mongoose virtuals to show up from deep populated fields. Here is the code of the backend function that is not behaving as I'd like it to:
exports.get_user_feed = async (req, res, next) => {
const options = { sort: { date: -1 } };
const user = await User.find(
{ username: req.params.user },
"username posts avatar followers following"
)
.populate({
path: "posts",
options,
populate: [
{
path: "author",
},
{ path: "comments", populate: { path: "author" } },
],
})
.sort({ "posts.date": 1 });
res.json({ ...user });
};
And here is the comment schema:
const mongoose = require("mongoose");
const { DateTime } = require("luxon");
const Schema = mongoose.Schema;
const CommentSchema = new Schema({
targetPost: { type: Schema.Types.ObjectId, ref: "Post", required: true },
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
date: { type: Date, required: true },
content: { type: String, maxlength: 400 },
comments: [{ type: Schema.Types.ObjectId, ref: "Comment" }],
stars: [{ type: Schema.Types.ObjectId, ref: "User" }],
});
// Virtual for post's URL
CommentSchema.virtual("url").get(function () {
return "/" + this.targetPost.url + this._id;
});
// Virtual for formatted date.
CommentSchema.virtual("formatted_date").get(function () {
return (
DateTime.fromJSDate(this.date).toLocaleString(DateTime.DATE_MED) +
" at " +
DateTime.fromJSDate(this.date).toLocaleString(DateTime.TIME_SIMPLE)
);
});
//Export model
module.exports = mongoose.model("Comment", CommentSchema);
My goal is to get the comments from each post to also include the formatted_date of the comment, but this virtual is not getting included in the response that is sent - all the regular properties are being sent but not the virtual. Any help here would be appreciated.
Add this code in your Commnet Schema file before module.exports.
CommentSchema.method('toJSON', function () {
const {
...object
} = this.toObject({ virtuals:true });
return object;
});
Related
I'm creating Social Network app using MERN. I have made it so far that users can create posts and have followers and followings. My task is to make an option on the post so it can be public or private. I don't know how to do that. Has anybody idea or example of code how to do it? Thanks!
This is my post model:
const mongoose = require("mongoose");
const postSchema = new mongoose.Schema({
caption: String,
image: {
public_id: String,
url: String,
},
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
createdAt: {
type: Date,
default: Date.now,
},
likes: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
comments: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
comment: {
type: String,
required: true,
},
},
],
visibility: {
type: String,
enum : ["public", "private"],
default: "public"
},
});
module.exports = mongoose.model("Post", postSchema);
This is my user model:
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Please enter a name"],
},
avatar: {
public_id: String,
url: String,
},
email: {
type: String,
required: [true, "Please enter an email"],
unique: [true, "Email already exists"],
},
password: {
type: String,
required: [true, "Please enter a password"],
minlength: [6, "Password must be at least 6 characters"],
select: false,
},
posts: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Post",
},
],
followers: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
following: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
resetPasswordToken: String,
resetPasswordExpire: Date,
});
userSchema.pre("save", async function (next) {
if (this.isModified("password")) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
userSchema.methods.matchPassword = async function (password) {
return await bcrypt.compare(password, this.password);
};
userSchema.methods.generateToken = function () {
return jwt.sign({ _id: this._id }, process.env.JWT_SECRET);
};
userSchema.methods.getResetPasswordToken = function () {
const resetToken = crypto.randomBytes(20).toString("hex");
this.resetPasswordToken = crypto
.createHash("sha256")
.update(resetToken)
.digest("hex");
this.resetPasswordExpire = Date.now() + 10 * 60 * 1000;
return resetToken;
};
module.exports = mongoose.model("User", userSchema);
and here is the logic how of creating post:
exports.createPost = async (req, res) => {
try {
const myCloud = await cloudinary.v2.uploader.upload(req.body.image, {
folder: "posts",
});
const newPostData = {
caption: req.body.caption,
image: {
public_id: myCloud.public_id,
url: myCloud.secure_url,
},
owner: req.user._id,
};
const post = await Post.create(newPostData);
const user = await User.findById(req.user._id);
user.posts.unshift(post._id);
await user.save();
res.status(201).json({
success: true,
message: "Post created",
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
};
In your post's model, you can add a "visibility" field with private & public as possible values (one of them default, obviously). Then every time you are querying for that post or list of posts, use that visibility column to decide if the user should be served that post or a 403 response page.
Check this for enum validation.
I almost have the desired functionality but it's not exactly what I wanted to approach.
I have two model schema, Control and SubControl. The SubControl is referenced in the Control model. I want to post the control model + a reference of the SubControl.
My post method:
router.post(
'/add',
auth,
role.checkRole(role.ROLES.Admin, role.ROLES.Regulator),
async (req, res) => {
try {
const subControl = new SubControl({...req.body});
const subControlDoc = await subControl.save();
const control = new Control({...req.body});
control.subControl.push(subControlDoc._id);
const savedControl = await control.save();
res.status(200).json({
success: true,
message: `Control has been added successfully!`,
control: savedControl
});
} catch (error) {
return res.status(400).json({
error
// error: 'Your request could not be processed. Please try again.'
});
}
}
);
My Control Schema:
const ControlSchema = new Schema({
_id: {
type: Schema.ObjectId,
auto: true
},
mainControl: {
type: String
},
subControl: [{
subControlNo: {type: Mongoose.Schema.Types.String, ref: 'SubControl'}
}],
controlDescription: {
type: String,
trim: true
},
updated: Date,
created: {
type: Date,
default: Date.now
}
});
module.exports = Mongoose.model('Control', ControlSchema);
My SubControl schema:
const SubControlSchema = new Schema({
_id: {
type: Schema.ObjectId,
auto: true
},
subControlNo: {
type: String
},
updated: Date,
created: {
type: Date,
default: Date.now
}
});
module.exports = Mongoose.model('SubControl', SubControlSchema);
Postman:
{
"mainControl": "nn",
"controlDescription": "controldescription",
"subControl":
[
{
"subControlNo": "1-2"
},
{
"subControlNo": "1-2-1"
}
]
}
Result I'm getting:
Question: Why am I getting 3 object id's although I inserted 2 and why only the last object ID is saved in my SubControl database? I this the way to add array of object id's or not?
at C:\Users\Deepak\Desktop\mern-ecommerce\mern-back-end\src\controller\cart.js:75:44
I am getting this error in controllers while making a post request.
Here is my code:
Controllers (Cart.js)
exports.getCartItems = (req, res) => {
Cart.findOne({ user: req.user._id })
.populate("cartItems.product", " _id name price productPictures")
.exec((error, cart) => {
if (error) return res.status(400).json({ error });
if (cart) {
let cartItems = {};
cart.cartItems.forEach((item, index) => {
cartItems[item.product._id.toString()] = {
_id: item.product._id.toString(),
name: item.product.name,
img: item.product.productPictures[0].img,
price: item.product.price,
qty: item.quantity,
};
});
res.status(200).json({ cartItems });
}
});
};
Routes (Cart.js)
router.post('/user/getCartItems', requireSignin, userMiddleware, getCartItems);
Models (Cart.js)
const mongoose = require('mongoose');
const cartSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
cartItems: [
{
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Products', required: true },
quantity: { type: Number, default: 1 }
}
]
}, {timestamps: true});
module.exports = mongoose.model('Cart', cartSchema);
I am getting error on console that : Cannot read property 'likes' of null
I am using postman for getting requests and putting response and response.
The array 'likes' is empty and here I am trying to insert the user id inside it but unable to insert it through unshift() method.
This is schema defined in a file Posts.js
const { text } = require('express');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String
},
avatar: {
type: String
},
likes: [
{
users: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}
],
comment: [
{
users: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String,
},
avatar: {
type: String
},
date: {
type: Date,
default: Date.now
}
}
],
date: {
type: Date,
default: Date.now
}
});
module.exports = Post = mongoose.model('post', PostSchema);
This is a express code for put request in file posts.js
const express = require('express');
const router = express.Router();
const { check, validationResult } = require('express-validator/check');
const auth = require('../../middleware/auth');
const Posts = require('../../models/Posts');
const User = require('../../models/User');
const { route } = require('./profile');
router.put('/like/:id', auth, async(req, res) => {
try {
const post = await Post.findById(req.params.id);
// Check if the post has already been liked
if(post.likes.filter(like => like.user.toString() === req.user.id).length > 0) {
return res.status(400).json({ msg: 'Post already liked' });
}
post.likes.unshift({ user: req.user.id });
await post.save();
res.json(post.likes);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
Here is the typo error I made. In the schema Posts.js in likes and comment array I wrote users instead of user.
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String
},
avatar: {
type: String
},
likes: [
{
users: { // Here it has to be user
type: Schema.Types.ObjectId,
ref: 'users'
}
}
],
comment: [
{
users: { //Here it has to be user
type: Schema.Types.ObjectId,
ref: 'users'
},
It's possible that i'm just burned out, but I have the following models:
user
const mongoose = require('mongoose');
const validate = require('mongoose-validator');
const Post = require('./post');
let UserSchema = mongoose.Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
email: {
type: String, required: true, lowercase: true, trim: true, unique: true, index: true,
validate: [validate({ validator: 'isEmail', message: 'Invalid Email!' })]
},
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }]
})
module.exports = mongoose.model('User', UserSchema);
posts
const _ = require('lodash');
const mongoose = require('mongoose');
const User = require('./user');
let PostSchema = mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
title: { type: String, required: true },
body: { type: String, require: true }
})
PostSchema.post('save', async function (next) {
await User.update({ _id: this.user }, { $push: { posts: this._id } })
return next();
})
module.exports = mongoose.model('Post', PostSchema);
When trying to add a new post, the post save hook runs, but I get the error User.update is not a function (same goes for findOneAndUpdate, findOne, etc).
I can call user.update from the rest of the app without issues, so not sure whats happening here. Both models are in the same directory.
What you missed is that post middleware has the first argument as the "document" and not the next handler:
user.js
const { Schema } = mongoose = require('mongoose');
const userSchema = new Schema({
firstName: String,
lastName: String,
posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }]
});
post.js
const { Schema } = mongoose = require('mongoose');
const User = require('./user');
const postSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User' },
title: String,
body: String
});
// note that first argument is the "document" as in "post" once it was created
postSchema.post('save', async function(doc, next) {
await User.update({ _id: doc.user._id },{ $push: { posts: doc._id } });
next();
});
index.js
const { Schema } = mongoose = require('mongoose');
const User = require('./user');
const Post = require('./post');
const uri = 'mongodb://localhost/posttest';
mongoose.set('debug', true);
mongoose.Promise = global.Promise;
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
let user = await User.create({ firstName: 'Ted', lastName: 'Logan' });
let post = new Post({ user: user._id, title: 'Hi', body: 'Whoa!' });
post = await post.save();
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
Returns:
Mongoose: users.remove({}, {})
Mongoose: posts.remove({}, {})
Mongoose: users.insertOne({ posts: [], _id: ObjectId("5b0217001b5a55208150cc9b"), firstName: 'Ted', lastName: 'Logan', __v: 0 })
Mongoose: posts.insertOne({ _id: ObjectId("5b0217001b5a55208150cc9c"), user: ObjectId("5b0217001b5a55208150cc9b"), title: 'Hi', body: 'Whoa!', __v: 0 })
Mongoose: users.update({ _id: ObjectId("5b0217001b5a55208150cc9b") }, { '$push': { posts: ObjectId("5b0217001b5a55208150cc9c") } }, {})
Showing that the update fires with the correct detail.
In good design you really should avoid this and simply drop the posts array from the User model. You can always either use a virtual instead:
userSchema.virtual('posts', {
ref: 'Post',
localField: '_id',
foreignField: 'user'
})
Or just get the data via $lookup:
User.aggregate([
{ "$match": { "_id": userId } }
{ "$lookup": {
"from": Post.collection.name,
"localField": "_id",
"foreignField": "user",
"as": "posts"
}}
])
Storing and maintaining arrays of related ObjectId values "on the parent" is kind of an "anti-pattern" and leads to unnecessary overhead such as writing in two places where you only need "one".
Also in general you should be opting for embedding "first", and only considering "referencing" if and when the usage pattern of the application actually demands it. Simply copying the same patterns of an RDBMS with a database engine that was not designed for that is not the best way to utilize it.