MongoDB mongoose subdocuments created twice - node.js

I am using a simple form that can be used to register an article to a website.
the back-end looks like this:
// Post new article
app.post("/articles", function(req, res){
var newArticle = {};
newArticle.title = req.body.title;
newArticle.description = req.body.description;
var date = req.body.date;
var split = date.split("/");
newArticle.date = split[1]+'/'+split[0]+'/'+split[2];
newArticle.link = req.body.link;
newArticle.body = req.body.body;
var platforms = req.body.platforms;
console.log(platforms);
Article.create(newArticle, function(err, createdArticle){
if(err){
console.log(err.message);
} else {
var counter=0;
platforms.forEach(function(platform){
var platformed=mongoose.mongo.ObjectID(platform);
Platform.findById(platformed, function(err, foundPlatform){
if(err){
console.log(err);
} else {
counter++;
foundPlatform.articles.push(createdArticle);
foundPlatform.save();
createdArticle.platforms.push(foundPlatform);
createdArticle.save();
if(counter==platforms.length){
res.redirect('articles/' + createdArticle._id);
}
}
});
});
}
});
});
The platforms field is passed to the back-end as an array of strings, one string being one objectID. When platforms only contains 1 string i.e. 1 platform to be linked to, everything works fine. When platforms contains multiple string. the created article has duplicates of each platform. Or sometimes only duplicates of some platforms
Any ideas?
UPDATE 1:
Article Schema:
var mongoose = require("mongoose");
var articleSchema = new mongoose.Schema({
title : String,
description : String,
link : String,
date : String,
body : String,
platforms : [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Platform"
}
]
})
module.exports = mongoose.model("Article", articleSchema);
Platform Schema:
var mongoose = require("mongoose");
var platformSchema = new mongoose.Schema({
name : String,
category : String,
contacts : [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Contact"
}
],
website : String,
country : String,
contactInformation : String,
businessModelNotes : String,
source : String,
generalNotes : String,
projects : [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Project"
}
],
articles : [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Article"
}
],
privacy : String,
comments : [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment"
}
]
});
module.exports = mongoose.model("Platform", platformSchema);

The forEach loop in your attempt does not recognise the callback completion of the findById() async method before the next iteration. You need to use any of async library methods async.each, async.whilst, or async.until which are equivalent to a for loop, and will wait until async's callback is invoked before moving on to the next iteration (in other words, a for loop that will yield).
For example:
var platform_docs = [];
async.each(platforms, function(id, callback) {
Platform.findById(id, function(err, platform) {
if (platform)
platform_docs.push(platform);
callback(err);
});
}, function(err) {
// code to run on completion or err
console.log(platform_docs);
});
For the whole operation, you could use the async.waterfall() method which allows each function to pass its results on to the next function.
The first function in the method creates the new article.
The second function uses the async.each() utility function to iterate over the platforms list, perform an asynchronous task for each id to update the platform using findByIdAndUpdate(), and when they're all done return the results of the update query in an object variable to the next function.
The final function will update the newly created article with the platform ids from the previous pipeline.
Something like the following example:
var newArticle = {},
platforms = req.body.platforms,
date = req.body.date,
split = date.split("/");
newArticle.title = req.body.title;
newArticle.description = req.body.description;
newArticle.date = split[2]+'/'+split[0]+'/'+split[2];
newArticle.link = req.body.link;
newArticle.body = req.body.body;
console.log(platforms);
async.waterfall([
// Create the article
function(callback) {
var article = new Article(newArticle);
article.save(function(err, article){
if (err) return callback(err);
callback(null, article);
});
},
// Query and update the platforms
function(articleData, callback) {
var platform_ids = [];
async.each(platforms, function(id, callback) {
Platform.findByIdAndUpdate(id,
{ "$push": { "articles": articleData._id } },
{ "new": true },
function(err, platform) {
if (platform)
platform_ids.push(platform._id);
callback(err);
}
);
}, function(err) {
// code to run on completion or err
if (err) return callback(err);
console.log(platform_ids);
callback(null, {
"article": articleData,
"platform_ids": platform_ids
});
});
},
// Update the article
function(obj, callback) {
var article = obj.article;
obj.platform_ids.forEach(function(id){ article.platforms.push(id); });
article.save(function(err, article){
if (err) return callback(err);
callback(null, article);
});
}
], function(err, result) {
/*
This function gets called after the above tasks
have called their "task callbacks"
*/
if (err) return next(err);
console.log(result);
res.redirect('articles/' + result._id);
});

Move your save function
if(counter==platforms.length){
createdArticle.save(function(err, savedObject){
if(err || !savedObject) console.log(err || "not saved");
else {
res.redirect('articles/' + savedObject._id.toString());
}
});
}
============= EDIT
Its because you have to call article.save only one time, and not at each loop. In addition you use save() as a sync function but it's async.
I thinks you should use directly update function :
} else {
var counter=0;
// map plateform array id with ObjectID
var idarray = platforms.map(function(e){return mongoose.mongo.ObjectID(e);});
// update all plateform with article id
Platform.update({_id:{$in: idarray}}, {$push:{articles: createdArticle}}, {multi:true, upsert:false}, function(err, raw){
if(err)
{
// error case
return res.status(403).json({});
}
// retrieve plateform
Platform.find({_id:{$in: idarray}}, function(err, results){
if(err || !results)
{
// error case
return res.status(403).json({});
}
Article.update({_id: createdArticle._id.toString()}, {$push:{platforms:{$each: results}}}, {multi:false, upsert:false}, function(err, saved){
if(err || !saved)
{
// error
return res.status(403).json({});
}
res.redirect('articles/' + savedObject._id.toString());
});
});
});
But it's a bad idea to store full objects, why not storing only id ??

Related

Sending results of MongoDB find() using Express

I am new to NodeJS and I am trying to connect to MongoDB I prepared a query and I can see the output on server by traversing the cursor. But I want to send the output of find() to response.
var cursor = db.collection('dbLocations').find(
{$and: [
{type: locationType},
{createdAt : { "$gte": new Date(createdAt), "$lt": date1 }}
]}).skip(skip).limit(count);
Now I am getting error like
Cannot set headers after they are sent to the client if I do a string concate and then JSON.stringify
I tried pretty() but it gives me error skip(...).limit(...).toString(...).pretty is not a function
or skip(...).limit(...).pretty is not a function.
I am totally unable to figure out how to convert as I am not clear about the concept of callback() and all solutions have that. Is there a simple stringify, parse or pretty kind of solution to it.
Below is my express snippet for get, it looks very messy right now. I want to send the find() output instead of random stuff.
app.get('/api/first', function(request, response) {
response.writeHead(200, {'Content-Type': 'application/json'});
var locationType = request.body.type;
var createdAt = request.body.createdAt;
//var pageNumber = parseInt(request.body.pageNumber);
console.log(locationType);
console.log(createdAt);
//console.log(pageNumber);
var date1 = new Date(createdAt);
date1.setDate(date1.getDate() + 1);
var count = 2;
var str="";
var skip;
if(request.body.pageNumber)
skip = parseInt((request.body.pageNumber-1)*count);
else
skip = 0;
MongoClient.connect(url, function(err, client) {
if (err) throw err;
console.log('Connected');
var db = client.db('locationapi');
var cursor = db.collection('dbLocations').find(
{$and: [
{type: locationType},
{createdAt : {"$gte": new Date(createdAt), "$lt": date1}}
]}
).skip(skip).limit(count);
cursor.each(function(err, doc) {
if(err) throw err;
if(doc !== null) {
console.log(doc); str=str+doc;
} else client.close();
});
client.close();
});
var myObj = {
name: 'jgj',
job: 'Ninja'
}; // random stuff
response.end(JSON.stringify(myObj));
});
db.collection('dbLocations').find({
$and: [{
type: locationType
}, {
createdAt: {
"$gte": new Date(createdAt),
"$lt": date1
}
}]
}, {
skip: skip,
limit: count
}).toArray(function (err, docs) {
if (err) return res.status(500).send({error: err})
res.send(docs)
});
https://mongodb.github.io/node-mongodb-native/3.3/api/Collection.html#find
https://mongodb.github.io/node-mongodb-native/3.3/api/Cursor.html#toArray

Mongodb schema defining

Coding a news/media website, I want a "News" section, "Reviews" section, a
"Trending" section, which combines both the previous sections, just like here:
I have made one schema for "News", one for "Reviews".How can I make a "Trending" section(as in the image above "Movies" section)?
Code :
In app.js,
//LANDING PAGE
app.get('/', function (req, res,next) {
Blogdemo.find({}).sort([['_id', -1]]).limit(3).exec(function(err,allBlogs) { //finds latest posts for 1st Schema (upto 3)
if(err) {
console.log(err);
next();
} else {
res.locals.blog = allBlogs;
// res.render("landing", {blog : allBlogs , moment : now});
next();
}
})
}, function (req, res) {
Review.find({}).sort([['_id', -1]]).limit(3).exec(function(err,allReviews) { //finds latest posts of 2nd Schema
if(err) {
console.log(err);
} else {
res.locals.review = allReviews;
res.render("landing", res.locals);
}
})
})
In review.js ,
var mongoose = require("mongoose");
//SCHEMA SETUP
var reviewSchema = new mongoose.Schema({
image : String,
title : String,
body : String,
rating : String,
created : {type : Date, default : Date.now()},
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment" //name of the model
}
]
})
module.exports = mongoose.model("review", reviewSchema);
The "News" schema is almost the same(no review).
Is my way of defining schema wrong? If not, then how can I build the "Trending" section?
Is there any mongodb method which can find the latest posts from "News" and "Reviews" to build the "Trending" section(just like in 1st picture)?
From what i can see from your code, your current News and Review Schema looks fine.
You need to define another Schema for Trending.
var TrendingSchema = new mongoose.Schema({
referenceId : {
type : mongoose.Schema.Types.ObjectId
},
postType : String //To store News or Reviews
});
While saving new News or Reviews, insert the _id of newly saved document in the trending collection.
var news = new News();
news.image = newsImage;
...
news.save(function(err,result)
{
if(!err)
{
var trending = new Trending();
trending.referenceId = result._id;
trending.postType = "News";
treding.save(function(err)
{
if(!err)
{
//success response
}
else
{
//error response
}
});
}
else
{
//send error response
}
});
Similarly while saving Review Post
var review = new Review();
review.image = reviewImage;
...
review.save(function(err,result)
{
if(!err)
{
var trending = new Trending();
trending.referenceId = result._id;
trending.postType = "review"
treding.save(function(err)
{
if(!err)
{
//success response
}
else
{
//error response
}
});
}
else
{
//send error response
}
});
Thus now Trending Collection will contain, newly saved News or Review, in the order they are created. Thus you will be able to get new Review or News Post.
While fetching Trending, you can populate them using News or Review Schema based on the postType.
Trendign.find({}).limit(10).exec(function(err,result)
{
if(!err && result.length!=0)
{
var trendingPosts = [];
result.forEach(function(trending){
if(trending.postType === "News"){
trending.populate({path : 'referenceId',model : 'News'},function(err,populatedItem)
{
if(!err)
{
trendingPosts.push(populatedItem);
}
});
}
else if(trending.postType === "Review"){
trending.populate({path : 'referenceId',model : 'Review'},function(err,populatedItem)
{
if(!err)
{
trendingPosts.push(populatedItem);
}
});
}
});
//now send the trendingPost array with latest News and Review Posts
}
else
{
//send Error response
}
});
Now you can show the latest News or Review and write the type postType.
Hope this is what you want.

How to convert multiple Mongoose documents?

Each of my schemas have a method, called toItem() which converts the doc to a more verbose / human-readable form. How can I create a toItems() method to do the same thing for an array of documents?
My example schema:
var mongoose = require('mongoose');
var membershipSchema = new mongoose.Schema({
m : { type: mongoose.Schema.ObjectId, ref: 'member' },
b : { type: Date, required: true },
e : { type: Date },
a : { type: Boolean, required: true }
});
var accountSchema = new mongoose.Schema({
n : { type: String, trim: true },
m : [ membershipSchema ]
});
accountSchema.methods.toItem = function (callback) {
var item = {
id : this._id.toString(),
name : this.n,
members : []
};
(this.m || []).forEach(function(obj){
item.members.push({
id : obj.m.toString(),
dateBegin : obj.b,
dateEnd : obj.e,
isAdmin : obj.a
});
});
return callback(null, item);
};
var accountModel = mongoose.model('account', accountSchema);
module.exports = accountModel;
I've tried using statics, methods, and third-party libraries, but nothing clean works. I would like to keep this as simple / clean as possible and have the toItems() function contained within my model file.
Thank you, in advance.
Your toItem() method is specific to the schema / model. Your toItems() method sounds more like a utility method which can / will be used by all of your models. If so, I would move create the toItems() method inside a utility file. You would simply pass in the array of documents and the utility method would call the individual toItem() method on each document.
For example:
var async = require('async');
var toItems = function (models, callback) {
models = models || [];
if (models.length < 1) { return callback(); }
var count = -1,
items = [],
errors = [];
async.forEach(models, function (model, next) {
count++;
model.toItem(function (err, item) {
if (err) {
errors.push(new Error('Error on item #' + count + ': ' + err.message));
}
else {
items.push(item);
}
next();
});
}, function (err) {
if (err) {
return callback(err);
}
if (errors.length > 0) {
return callback(errors[0]);
}
return callback(null, items);
});
};
module.exports.toItems = toItems;

MongoDB - get data from table and check if id exists in another

As my title say's, is it possible to query (with nodejs) a list of data in my mongodb, and check if the id exists in another table?
So lets say I have:
var users = new Schema({
name: String
});
And I have another table:
var books = new Schema({
user_id: { type: Schema.Types.ObjectId, ref: 'users' }
});
When I get 'all' the users, I want to check if they have books for example. How would I go about this in nodejs?
You can achieve this with nested loops and record counting:
function getAllUsersWithBooks (callback) {
var books_by_users = []
// return only user_id
user.find({}, {_id : 1}, function (e, ids) {
if (e) return callback(e);
var remaining = ids.length;
ids.forEach(function (user) {
books.find({user_id : user._id}, function (e, books) {
if (e) return callback(e);
var users_books = {user_id : user:_id, books : []};
var books_left = books.length;
books.forEach(function (b) {
users_books.books.push(b.toObject());
if (!--booksLeft) {
books_by_user.push(user_books);
if (!--remaining) {
callback(null, books_by_user);
}
}
})
})
})
})
}
getAllUsersWithBooks(function (e, list) {
if (e) {
// handle error
}
console.log(list);
/*
[{user_id : 123123, books : [b1, b2, etc],
{user_id : 342342, books : [b3, b4, etc]}
*/
})

Mongoose nested document update failing?

If I have a nested document, how can I update a field in that nested document in Mongoose?
I carefully researched this problem using everything available I could find, and even changed my test code to match a similar answered question about this here on Stackoverflow, but I am still unable to figure this out. Here are is my Schema and Models, the code, and the Mongoose debug output. I am unable to understand what I am doing wrong, here.
var mongoose = require('mongoose')
, db = mongoose.createConnection('localhost', 'test')
, assert = require("node-assert-extras");
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
db.once('open', function () {
// yay!
});
mongoose.set('debug', true);
var PDFSchema = new Schema({
title : { type: String, required: true, trim: true }
})
var docsSchema = new Schema({
PDFs : [PDFSchema]
});
var A = db.model('pdf', PDFSchema);
var B = db.model('docs', docsSchema);
function reset(cb) {
B.find().remove();
// create some data with a nested document A
var newA = new A( { title : "my title" })
var newB = new B( { PDFs: newA});
newB.save();
cb();
}
function test1( ) {
reset(function() {
B.findOne({}, 'PDFs', function(e,o)
{
console.log(o);
pdf_id = o.PDFs[0]._id;
console.log("ID " + pdf_id);
B.update(
{ 'pdfs.pdf_id': pdf_id },
{ $set: {
'pdfs.$.title': 'new title'
}}, function (err, numAffected) {
if(err) throw err;
assert.equal(numAffected,1); //KA Boom!
}
);
});
});
}
test1();
/*
$ node test2.js
Mongoose: docs.remove({}) {}
Mongoose: docs.findOne({}) { fields: { PDFs: 1 }, safe: true }
Mongoose: docs.insert({ __v: 0, PDFs: [ { _id: ObjectId("50930e3d0a39ad162b000002"), title: 'my title' } ], _id: ObjectId("50930e3d0a39ad162b000003") }) { safe: true }
{ _id: 50930e3d0a39ad162b000003,
PDFs: [ { _id: 50930e3d0a39ad162b000002, title: 'my title' } ] }
ID 50930e3d0a39ad162b000002
assert.js:102
throw new assert.AssertionError({
^
AssertionError: 0 == 1
*/
You're not using the correct field names in your B.update call. It should be this instead:
B.update(
{ 'PDFs._id': pdf_id }, // <== here
{ $set: {
'PDFs.$.title': 'new title' // <== and here
}}, function (err, numAffected) {
if(err) throw err;
assert.equal(numAffected,1);
}
);
You should also fix your reset function to not call its callback until the save completes:
function reset(cb) {
B.find().remove();
// create some data with a nested document A
var newA = new A( { title : "my title" })
var newB = new B( { PDFs: newA});
newB.save(cb); // <== call cb when the document is saved
}

Resources