Understanding Mongoose sub documents - javascript

I'm learning Mongoose. At the moment I did few nice things but I really don't understand exactly how Mongoose manage relationships between Schemas.
So, easy thing (I hope): I'm doing a classic exercise (by my self because I cannot find a good tutorial that create more than 2 Schemas) with 3 Schemas:
User, Post, Comment.
User can create many Post;
User can create many Comment;
Post belong to User.
Comment belong to User and Post.
I don't think it is something very hard uhu?
At the moment I can manage very well Relation between User and Post. My Unit test return exactly what I need, at the moment I'm using mongo-relation and I don't know if it is a good idea...
it('Use should create a Post', function(done) {
User.findOne({ email: 'test#email.com' }, function(err, user) {
var post = {
title: 'Post title',
message: 'Post message',
comments: []
};
user.posts.create(post, function(err, user, post) {
if (err) return done(err);
user.posts[0].should.equal(post._id);
post.author.should.equal(user._id);
// etc...
done();
});
});
});
The problem now is to create a comment.
I can not create a comment that refere to the Post and to the User together.
I did something like that and works but when I perform a remove it is removed only from the Post and not from the User.
So I think there is something I miss or I still need to study to enhance it.
it('User should add a Comment to a Post', function(done) {
User.findOne({ email: 'test#email.com' }, function(err, user) {
if (err) return done(err);
var comment = new Comment({
author: user._id,
message: 'Post comment'
});
Post.findOne({ title: 'Post title'}, function(err, post) {
if (err) return done(err);
post.comments.append(comment, function(err, comment) {
if (err) return done(err);
post.save(function(err) {
if (err) return done(err);
});
comment.author.should.equal(user._id);
post.comments.should.have.length(1);
// etc...
done();
});
});
});
});
As you can see the code is not very "nice to see" but it works fine in terms of creations.
The problem is when I remove a Comment. It seems like something is wrong.
Here is the Model relationship:
// User Schema
var userSchema = new mongoose.Schema({
// [...],
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }],
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
});
// Post Schema
var postSchema = new mongoose.Schema({
author: { type: mongoose.Schema.ObjectId, ref: 'User', refPath: 'posts' },
title: String,
message: String,
comments: [{ type: mongoose.Schema.ObjectId, ref: 'Comment' }]
});
// Comment Schema
var commentSchema = new mongoose.Schema({
author: { type: mongoose.Schema.ObjectId, ref: 'User', refPath: 'comments' },
post: { type: mongoose.Schema.ObjectId, ref: 'Post', refPath: 'comments' },
message: String
});
I really hope in your help to understand all this.
It will be nice also a simple good tutorial about it.

I think you are misunderstanding subdocuments. The way you have your schema setup you are creating references to documents in other collections.
For example if you create a post, in the database it will look like this:
{
"author": ObjectId(123),
"title": "post title",
"message": "post message",
"comments": [ObjectId(456), ObjectId(789)]
}
Notice the "author" field just contains the ID of the author who created it. It does not actually contain the document itself.
When you read the document from the DB you can use the mongoose 'populate' functionality to also fetch the referred to document.
Ex (with populate):
Post
.findOne({ title: 'Post title'})
.populate('author', function(err, post) {
// this will print out the whole user object
console.log(post.author)
});
Ex (no populate):
Post
.findOne({ title: 'Post title'}, function(err, post) {
// this will print out the object ID
console.log(post.author)
});
Subdocuments:
You can actually nest data in the DB using subdocuments, the schema would look slightly different:
var postSchema = new mongoose.Schema({
author: { userSchema },
title: String,
message: String,
comments: [commentSchema]
});
When saving a post the user document would be nested inside the post:
{
"author": {
"name": "user name",
"email": "test#email.com"
...
},
"title": "post title",
"message": "post message",
"comments": [{
"message": "test",
...
}, {
"message": "test",
...
}]
}
Subdocuments can be useful in mongo, but probably not for this case because you would be duplicating all of the user data in every post.
Removing documents
When you issue a Comment.remove(id) the comment will be removed but it will not affect the other documents referring to it. So you will then have a Post and a User with a comment ID that does not exist. You need to manually clean up the comment ID from the other documents. You could use the mongoose pre remove event to do this. http://mongoosejs.com/docs/middleware.html
commentSchema.pre('remove', function (next) {
// this refers to the document being removed
var userId = this.author;
var postId = this.post;
User.findById(userId, function(err, user) {
// remove comment id from users.comments here;
Post.findById(postId, function(err, post) {
// remove comment id from post.comments;
next();
});
});
});

Related

Mongoose cast to ObjectID failed for value... but why

I know what the problem is, but can't figure out why it is happening. I have a simple recipe app using express and mongoose. User passes in recipe info via form and is saved to database via mongoose methods. This part seems to work perfectly and when I console.log using test data, I see that the following data is saved:
{
ingredients: [ 'peanut butter', 'jelly', 'bread' ],
_id: 5e47d564f775ce247052d01c,
name: 'pb jelly sammich',
author: 'rob',
oneLiner: 'classic pb jelly sammich',
image: 'picofpbsammich here',
method: 'add all the ingredients together and boom! pb jelly sammich.',
__v: 0
}
(This is also what shows when I check mongo db using db.recipes.find() and also what displays when I pass in the object to my ejs show template.
However, when I access my show route via get request, I get a long error message using the above test data. Here is they key part of the error message:
'Cast to ObjectId failed for value "picofpbsammich here" at path "_id" for model "Recipes"',
I understand what the problem is, but baffled as to why it is happening. Here is my show route:
app.get("/recipes/:id", function (req, res) {
console.log(req.params.id)
Recipe.findById(req.params.id, function (err, foundRecipe) {
if (err) {
console.log(err);
} else {
res.render("show", { recipe: foundRecipe });
}
})
})
console logging the req.params.id as shown above, prints the following:
5e47d564f775ce247052d01c
picofpbsammich here
The first line is the correct ID, the second is obviously not and the cause of the problem, but I have no idea where that could be coming from :S Why would req.params.id be pulling the VALUE of a property that is named something completely different?
I'm new to mongoose so it's probably something silly I'm doing and any explanations appreciated.
Here is the model:
var mongoose = require("mongoose");
let recipeSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
author: String,
oneLiner: String,
ingredients: [String],
image: String,
method: String
})
module.exports = mongoose.model("Recipes", recipeSchema)
You posted the following code:
app.get("/recipes/:id", function (req, res) {
console.log(req.params.id)
Recipe.findById(req.params.id, function (err, foundRecipe) {
if (err) {
console.log(err);
} else {
res.render("show", { recipe: foundRecipe });
}
})
})
And you mention that in the console.log you receive:
5e47d564f775ce247052d01c
picofpbsammich here
Followed by the exception being logged:
'Cast to ObjectId failed for value "picofpbsammich here" at path "_id"
for model "Recipes"',
Makes me logically assume that you are making two requests, one of which the id is not valid, being:
picofpbsammich here
Mongoose is not able to cast this value to an ObjectId, hence you get the exception, which makes sense imo.

POSTING an array of objects to node.js/save to database using mongoose

I have a ReactJS form, in which you can dynamically add form "parts"(sections with form input). Here's an example of what I mean by "parts":
<div>
<input placeholder="Author" />
<input placeholder="Age" />
<input placeholder="Amount of books written" />
</div>
Something like this. You can add as many of these divs as you like.
I'm saving the values of these inputs in the state, which gives me a nested array like so:
this.state = {
formdata : [
{author: "somebody", age: 34, books: 0},
{author: "somebody else", age: 100, books: 1}
]
}
Right now I'm use axios post to post the data to node.js with express. This is my post function:
handleSubmit(e) {
axios.post('receivedata',
querystring.stringify({
formdata : this.state.formdata
}), {
headers : {"Content-Type": "application/x-www-form-urlencoded"}
}
)
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
And this is the mongoose schema that I use:
var EntrySchema = new mongoose.Schema({
formdata: [{type:String}],
updated_at: {type: Date, default: Date.now}
});
and this is how I (try to) send the data to my database:
router.post("/", function(req, res) {
newEntry.formdata = req.body.formdata;
newEntry.save(function (err) {
if(err)
res.send(err);
res.send("Entry added successfully!");
});
});
That doesn't work though and when I check the database, I receive an array with an empty string like so: formdata:[{""}]
I think the problem is with how the schema is set up, since, when I do console.log(this.state.formdata) it correctly returns the data. My next guess would be that axios is not able to send nested array, but after some research I found that that's not the case so I'm assuming that there's a special way to define nested arrays in the mongoose schema? How would I go about that?
Edit: I was thinking that maybe I could do something along the lines of:
var EntrySchema = new mongoose.Schema({
formdata: [{
author: String,
age: Number,
books: Number
}],
updated_at: {type: Date, default: Date.now}
});
I tried this and it doesn't work either. Now, I don't know if I'm on the right track or how else to do this.
I also tried changing the Content-Type in the header to "application/ json", as suggested in another answer. Once again, it didn't work.
Any help is appreciated.
Okay so after some playing around I figured it out: I used querystring.stringify() before, after changing it to JSON.stringify() it worked perfectly for me.

Modify user signup behaviour in meteor accounts

I am following the "To-do list" tutorial for meteor and trying to make a few changes to it. I am trying to add a few fields by default to a collection called records(same as the collection tasks until now) when she signs up.
I came across this and wrote the following piece of code in startup/accounts-config.js
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { Rec } from '../api/records.js';
Accounts.ui.config({
passwordSignupFields: 'USERNAME_ONLY',
});
Accounts.onCreateUser(function(options, user) {
Rec.insert({
"text",
createdAt: new Date(),
owner: this.userId,
username: Meteor.users.findOne(this.userId).username,
});
return user;
});
But my application wouldn't compile and throw this error
imports/startup/accounts-config.js:12:12: Unexpected token (12:12)
Could someone please help me with this? I am new to front end development.
"text" is sitting on it's own without a value, you probably mean this:
text: "something goes here...",
Its probably because you have this.userId. You should use user._id there which is the created user object and user.username for the username.
Rec.insert({
"text",
createdAt: new Date(),
owner: user._id,
username: user.username,
});
As Mikkel said,
There should always be a field and value pair in a query ,
You are passing only value here , This should be like
Accounts.onCreateUser(function(options, user) {
Rec.insert({
field:"text",
createdAt: new Date(),
owner: this.userId,
username: Meteor.users.findOne(this.userId).username,
});

Nested mongoose Schema giving trouble when trying to query in controller

I'm working on a small project and I have a solution to this problem, but it involves creating a new Schema with a reference to the new Schema in the old Schema. I would like to avoid this if at all possible because it will mean spending a couple hours rewriting some code and messing with tests.
The project is a forum site, and there are three main Schemas that comprise it (in addition to cursory Schemas for the forums, notifications, settings and the schemas for the user and the users activities). The Board Schema (contains a list of all forum sections if that wasn't apparent) Is a Schema that makes a reference to the Threads Schema so it can get the threads for each Board. My problem is in the Thread Schema.
var ThreadSchema = new mongoose.Schema({
... other unrelated Schema stuff...
comments: [{
created: {
type: Date,
default: Date.now
},
creator: {
type: mongoose.Schema.ObjectId,
required: true,
ref: 'User'
},
content: {
type: String,
required: true,
get: escapeProperty
},
likes: [{
type: mongoose.Schema.ObjectId,
required: false,
ref: 'User'
}],
liked: {
type: Boolean,
default: false
},
saved: [{
type: mongoose.Schema.ObjectId,
required: false,
ref: 'User'
}]
}]
});
blah blah blah.
I'm trying to pull for the users profile only the comments that that user has posted. The threads were easy, but comment data is not coming through. The request to the server goes through as successful, but I don't get any data back. This is what I am trying.
obj.profileComments = function (req, res) {
var userId = req.params.userId;
var criteria = {'comments.creator': userId};
Thread.find(criteria)
.populate('comments')
.populate('comments.creator')
.skip(parseInt(req.query.page) * System.config.settings.perPage)
.limit(System.config.settings.perPage + 1)
.exec(function (err, threads) {
if (err) {
return json.bad(err, res);
}
json.good({
records: threads
}, res);
});
};
This is a controller, and the json.bad and json.good are helpers that I have created and exported they basically are res.send.
var good = function (obj, res) {
res.send({
success: 1,
res: obj
});
};
and the bad is very similar, it just handles errors in an obj.res.errors and puts them into messages.
So now that that is all out of the way, I'm a little lost on what to do?
Is this something I should try to handle with a method in my Schema? It seems like I might have a little bit more luck that way. Thank you for any help.

Waterline queries similar to HQL

I have models in Sails as follows:
User Model
attributes: {
firstName: {
type: 'string',
required: true
},
lastName: {
type: 'string',
required: true
},
company: {
model: 'Company'
}
}
Company
name: {
type: 'string',
required: true,
unique: true
},
description: {
type: 'string',
required: true
}
In HQL queries, for getting a user working in a specific company, we use something like this:
Select * from User where company.name=?
How can I achieve same in Sails, Right now there are two queries which I am running, one to get User and then another to get company for that user. Is there any way both can be combined in one?
and one more thing, how does waterline deal with scenarios where in we need to get something out of a foreign key directly i.e. If I get user data, then can I get company details by just calling:
User.findOne({id:1}, function(err, res){
//Res contains user data with just companyId now,
//how can i get whole company object here
var companyName = res.company.name; //This is not working currently
})
Try something like this:
User.findOne({id: 1})
.populate('company')
.exec(function(err, user) {
if(err){
//handle errors
}else{
//do stuff
}
});
This should get the values from the association (foreign key).

Categories

Resources