mongoose how to manage count in a reference document - node.js

So I've got these schemas:
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Comment Schema
*/
var CommentSchema = new Schema({
post_id: {
type: Schema.Types.ObjectId,
ref: 'Post',
required: true
},
author:{
type: String,
required: true
},
email:{
type: String,
required: true
},
body: {
type: String,
required: true,
trim: true
},
status: {
type: String,
required: true,
default: 'pending'
},
created: {
type: Date,
required: true,
default: Date.now
},
meta: {
votes: Number
}
});
/**
* Validations
*/
CommentSchema.path('author').validate(function(author) {
return author.length;
}, 'Author cannot be empty');
CommentSchema.path('email').validate(function(email) {
return email.length;
}, 'Email cannot be empty');
CommentSchema.path('email').validate(function(email) {
var emailRegex = /^([\w-\.]+#([\w-]+\.)+[\w-]{2,4})?$/;
return emailRegex.test(email);
}, 'The email is not a valid email');
CommentSchema.path('body').validate(function(body) {
return body.length;
}, 'Body cannot be empty');
mongoose.model('Comment', CommentSchema);
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
monguurl = require('monguurl'),
Schema = mongoose.Schema;
/**
* Article Schema
*/
var PostSchema = new Schema({
title: {
type: String,
required: true,
trim: true
},
author:{
type: String,
required: true,
default: 'whisher'
},
slug: {
type: String,
index: { unique: true }
},
body: {
type: String,
required: true,
trim: true
},
status: {
type: String,
required: true,
trim: true
},
created: {
type: Date,
required: true,
default: Date.now
},
published: {
type: Date,
required: true
},
categories: {
type: [String],
index: { unique: true }
},
tags: {
type: [String],
required: true,
index: true
},
comment: {
type: Schema.Types.ObjectId,
ref: 'CommentSchema'
},
meta: {
votes: Number
}
});
/**
* Validations
*/
PostSchema.path('title').validate(function(title) {
return title.length;
}, 'Title cannot be empty');
PostSchema.path('body').validate(function(body) {
return body.length;
}, 'Body cannot be empty');
PostSchema.path('status').validate(function(status) {
return /publish|draft/.test(status);
}, 'Is not a valid status');
PostSchema.plugin(monguurl({
source: 'title',
target: 'slug'
}));
mongoose.model('Post', PostSchema);
by an api I query Post like
exports.all = function(req, res) {
Post.find().sort('-created').exec(function(err, posts) {
if (err) {
res.jsonp(500,{ error: err.message });
} else {
res.jsonp(200,posts);
}
});
};
How to retrieve how many comments has the post ?
I mean I want an extra propriety in post object
like post.ncomments.
The first thing I think of is adding an extra
field to the post schema and update it whenever a user
add a comment
meta: {
votes: Number,
ncomments:Number
}
but it seems quite ugly I think

If you want the likely the most efficient solution, then manually adding a field like number_comments to the Post schema may be the best way to go, especially if you want to do things like act on multiple posts (like sorting based on comments). Even if you used an index to do the count, it's not likely to be as efficient as having the count pre-calculated (and ultimately, there are just more types of queries you can perform when it has been pre-calculated, if you haven't chosen to embed the comments).
var PostSchema = new Schema({
/* others */
number_comments: {
type: Number
}
});
To update the number:
Post.update({ _id : myPostId}, {$inc: {number_comments: 1}}, /* callback */);
Also, you won't need a comment field in the PostSchema unless you're using it as a "most recent" style field (or some other way where there'd only be one). The fact that you have a Post reference in the Comment schema would be sufficient to find all Comments for a given Post:
Comments.find().where("post_id", myPostId).exec(/* callback */);
You'd want to make sure that the field is indexed. As you can use populate with this as you've specified the ref for the field, you might consider renaming the field to "post".
Comments.find().where("post", myPostId).exec(/* callback */);
You'd still only set the post field to the _id of the Post though (and not an actual Post object instance).
You could also choose to embed the comments in the Post. There's some good information on the MongoDB web site about these choices. Note that even if you embedded the comments, you'd need to bring back the entire array just to get the count.

It looks like your Post schema will only allow for a single comment:
// ....
comment: {
type: Schema.Types.ObjectId,
ref: 'CommentSchema'
},
// ....
One consideration is to just store your comments as subdocuments on your posts rather than in their own collection. Will you in general be querying your comments only as they related to their relevant post, or will you frequently be looking at all comments independent of their post?
If you move the comments to subdocuments, then you'll be able to do something like post.comments.length.
However, if you retain comments as a separate collection (relational structure in a NoSQL DB-- there are sometimes reasons to do this), there isn't an automatic way of doing this. Mongo can't do joins, so you'll have to issue a second query. You have a few options in how to do that. One is an instance method on your post instances. You could also just do a manual CommentSchema.count({postId: <>}).
Your proposed solution is perfectly valid too. That strategy is used in relational databases that can do joins, because it would have better performances than counting up all the comments each time.

Related

Setting up a complex comment model in NodeJs and mongoose

I am setting up a comment model where users can post comments reference and can also reply. the complication comes with the reply part. I want users to be able to reply to comments or others' replies, and I am lost on how to set up my model for that.
How should I set up my model to be able to capture that data in my reply?
also, any other suggestion would be appreciated
Here is the model I am currently setting up
const mongoose = require('mongoose')
const commentSchema = new mongoose.Schema({
owner: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
reference: {
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: 'Project' || null,
default: false
},
body: {
type: String,
required: true,
trim: true
},
reply: {
owner: {
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: 'User'
},
body: {
type: String,
required: true
}
}
}, {
timestamps: true
})
const Comment = mongoose.model('Comment', commentSchema)
module.exports = Comment
If you are thinking about a model where we have
some post
>commentA
>replyA-a
>replyA-a-a
>replyA-a-a-a
>replyA-b
>commentB
>commentC
I would aggregate everything for the corresponding entity
Comment {
user,
body,
replies: [Comment] // pattern composite
}
EntityComment { // only persist this one
reference: { id, type: post|topic|whatever },
comment: [Comment]
}
Props are:
an entityComment can grow big (is this problematic?)
no need for multiple fetch, everything's there
easy to "hide" some comments and just show its count (array length)
If record entityComment becomes too big (the max record length seems to be 16MB so likely not be the limit, but maybe the payload is slow to load), then
we can think of saving each comment (using replies: [{ ref: Comment, type: ObjectId)}])
but maybe a better idea is to use a reference for body (body: [ref: CommentBody, type: ObjectId])
The reason is body is likely the culprit (datasize wise), and this would allow to
keep everything nested in entityComment
delay the fetch of the bodies we are interested in (not the whole hierarchy)
There are tradeoffs:
is fine for read
is simpler for writes (just update/delete a singular comment)

how reuse a like model in nodejs

I have 2 models named "posts" and "status" and want to implement likes in them. first of all, I want the like to be able to record data like timestamps and other stuff depending on how it grows, which is why I made "like" be a model of its own.
The issue is since "posts" and "status" two models of their own are going to have "like" functionality.
Is there a way I could reuse the "like" model, instead of creating a separate "like" model for "posts" and "status", or how would you personally implement something like this?
Below is the post model
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true,
trim: true
},
owner: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
}
}, {
timestamps: true
})
below is the status model
const statusSchema = new mongoose.Schema({
Body: {
type: String,
required: true,
trim: true
},
owner: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
tags: [{
type: mongoose.Schema.Types.ObjectId,
required: True,
ref: 'User'
}]
}, {
timestamps: true
})
here is the like model which I would like users to be able to like both posts by users and statuses, while still able to retain information like the time it was liked and other information depending on the growth and need
const likeSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
likedObject: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Posts'
}
}, {
timestamps: true
})
Is there a way I could reuse the "like" model, instead of creating a separate "like" model for "posts" and "status" to capture the users and the time that they liked other user's statuses and posts?
Making Like Model was over complicating the entire thing so you could have something like this in your postSchema.
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true,
trim: true
},
owner: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
timeStamps:true,
//Adding the like property in each post but setting it to a default of 0
likes:{type:Number, default:0}
})
Now the key is identifying the post when like button is smashed
So what you could do on the client side is making sure that when like button is clicked you have an ID of the post to the function that your calling and, then pass that ID back to the server and you can have a logic like this below on your server and you would be able to add like functionality..
Server logic for adding like functionality
router.post("/api/likes/:id", async (request, response) => {
const post_id = request.params.id;
const post = await postModel.findOne({ _id: post_id });
post.likes += 1;
const updateDocument = await postModel.findOneAndUpdate(
{ _id: post_id },
post,
{
new: true,
}
);
return response.status(201).json({ msg: "Liked post" });
});
So the idea is two always be updating that specific document

Get info from 2 separate Mongo documents in one mongoose query

Im using MongoDb, and I have a workspace schema with mongoose (v4.0.1):
var Workspace = new mongoose.Schema({
name: {
type: String,
required: true
},
userId: {
type: String,
required: true
},
createdOn: {
type: Date,
"default": Date.now
}
});
And a user schema:
var User = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
organisation: {
type: String,
required: true
},
location: {
type: String,
required: true
},
verifyString: {
type: String
},
verified: {
type: Boolean,
default: false
},
password: {
type: String,
required: true
},
createdOn: {
type: Date,
"default": Date.now
},
isAdmin: {
type: Boolean,
default: false
}
});
So the Workspace userId is the ObjectID from the User document.
When Im logged in as an adminstrator, I want to get all workspaces, as well as the email of the user that owns the workspace.
What Im doing is getting very messy:
Workspace.find({}).exec.then(function(workspaceObects){
var userPromise = workspaceObects.map(function(workspaceObect){
// get the user model with workspaceObect.userId here
});
// somehow combine workspaceObjects and users
});
The above doesnt work and gets extremely messy. Basically I have to loop through the workspaceObjects and go retrieve the user object from the workspace userId. But because its all promises and it becomes very complex and easy to make a mistake.
Is there a much simpler way to do this? In SQL it would require one simple join. Is my schema wrong? Can I get all workspaces and their user owners email in one Mongoose query?
var Workspace = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'User' //add this to your schema
}
});
Workspace.find().populate('userId').exec( (err, res) => {
//you will have res with all user fields
});
http://mongoosejs.com/docs/populate.html
Mongo don't have joins but mongoose provides a very powerfull tool to help you with you have to change the model a little bit and use populate:
Mongoose population
You have to make a few changes to your models and get the info of the user model inside your workspace model.
Hope it helps

meanjs controller for subdocument

I have architectural question about how to design my meanjs controller and routes for mongoose subdocuments.
my model looks as following:
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Customerrpc Schema
*/
var CustomerrcpSchema = new Schema({
company: {
type: String,
enum: ['Option1', 'Option2'],
required: 'Please fill company name'
},
rcp: {
type: String,
required: 'Please fill rcp'
},
created: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
/**
* Customer Schema
*/
var CustomerSchema = new Schema({
name: {
type: String,
default: '',
required: 'Please fill Customer name',
trim: true
},
description: {
type: String,
default: '',
//required: 'Please fill Customer description',
trim: true
},
url: {
type: String,
default: '',
//required: 'Please fill Customer url',
trim: true
},
rcp: [CustomerrcpSchema],
created: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
mongoose.model('Customer', CustomerSchema);
mongoose.model('Customerrcp', CustomerrcpSchema);
I tried it out by adding on the server controller the following code during the create methode:
exports.create = function(req, res) {
var customer = new Customer(req.body);
customer.user = req.user;
var rcp = new Customerrcp({
company: 'Option1',
rcp: 'dumm',
user: req.user
});
customer.rcp = rcp;
customer.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(customer);
}
});
};
This works perfectly fine. Now my question is what is the best procedure to create / modify / remove a subdocument from the maindocument?
I thought of always work with the main document 'Customer' but this brings several issues with it that i dont like, like saving always the hole document. Since i do have a uniq _id for each subdocument i guess there must be a better way.
What i would like to have is a controller only for the subdocument with the create / save / remove statement for it. Is this even possible?
As far as i understand it:
to create a new subdocument i need the following: _id of the main document for the mongoose query. So i need a service which would handover the _id of the maindocument to the controller of the selected subdocument. I was able to do this.
But im insecure if this is the proper way.
Any Ideas?
Cheers,
Michael

Mongoose Populate - array

can someone please help me with population of this schema? I need to populate array of Staff by their userId.
var PlaceSchema = new Schema ({
name: { type: String, required: true, trim: true },
permalink: { type: String },
country: { type: String, required: true },
...long story :D...
staff: [staffSchema],
admins: [adminSchema],
masterPlace:{ type: Boolean },
images: []
});
var staffSchema = new Schema ({
userId: { type: Schema.Types.ObjectId, ref: 'Account' },
role: { type: Number }
});
var adminSchema = new Schema ({
userId: { type: Schema.Types.ObjectId, ref: 'Account'}
})
var Places = mongoose.model('Places', PlaceSchema);
I tried to use this query, but without success.
Places.findOne({'_id' : placeId}).populate('staff.userId').exec(function(err, doc){
console.log(doc);
});
Polpulation is intended as a method for "pulling in" information from the related models in the collection. So rather than specifying a related field "directly", instead reference the related fields so the document appears to have all of those sub-documents embedded in the response:
Places.findOne({'_id' : placeId}).populate('staff','_id')
.exec(function(err, doc){
console.log(doc);
});
The second argument just returns the field that you want. So it "filters" the response.
There is more information on populate in the documentation.

Resources