I am building an API in node.js which uses mongodb and mongoose. Currently I have an embedded document within an embedded document (Schema within a Schema) that is simply not persisted to the database, and I have tried all I can but no luck.
I have the Schema's defined in mongoose as:
var BlogPostSchema = new Schema({
creationTime: { type: Date, default: Date.now },
author: { type: ObjectId, ref: "User" },
title: { type: String },
body: { type: String },
comments: [CommentSchema]
});
var CommentSchema = new Schema({
creationTime: { type: Date, default: Date.now },
user: { type: ObjectId, ref: "User" },
body: { type: String, default: "" },
subComments: [SubCommentSchema]
});
var SubCommentSchema = new Schema({
creationTime: { type: Date, default: Date.now },
user: { type: ObjectId, ref: "User" },
body: { type: String, default: "" }
});
And the code I execute is as follows:
// Create a comment
app.post("/posts/:id/comments", function(req, res, next) {
Posts.find({ _id : req.params.id }, function(err, item){
if(err) return next("Error finding blog post.");
item[0].comments.push(new Comment(JSON.parse(req.body)));
item[0].save(); // <= This actually saves and works fine
respond(req, res, item[0].comments, next);
});
});
// Create a subcomment
app.post("/posts/:id/comments/:commentid/subcomments", function(req, res, next) {
Posts.find({ _id : req.params.id }, function(err, item){
if(err) return next("Error finding blog post.");
item[0].comments[req.params.commentid - 1].subcomments.push(new SubComment(JSON.parse(req.body)));
item[0].save(); // <= This completes (without error btw) but does not persist to the database
respond(req, res, item[0].comments[req.params.commentid - 1].subcomments, next);
});
});
I can create Blog Posts with comments without problem, but for some reason I can not create subcomments on a comment. The Blog Post document actually has the comments and subcomments attached when printing to the console during execution - only it does not save to the database (it saves the Blog Post with a comment, but no subcomments).
I have tried to "markModified" on the comments array, but no change:
Posts.markModified("comments"); // <= no error, but also no change
...
Posts.comments.markModified("subcomments"); // <= produces an error: "TypeError: Object [object Object] has no method 'markModified'"
Problem since solved. I was given the answer by Aaron Heckmann over on the mongoose Google Group:
Always declare your child schemas before passing them to you parent schemas otherwise you are passing undefined.
SubCommentSchema should be first, then Comment followed by BlogPost.
After reversing the schemas it worked.
I thing the updating of a document is not an important issue as the embedded documents are also fully capable of any services.
Related
In my Nodejs and Express app, I have a mongoose User schema, a Post schema and a Comment schema as follows:
const UserSchema = new Schema({
username: {
type: String,
required: true,
unique: true
},
password: String,
posts : [
{
type : mongoose.Schema.Types.ObjectId,
ref : 'Post'
}
]
});
const PostSchema = new Schema({
author : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
createdAt: { type: Date, default: Date.now },
text: String,
comments : [
{
type : mongoose.Schema.Types.ObjectId,
ref : 'Comment'
}
],
});
const CommentSchema = new Schema({
author : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
createdAt: { type: Date, default: Date.now },
text: String
});
I have coded the general CRUD operations for my User. When deleting my user, I can easily delete all posts associated with that user using deleteMany:
Post.deleteMany ({ _id: {$in : user.posts}});
To delete all the comments for all the deleted posts, I can probably loop through posts and delete all the comments, but I looked at mongoose documentation here and it seems that deleteMany function triggers the deleteMany middleware. So In my Post schema, I went ahead and added the following after defining schema and before exporting the model.
PostSchema.post('deleteMany', async (doc) => {
if (doc) {
await Comment.deleteMany({
_id: {
$in: doc.comments
}
})
}
})
When deleting user, this middleware is triggered, but the comments don't get deleted. I got the value of doc using console.log(doc) and I don't think it includes what I need for what I intend to do. Can someone tell me how to use the deleteMany middleware properly or if this is not the correct path, what is the most efficient way for me to delete all the associated comments when I delete the user and their posts?
deleteMany will not give you access to the affected document because it's a query middleware rather than a document middleware (see https://mongoosejs.com/docs/middleware.html#types-of-middleware). Instead it returns the "Receipt-like" object where it tells it successfully deleted n objects and such.
In order for your hook to work as expected, you'll need to use something other than deleteMany, such as getting all of the documents (or their IDs), and loop through each one, using deleteOne.
I have been following a tutorial on the MDN site for Node/ express/ mongoose. It may be familiar to many people but I will put the code down anyway. What I want to do is create a view that is similar to the book_list page, however, I wish to have the ability to send the book instances with each book (details will follow). In other words I wish to be able to have the BookInstances for each book as part of the book object on the list page - it is mainly for the count (or length) but I may wish to also use it in other ways.
The book model
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var BookSchema = new Schema({
title: {type: String, required: true},
author: { type: Schema.ObjectId, ref: 'Author', required: true },
summary: {type: String, required: true},
isbn: {type: String, required: true},
genre: [{ type: Schema.ObjectId, ref: 'Genre' }]
});
// Virtual for this book instance URL.
BookSchema
.virtual('url')
.get(function () {
return '/catalog/book/'+this._id;
});
// Export model.
module.exports = mongoose.model('Book', BookSchema);
BookInstance Schema part of the Model:
var BookInstanceSchema = new Schema(
{
book: { type: Schema.Types.ObjectId, ref: 'Book', required: true },//reference to the associated book
imprint: { type: String, required: true },
status: { type: String, required: true, enum: ['Available', 'Maintenance', 'Loaned', 'Reserved'], default: 'Maintenance' },
due_back: { type: Date, default: Date.now }
}
);
The book_list controller:
// Display list of all Books.
exports.book_list = function(req, res, next) {
Book.find({}, 'title author')
.populate('author')
.exec(function (err, list_books) {
if (err) { return next(err); }
//Successful, so render
res.render('book_list', { title: 'Book List', book_list: list_books });
});
};
The book detail controller:
// Display detail page for a specific book.
exports.book_detail = function(req, res, next) {
async.parallel({
book: function(callback) {
Book.findById(req.params.id)
.populate('author')
.populate('genre')
.exec(callback);
},
book_instance: function(callback) {
BookInstance.find({ 'book': req.params.id })
.exec(callback);
},
}, function(err, results) {
if (err) { return next(err); }
if (results.book==null) { // No results.
var err = new Error('Book not found');
err.status = 404;
return next(err);
}
// Successful, so render.
res.render('book_detail', { title: 'Book Detail', book: results.book,
book_instances: results.book_instance } );
});
};
I have a feeling it must be something that can maybe be done with populate but I have not got that to work. The only way I have managed to get the book instance object to appear in the list for each book item is to send all book instances to the view. From there I use a foreach loop and then IF statement to get the book instances for each book. It looks really ugly and I am sure there must be some other way to do this. I am used to asp.net mvc - in that you use a virtual object. I am not sure if I am supposed to modify the model here or the controller. I may also want to pass in a much more complex model with lists within lists.
I have noted the genre is actually saved into the book document unlike bookinstances - hence the lines in the book detail controller:
book_instance: function(callback) {
BookInstance.find({ 'book': req.params.id })
.exec(callback);
},
Below I have shown what I have done. I could also have done this as objects in the controller but this is what I have now:
Book Controller:
exports.book_list = function (req, res, next) {
async.parallel({
books: function (callback) {
Book.find()
.exec(callback)
},
bookinstances: function (callback) {
BookInstance.find()
.exec(callback)
},
}, function (err, results) {
if (err) { return next(err); } // Error in API usage.
// Successful, so render.
res.render('book_list', { title: 'Book Detail', books: results.books,
bookinstances: results.bookinstances });
});
};
book_list.pug code:
extends layout
block content
h1= title
table.table
th Book
th BookInstance Count
th
//- above th is for buttons only (no title)
each book in books
- var instCount = 0
each bookinstance in bookinstances
if book._id.toString() === bookinstance.book.toString()
- instCount++
tr
td
a(href=book.url) #{book.title}
td #{instCount}
td
a.btn.btn-sm.btn-primary(href=book.url+'/update') Update
if !instCount
a.btn.btn-sm.btn-danger(href=book.url+'/delete') Delete
else
li There are no books.
What the page comes out as:
The problem was identified as me trying to use MongoDb like a relational database and not a document type. The solution to this problem is to use an array of the BookInstances in the Book document in the same way as genre:
var BookSchema = new Schema({
title: {type: String, required: true},
author: { type: Schema.ObjectId, ref: 'Author', required: true },
summary: {type: String, required: true},
isbn: {type: String, required: true},
genre: [{ type: Schema.ObjectId, ref: 'Genre' }],
bookInstances: [{ type: Schema.ObjectId, ref: 'BookInstance' }]
});
All the details can be kept in the BookInstance document still because the _id is all that is required in the Book document. Whenever a BookInstance is added it can be pushed onto the Book/ BookInstances array (this post helps: Push items into mongo array via mongoose). This does also mean that the BookInstance will need to be deleted (pulled) from the array as well as the document that contains its details.
Now the mongoose populate() can be used in the normal way.
I'm having trouble with population all of a sudden (was working fine before I updated Mongoose package version). Currently using Mongoose 4.7.6.
var userSchema = require('schemas/user'),
User = db.model('User', userSchema); // db is already known
User
.findById(user._id) // user is already known due to auth
.populate('group currentPlayer')
.exec(function (findErr, userPlayer) { ... });
If my Schema for User is necessary, I will post it, but currently haven't due to its length (virtuals, methods, statics).
Error:
/app/node_modules/mongoose/lib/model.js:2986
var virtual = modelForCurrentDoc.schema._getVirtual(options.path);
^
TypeError: modelForCurrentDoc.schema._getVirtual is not a function
at getModelsMapForPopulate (/app/node_modules/mongoose/lib/model.js:2986:49)
at populate (/app/node_modules/mongoose/lib/model.js:2660:15)
at _populate (/app/node_modules/mongoose/lib/model.js:2628:5)
at Function.Model.populate (/app/node_modules/mongoose/lib/model.js:2588:5)
at Immediate.<anonymous> (/app/node_modules/mongoose/lib/query.js:1275:17)
...
User Schema
var
Mongoose = require('mongoose'),
Bcrypt = require('bcrypt'),
ObjectID = Mongoose.Schema.Types.ObjectId,
UserSchema = new Mongoose.Schema({
active : { type: Boolean, default: true },
created : { type: Date, required: true, default: Date.now },
modified : { type: Date, required: true, default: Date.now },
createdBy : { type: ObjectID, ref: 'User' },
modifiedBy : { type: ObjectID, ref: 'User' },
email : { type: String, required: true },
salt : { type: String },
hash : { type: String },
session : String,
group : { type: ObjectID, ref: 'Group', required: true },
currentPlayer : { type: ObjectID, ref: 'Player' },
validated : { type: Boolean, default: false },
ipAddress : String,
lastIp : String,
notes : String
});
var _checkPassword = function (password) { ... };
UserSchema.pre('validate', function (next) {
if (this.password && !_checkPassword(this.password)) {
this.invalidate('password', 'invalid', "Six character minimum, must contain at least one letter and one number or special character.");
}
next();
});
UserSchema.pre('save', function (next) {
this.modified = Date.now();
next();
});
UserSchema.virtual('password')
.get(function () { return this._password; })
.set(function (passwd) {
this.salt = Bcrypt.genSaltSync(10);
this._password = passwd;
this.hash = Bcrypt.hashSync(passwd, this.salt);
});
UserSchema.method('verifyPassword', function (password, done) {
Bcrypt.compare(password, this.hash, done);
});
UserSchema.static('authenticate', function (email, password, done) {
...
});
module.exports = UserSchema;
If anyone comes across this problem, it is probably because you have multiple package.json files with mongoose as a dependency in two of them. Make sure you use one package version of mongoose in your project and register your models there.
I have now filed a bug on GitHub, since reverting to version 4.6.8 allows my application to work again. https://github.com/Automattic/mongoose/issues/4898
After upgrading to Mongoose v4.7, I now receive an error when populating documents.
The chain of events to reproduce this error:
- define a Schema in its own file and use module.exports on the defined Schema object
- require() the Schema file
- use mongoose.model() to build a model from this Schema
- attempt to retrieve a record by using find() and populate()
- TypeError: modelForCurrentDoc.schema._getVirtual is not a function
If I do NOT use an "external" Schema file, and instead define the Schema inline, the problem goes away. However, this is not tenable due to statics, methods and virtuals defined in many Schemas.
Α possible answer can be found here:
The error was raised because I had a field called id that probably was
overriding the internal _id field.
Source : https://stackoverflow.com/a/53569877/5683645
Here is my schema:
/** Schemas */
var profile = Schema({
EmailAddress: String,
FirstName: String,
LastName: String,
BusinessName: String
});
var convSchema = Schema({
name: String,
users: [{
type: Schema.Types.ObjectId,
ref: 'Profiles'
}],
conversationType: {
type: String,
enum: ['single', 'group'],
default: 'single'
},
created: {
type: Date,
default: Date.now
},
lastUpdated: {
type: Date,
default: Date.now
}
});
/** Models */
db.Profiles = mongoose.model('Profiles', profile);
db.Conversations = mongoose.model('ChatConversations', convSchema);
module.exports = db;
Then I try to populate Users using following code (http://mongoosejs.com/docs/populate.html):
db.Conversations.find(query).populate('users').exec(function (err, records) {
console.log(records);
});
This is returning records but users array as a blank array [].
I also tried the other way around (http://mongoosejs.com/docs/api.html#model_Model.populate):
db.Conversations.find(query, function (err, records) {
db.Conversations.populate(records, {path: "users", select: "BusinessName"}, function (err, records) {
console.log(records);
});
});
Results are same. When I checked references into profile collection records are there.
Any idea what wrong here?
I got it working by renaming model (the 3rd arguement):
mongoose.model( "Profiles", profile, "Profiles" );
The issue was Mongoose was searching for profiles collection but its there as Profiles in database. So I renamed it to Profiles to match the exact name.
Phewww! Thanks to me.
I have a model called Shop whos schema looks like this:
'use strict';
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var ShopSchema = new Schema({
name: { type: String, required: true },
address: { type: String, required: true },
description: String,
stock: { type: Number, default: 100 },
latitude: { type: Number, required: true },
longitude: { type: Number, required: true },
image: String,
link: String,
tags: [{ type: Schema.ObjectId, ref: 'Tag' }],
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Shop', ShopSchema);
I want to use the array tags to reference to another model via ObjectId obviously. This set up works fine when I add ids into the property via db.shops.update({...}, {$set: {tags: ...}}) and the ids get set properly. But when I try to do it via the Express.js controller assigned to the model, nothing gets updated and there even is no error message. Here is update function in the controller:
// Updates an existing shop in the DB.
exports.update = function(req, res) {
if(req.body._id) { delete req.body._id; }
Shop.findById(req.params.id, function (err, shop) {
if (err) { return handleError(res, err); }
if(!shop) { return res.send(404); }
var updated = _.merge(shop, req.body);
shop.updatedAt = new Date();
updated.save(function (err) {
if (err) { return handleError(res, err); }
return res.json(200, shop);
});
});
};
This works perfect for any other properties of the Shop model but just not for the tags. I also tried to set the type of the tags to string, but that didn't help.
I guess I am missing something about saving arrays in Mongoose?
It looks like the issue is _.merge() cannot handle merging arrays properly, which is the tags array in your case. A workaround would be adding explicit assignment of tags array after the merge, if it is ok to overwrite the existing tags.
var updated = _.merge(shop, req.body);
if (req.body.tags) {
updated.tags = req.body.tags;
}
Hope this helps.. If the workaround is not sufficient you may visit lodash forums.