I'm trying to define a simple RESTful API using Node.js, mongoose and restify. The goal is to have users which can comment on profiles of others users. For this I have a comment endpoint that receives a text, the author and the target of the comment (other user).
I want to reference users so I defined next schemas:
User schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = new Schema({
"username": { type: String, unique: true, required: true },
"password": { type: String, required: true },
"comments": [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
});
mongoose.model('User', UserSchema);
Comment schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var CommentSchema = new Schema({
date: { type: Date, default: Date.now },
text: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
target: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }
});
mongoose.model('Comment', CommentSchema);
I also have this controller (just showing createComment function):
exports.createComment = function(req, res, next) {
var authorId, targetId;
User.findOne({ _id: req.params.authorId}, function(err, author) {
if (author) {
User.findOne({ _id: req.params.targetId}, function(err, target) {
if (target) {
var comment = new Comment();
comment.text = req.params.text;
comment.author = author._id;
comment.target = target._id;
comment.save(function(err, comment) {
if (err) {
res.status(500);
res.json({
type: false,
data: 'Error occurred: ' + err
});
} else {
res.json({
type: true,
data: comment
});
}
});
} else {
res.json({
type: false,
data: 'User ' + req.params.authorId + ' not found'
});
}
});
} else {
res.json({
type: false,
data: 'User ' + req.params.targetId + ' not found'
});
}
});
};
So, I have three questions:
Why do I need to check if the user received exists? I would like to receive only the id and store it but I have to do two more queries to check it myself.
What I have to do to store in User only comments where that user is the target? solved in the edited code
How can I simplify this code? Is a pain to have async queries executed in order. I would like to have generic errors and not to have to handle each one.
EDIT: I've simplified the code using validations on the schema:
Comment schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = mongoose.model('User');
var CommentSchema = new Schema({
date: { type: Date, default: Date.now },
text: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
target: { type: Schema.Types.ObjectId, ref: "User", required: true }
});
CommentSchema.path('author').validate(function(value, respond) {
User.findOne({ _id: value}, function(err, user) {
respond(!err && user);
});
}, 'Author doesn\'t exists');
CommentSchema.path('target').validate(function(value, respond) {
User.findOne({ _id: value}, function(err, user) {
respond(!err && user);
});
}, 'Target user doesn\'t exists');
mongoose.model('Comment', CommentSchema);
Controller:
exports.createComment = function(req, res, next) {
var comment = new Comment(req.body);
comment.save(function(err, comment) {
if (err) {
res.status(500);
res.json({
type: false,
data: 'Error occurred: ' + err
});
} else {
User.findOne({ _id: comment.target }, function(err, user) {
user.comments.push(comment);
user.save();
});
res.json({
type: true,
data: comment
});
}
});
};
The problem with this is that now I have to use _id on queries (I would like to use a custom id) and I'm doing three queries every time I want save a comment (2 for validation and one more to store the comment). Is there a better way to to this?
You can use the select option of query in mongo to select only the _id field of the user, like this:
User.findOne({_id:req.params.authorId}).select({_id:1}).exec(function(err,user) {})
After you pull the userTarget from mongo, you need to add the comment._id to his list of comments, and save him:
target.comments.push(comment._id);
target.save(function(err, targetAfterSaved) {})
read about async or q, they are my favorites Libraries to handle with async functions. For handle with errors like you want, you can add some listeners - here is the documentation from restify site.
Hope you understand, if you need any help let me know
Related
I am trying to fetch the blogs that a user has posted using mongoose populate method but not able to do so but when I try to look for the user who posted a blog I am able to do so. Tell me what I am doing wrong.
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
blogPosts: [{ type: mongoose.Types.ObjectId, ref: "Blogs", required: true }],
});
const Users = mongoose.model("Users", userSchema);
module.exports = Users;
This is my user model
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const blogSchema = new Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
user: {
type: mongoose.Types.ObjectId,
ref: "Users",
required: true,
},
});
const blogs = mongoose.model("Blogs", blogSchema);
module.exports = { blogs, blogSchema };
this is my blogs model
router.get("/users/:id", async (req, res) => {
let userBlogs;
try {
userBlogs = await users.find({ _id: req.params.id }).populate("blogPosts");
if (!userBlogs) {
return res.status(404).json({ msg: "No user found" });
}
return res.status(200).json(userBlogs);
} catch (err) {
return res.status(500).json({ err });
}
});
this is the route that I use for fetching all the blogs that a user has posted.This is just sending me back an empty posts array.
router.post("/newBlog/:id", async (req, res) => {
let blogAuthor;
try {
blogAuthor = await users.findOne({ _id: req.params.id });
} catch (err) {
return res.status(404).json({ message: "user not found" });
}
const newBlog = blogs({
title: req.body.title,
description: req.body.description,
image: req.body.imageUrl,
user: blogAuthor._id,
});
try {
await blogAuthor.blogPosts.push(newBlog);
await newBlog.save();
return res.status(200).json(blogAuthor.blogPosts);
} catch (err) {
return res.status(500).json(err);
}
});
the above is the route that I use to add new blog to the db.
router.get("/allBlogs", async (req, res) => {
let allBlogs;
try {
allBlogs = await blogs.find({}).populate("user");
} catch (err) {
return res.status(404).json(err);
}
res.status(200).json(allBlogs);
});
this is the route that sends all the blogs posted by all the users.
Interesting thing is that when I try to populate the user of a blogpost that is working exactly as expected it is populating the user of the blog from the users model however when I try to do the reverse that's not working. It is not populating all the blogs that a user has posted.
I have created the following User schema
const UserSchema = mongoose.Schema(
{
fullName: {
type: String,
required: true,
index: true,
},
department: {
type: mongoose.Schema.Types.ObjectId,
ref: "Department",
required: true,
},
},
});
and my Department schema looks like the following
const DepartmentSchema = mongoose.Schema(
{
name: {
type: String,
unique: true,
required: true,
},
description: {
desc: "Description.",
type: String,
},
},
{
strict: true,
versionKey: false,
timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" },
});
Now I'm able to create departments and all is working well in Postman and also my Angular application. But while creating User it only saves the department ID that I've selected or added in my form. This is the data that I've tried to create
user = {
department: "5f806be7c9a3c02c7c61f9f1"
fullName: "John Joshua"
}
and my create route controller looks like this
exports.create = async (req, res) => {
const newUser = new User({
fullName: req.body.fullName,
department: req.body.department,
});
try {
const errors = validationResult(req); // I've used express validator here
if (!errors.isEmpty()) {
res.status(400).json({ errors: errors.array() });
return;
}
const savedUser = await newUser.save();
res.json(savedUser); // Here I was expecting to return the new saved user with Department details
} catch (err) {
res.status(500).json({ message: err });
}
};
So my response is all good and working well but department only contains the ID with nothing more.
How can I send a response data with Department details? Any help is appreciated.
I am trying to obtain the object id for any article already in db so that I can validate that the article exists before comments are made.
The issue is on the router (/blog/article/comment). I cannot get the article object id from /blog/article/:postid. I want to pass this id to articleId like this:
articleId: req.params.postid
I have also tried:
articleId: req.article._id
model structure: comment.js
var mongoose = require('mongoose');
var CommentSchema = new mongoose.Schema({
content: { type: String },
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
articleId: { type: mongoose.Schema.Types.ObjectId, ref:'Article' },
dateCommented: { type: Date, default : Date.now }
});
Article model: article.js
var ArticleSchema = new mongoose.Schema({
category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' },
commentId:{type: mongoose.Schema.Types.ObjectId, ref:'Comment'},
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User'},
blog: [{
topic: { type: String, unique: false, lowercase: true },
body: { type: String, unique: false, lowercase: true },
tags: [ 'first', 'mongodb', 'express'],
created: Date,
modified: { type : Date, default : Date.now },
state: { type: String, unique: false, lowercase: true }
}]
});
main.js
router.param('postid', function(req, res, next, id) {
if (id.length !=24) return next(new Error ('The post id is not having the correct length'));
//articleId: req.param('postid'),
Article.findOne({ _id: ObjectId(id)}, function(err, article) {
if (err) return next(new Error('Make sure you provided correct post id'));
req.article = article;
next();
});
});
router.get('/blog/article/:postid', function (req, res, next) {
Article.findById({ _id: req.params.postid }, function (err, article) {
if (err) return next(err);
res.render('main/publishedArticle', {
article: article
});
});
});
router.post('/blog/article/comment', function(req, res, next) {
async.waterfall([
function(callback) {
var comment = new Comment({
articleId: req.params.postid,
content: req.body.content,
user: req.user._id
});
comment.save(function(err) {
if (err) return next (err);
req.flash('success', 'Thank you for your comment');
callback(err, comment);
});
},
function(comment) {
Article.update({_id : comment.articleId }, { $set: { commentId: {} }}, function(err, updated) {
if (updated) {
res.redirect('/')
}
});
}
]);
});
Another issue I have is how to update the commentId for each comment in the Article
Article.update({_id : comment.articleId }, { $set: { commentId: {} }}, function(err, updated)
Since the /blog/article/comment route is a post request. Just submit your articleId in the body of that request. You'll have to send it up from the client. You can access it with req.body.articleID (If that is what you call the variable).
See here for more info on POST requests in node.
For your second question:
Within your article schema you have commentId, That is a single record. What you want is an array of comments. Something like this:
comments: [{type: mongoose.Schema.Types.ObjectId, ref:'Comment'}]
Then within your code...
...
function(comment) {
//comment should contain all the comments
//Grab the article
Article.findOne({ _id: comment.articleId}, function(err, article){
//Go through all the comments in 'comment' compare them with the ones in artcle.comments.
//The ones that aren't already in the article object get put into newComments...
var newComments = [];
Article.update({ _id: comment.articleId }, { $addToSet: { comments: newComments } }, function(err, updated) {
if (updated) {
res.redirect('/')
}
});
});
}
...
I didn't fully implement the code, but it should get you off to the right start.
addToSet Documentation
Some more examples of add to set
I have two similar parent-child relationship trees in my data model and I am using Mongoose's middleware to handle cascade of deletes.
The first hierarchy is represented as follows:
blog hierarchy
//blog.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = require('./user.js');
var Entry = require('./entry.js');
var Follower = require('./follower.js');
var Blog = new Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
name: {
type: String,
required: true
},
headerImageUrl: String,
description: {
type: String,
required: true
}
}, {
timestamps: true
});
Blog.pre('remove', function(next) {
Entry.find({"blogId": this._id})
.exec(function(err, entries) {
console.log("found entries " + entries);
for(var i = entries.length -1; i >= 0; i--) {
entries[i].remove();
}
});
Follower.remove({"blogId": this._id}).exec();
next();
});
module.exports = mongoose.model('Blog', Blog);
//entry.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var Blog = require('./blog.js');
var Comment = require('./comment.js');
var Entry = new Schema({
blogId: {
type: Schema.Types.ObjectId,
ref: 'Blog'
},
title: String,
thumbnailUrl: String,
content: String
}, {
timestamps: true
});
Entry.pre('remove', function(next) {
Comment.remove({"entryId": this._id}).exec();
next();
});
module.exports = mongoose.model('Entry', Entry);
//comment.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = require('./user.js');
var Entry = require('./entry.js');
var Post = require('./post.js');
var Comment = new Schema({
text: {
type: String,
required: true
},
postedBy: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
parentComment: this,
entryId: {
type: Schema.Types.ObjectId,
ref: 'Entry'
},
postId: {
type: Schema.Types.ObjectId,
ref: 'Post'
},
type: {
type: String,
enum: ['BLOG', 'FORUM', 'COMMENT'],
required: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Comment', Comment);
In this case, when remove is called on a blog instance, the Blog.pre('remove'... functionality does what it is intended to do and cleans up all children of the blog instance.
The calling code is as follows:
blogRouter.route('/:blogId')
//delete a specific blog by blog id: [OWNER OR ADMIN USER]
.delete(Verify.verifyOrdinaryUser, Verify.verifyBlogOwnerOrAdmin, function(req, res, next) {
Blog.findById(req.params.blogId, function(err, blog) {
if(err) return next(err);
blog.remove();
res.json(blog);
});
});
I am doing exactly the same thing for the second hierarchy, visually represented here: forum hierarchy and with the following code:
//forum.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = require('./user.js');
var Post = require('./post.js');
var Subscription = require('./subscription.js');
var Forum = new Schema({
title: {
type: String,
required: true
},
description: String,
moderators: [
{
type: Schema.Types.ObjectId,
ref: 'User'
}
]
}, {
timestamps: true
});
Forum.pre('remove', function(next) {
Post.find({"forumId": this._id})
.exec(function(err, posts) {
console.log("found posts " + posts);
for(var i = posts.length -1; i >= 0; i--) {
posts[i].remove();
}
});
Subscription.remove({"forumId": this._id}).exec();
next();
});
module.exports = mongoose.model('Forum', Forum);
//post.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = require('./user.js');
var Forum = require('./forum.js');
var Comment = require('./comment.js');
var Post = new Schema({
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
postText: {
type: String,
required: true
},
forumId: {
type: Schema.Types.ObjectId,
ref: 'Forum'
},
}, {
timestamps: true
});
Post.pre('remove', function(next) {
Comment.remove({"postId": this._id}).exec();
next();
});
module.exports = mongoose.model('Post', Post);
//comment.js [SAME AS INCLUDED ABOVE]
But in this case, when remove is called on a forum instance, the Forum.pre('remove'... functionality causes an error to be thrown: TypeError: Post.find is not a function. The error is thrown at forum.js line 24 (second line in the Forum.pre(...) block)
The calling code is as follows:
forumRouter.route('/:forumId')
//delete forum by id: [ADMIN]
.delete(Verify.verifyOrdinaryUser, Verify.verifyAdmin, function(req, res, next) {
Forum.findById(req.params.forumId, function(err, forum) {
if(err) return next(err);
forum.remove();
res.json(forum);
});
});
While I have found instance of this error being thrown in my online research here and elsewhere, the causes there were incorrectly exported models or incorrectly defined schemas. The Post schema is being used successfully throughout the rest of my code with no issues, it is only this call to Post.find that is causing the error. And in fact, my postRouter code calls the following successfully:
postRouter.route('/')
// retrieve all forum posts: [ALL USERS]
.get(function(req, res, next) {
Post.find({})
.populate({
path: 'forumId',
model: 'Forum',
populate: {
path: 'moderators',
model: 'User'
}
})
.populate('createdBy')
.exec(function(err, posts){
if(err) return next(err);
res.json(posts);
});
});
I am new to mongoose and nodejs. Please help me to understand what is wrong in my code while saving collection. My user.js looks like below
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');
var UserSchema = new Schema ({
name: { type: String, required: true },
username:{ type:String, required: true, index:{ unique: true}},
password:{ type: String, required: true, select: false},
email:{ type: String, required: true, select: true},
mobile:{ type: String, required: true, select: true},
nativecid:{ type: Schema.Types.ObjectId, ref:'City'},
regdate:{ type: Date, default: Date.now },
did:{ type: String, required: false }
});
UserSchema.pre('save', function(next){
var user = this;
if(!user.isModified('password')) return next();
bcrypt.hash(user.password, null, null, function(err, hash){
if(err) return next(err);
user.password = hash;
next();
});
});
UserSchema.methods.comparePassword = function(password){
var user = this;
return bcrypt.compareSync(password, user.password);
};
module.exports = mongoose.model('User', UserSchema);
And my City model ie city.js looks like below
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var CitySchema = new Schema({
name: { type: String, required: true },
status: { type: Boolean, default: true },
date: { type: Date, default: Date.now }
});
module.exports = mongoose.model( 'City', CitySchema );
City is already stored in database as below
{ "_id" : ObjectId("56a4a0adb0f445561cfd4e37"), "name" : "New York", "date" : ISODate("2016-01-24T10:00:13.220Z"), "status" : 1, "__v" : 0 }
I want this ObjectId of New York should be saved in user collection while user signups. I am trying like below but not succeed.
var User = require('../models/user');
var City = require('../models/city');
api.post('/signup', function(req, res){
var user = new User({
name: req.body.name,
username: req.body.username,
password: req.body.password,
email: req.body.email,
mobile: req.body.mobile
});
City
.findOne({ name: "New York" })
.populate('_id')
.exec(function (err, city1) {
if(err){
res.send(err);
return;
}
console.log('The creator is %s', city1.name);
return user.nativecid = city1._id;
})
user.save(function(err){
if(err){
res.send(err);
return;
}
res.json({
success: true,
message: 'User created successfully!'
});
});
});
I am not understanding what is wrong. Please advice me.
Just save user in city.findOne (nested) and may be no need to populate you should use projection.
City.findOne({ name: "New York" }, {name:1}) // ID return defaulat
.exec(function (err, city1) {
if(err){
res.send(err);
return;
}
console.log('The creator is %s', city1.name);
user.nativecid = city1._id;
user.save(function(err1){
if(err1){
res.send(err1);
return;
}
res.json({
success: true,
message: 'User created successfully!'
});
});
})