Save two referenced documents simultaneously - node.js

I've got an stock application where I want to set some details about the stock and then insert all the items of the stock. I want to insert the stock details and the items in two different collection so then I can filter the items. I'm using the MEAN Stack where I've modified the crud module to accept some extra fields and also made the UI for filling the items array.This what I have so far:
scope.stockItems = [];
$scope.createStockItem = function () {
$scope.stockItems.push(
{
brand: $scope.brand,
style: $scope.style,
amount: $scope.amount
}
);
$scope.brand = false;
$scope.style = false;
$scope.amount = '';
};
// Create new Stock
$scope.create = function() {
// Create new Stock object
var stock = new Stocks ({
name: this.name,
details: this.details,
stockDate: this.stockDate
});
// Redirect after save
stock.$save(function(response) {
$location.path('stocks/' + response._id);
// Clear form fields
$scope.name = '';
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};
The stock model:
var StockSchema = new Schema({
name: {
type: String,
default: '',
required: 'Please fill Stock name',
trim: true
},
details: {
type: String,
default: '',
required: 'Please fill Stock details'
},
stockDate: Date
created: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
and the method in the server controller:
exports.create = function(req, res) {
var stock = new Stock(req.body);
stock.user = req.user;
stock.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(stock);
}
});
};
How can I send into the request and save the stockItems also?

By saying 'simultaneously' I think you are requiring transaction feature, which is really an RDBMS thing, and is not supported by MongoDB. If your application strongly relies on such features, I'm afraid MongoDB is not the right choice for you.
So back to your question, I don't understand why you have to store stock and stock item in 2 different collections. Store them in one collection would be a better choice. You can refer to the Data Model Design of MongoDB Manual for more information. If it's just to filter all the stock items, aggregation framework is designed for such purpose. As well as Map/Reduce. Here aggregation framework suits better for your issue. You would have something like:
db.stock.aggregate([
{$match: {...}}, // same as find criteria. to narrow down data range
{$unwind: "$items"}, // unwind items.
... // probably some other $match to filter the items
]);

Related

How do I create a number of objects with insertMany with existing data from the db?

I'm currently trying to insert a large number of models through insertMany, but I can't seem to figure out how to populate the array when creating an object. I'm relatively new to Mongoose and any help would be appreciated, here is the code I have right now.
const ProgramsSchema = new mongoose.Schema({
program_id: {
type: String,
required: true
},
description: {
type: String
},
});
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: {
type: [{type: ProgramsSchema, ref: "Programs"}]
}
});
And here's the code where I try to create a number of schools and add it to the database.
let new_schools = []
for (let i = 0; i < schools.length; i++) {
let school = schools[i]
let p_arr = []
for (let p_index = 0; p_index < school["PROGRAMS"].length; p_index++) {
let p_id = school["PROGRAMS"][p_index]
Programs.find({program_id: p_id}).populate('Programs').exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0])
}
})
}
let newSchool = {
inst_url: school["INSTURL"],
programs: p_arr,
}
new_schools.push(newSchool);
}
Schools.insertMany(new_schools);
I can basically add all of the school data into the db, but none of the programs are being populated. I was wondering if there was a way to do this and what the best practice was. Please let me know if you guys need more info or if my question wasn't clear.
There are a few problems with your mongoose schemas. The operation you are trying to do in find is not available, based on your mongoose schemas. You cannot populate from "Programs" to "Schools". You can populate from "Schools" to "Programs", for instance:
Schools.find().populate(programs)
And to do that, several changes in your schemas are necessary. The idea is to store the programs _id in your programs array in School collection and be able to get the programs info through populate(), either regular populate or 'custom populate' (populate virtuals).
Regular populate()
I would change the schoolsSchema in order to store an array of _id into programs:
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: [
{type: String, ref: "Programs"}
]
});
You should change ProgramsSchema as well:
const ProgramsSchema = new mongoose.Schema({
_id: Schema.Types.ObjectId, // that's important
description: {
type: String
},
});
And now, you can do:
Programs.find({_id: p_id}).exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0]._id)
}
})
Your documents should be inserted correctly. And now you can populate programs when you are performing a query over School, as I indicated above:
Schools.find().populate(programs)
Populate Virtual
The another way. First of all, I have never tried this way, but I think it works as follows:
If you want to populate over fields that are not ObjectId, you can use populate virtuals (https://mongoosejs.com/docs/populate.html#populate-virtuals). In that case, your schemas should be:
const ProgramsSchema = new mongoose.Schema({
program_id: String,
description: {
type: String
},
});
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: [
{type: String, ref: "Programs"}
]
});
Enable virtual in your School schema:
Schools.virtual('programs', {
ref: 'Programs',
localField: 'programs',
foreignField: 'program_id'
});
Then, you should store the program_id.
Programs.find({program_id: p_id}).exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0].program_id)
}
})
And as before, you can populate() when you need.
I hope I helped

Mongoose schema for article

I'm building a news website, and I this mongoose schema:
let mongoose = require('mongoose');
let articleSchema = mongoose.Schema({
image1:{
type: String,
required: true
},
title:{
type: String,
required: true
},
author:{
type: String,
required: true
},
date:{
type: String,
required: true
},
updated:{
type: String,
default: 'not updated'
},
title_nd:{
type: String,
required: false
},
body:{
type: String,
required: true
},
comments: [commentsSchema],
likes:{ type:Number, default:0 }
});
let Article = module.exports = mongoose.model('Article', articleSchema);
And I want to add a form so users can add their comments.
The question is how do I create a new schema for comments and link it to article schema, and then if the user adds a comment the comment added to the database and then shows on the article comment section?
Modeling a separate schema for comment is not a good idea in my humble opinion, since it is a classic case of one to few mapping which is an ideal use case for embedding the document. To give you a basic idea about data modeling i am quoting here
You need to consider two factors:
Will the entities on the ā€œNā€ side of the One-to-N ever need to stand alone?
What is the cardinality of the relationship: is it one-to-few; one-to-many; or one-to-squillions?
Based on these factors, you can pick one of the three basic One-to-N schema designs:
Embed the N side if the cardinality is one-to-few and there is no need to access the embedded object outside the context of the parent object
Use an array of references to the N-side objects if the cardinality is one-to-many or if the N-side objects should stand alone for any reasons
Use a reference to the One-side in the N-side objects if the cardinality is one-to-squillions
Please refer to a very well written and articulated post 6 Rules of Thumb for MongoDB Schema Design: Part 1 from mongodb blogs.
Even after this if you think it is a good idea to link to another schema please refer to this SO question - Referencing another schema in Mongoose
so I found a solution for this:
// :id is all articles with all ids
router.post('/:id', function (req, res) {
let comment = {};
comment.body = req.body.body;
comment.user = req.user;
comment.date = new Date(Date.now()).toDateString();
// Express validator
req.checkBody('body').len(5, 100);
let errors = [];
errors = req.validationErrors();
if(errors) {
Article.findById(req.params.id, function (err, article) {
if(err)throw err;
req.flash('danger', 'Body minimum length is 5 and maximum 100!');
res.redirect('/articles/'+article.id);
});
} else {
Article.findById(req.params.id, function (err, article) {
if(err)throw err;
article.comments.push({'body':comment.body,'user':comment.user,'date':comment.date});
article.save(function (err) {
if (err) {
throw err;
}else {
req.flash('success', 'Comment added!');
res.redirect('/articles/'+article.id);
}
});
});
}
});
EDIT: code above in more readable form:
router.post('/:id', async (req, res) => {
let article = await Article.findById(req.params.id);
if (!article) res.status("403");
let articleUrl = "/articles/${article.id}";
let comment = {
body: req.body.body,
user: req.user,
date: new Date(Date.now()).toDateString();
};
if (commment.body.lengh >= 100 || comment.body.length <= 5) {
req.flash('danger', 'Body minimum length is 5 and maximum 100!');
return res.redirect(articleUrl);
}
articles.comments.push(comment);
await article.save();
req.flash('success', 'Comment added!');
res.redirect(articleUrl);
});

How to count unique views to a blog post (logged in and non logged in user) using Node.Js, MongoDB, Mongoose, Express

I want to display how many times a blog post has been read kind of like what Business Insider has.
The objective is...
View count that doesn't increment with every reload.
Every blog post stores its own view count
Fetch the view count from MongoDB/Mongoose Schema field and display it in HTML.
var express = require('express');
var cookieParser = require('cookie-parser');
var mongoose = require('mongoose');
var express-session = require('express-session');
//Show Blog Post
router.get('/blog/:categorySlug/:slug', function (req, res)
var slug = req.params.slug;
Blog.findOne({'slug' : slug}).populate('comments').exec(function (err, foundBlog) {
if (err) {
console.log(err);
} else {
res.render('show', { main: foundBlog, title: foundBlog.title});
}
});
});
I know that if i use req.session.views, it will increment the view count with every reload across all pages.
router.get('/blog/:categorySlug/:slug', function (req, res) {
req.session.views = (req.session.views || 0) + 1
var slug = req.params.slug;
Blog.findOne({'slug' : slug}).populate('comments').exec(function (err, foundBlog) {
if (err) {
console.log(err);
} else {
res.render('show', { main: foundBlog, title: foundBlog.title, pageViewCount: req.session.views});
}
});
});
So what could i do to save the view count of each blog separately and store that view count value in that blog post's viewCount field in the database and then render it in HTML using <%= main.viewCount %>
// Blog Schema
var mainBlogSchema = new mongoose.Schema({
image: String,
imageDescription: String,
priority: {
type: String,
default: ""
},
title: String,
content: String,
author: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
username: String,
name: String,
},
slug: {
type: String,
unique: true,
},
status: String,
viewCount: {
type: Number,
default: 0,
},
category: String,
categorySlug: String,
tags: String,
updated: {
type: Boolean,
default: false
},
date: { type: Date, default: Date.now },
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment',
},
],
},{
timestamps: {
createdAt: 'createdAt',
updatedAt: 'updatedAt'
}
});
One approach can be (might need changes)
Before the UI page loads, a script checks for user-token(separate token or some random string token)in the browser.
If token is not available then send request for new token along with page-id.
If token is available, call API with the token and page-id.
Make sure it gets called for every page load or even page transition.
Backend implementation can be like
Page collection will have page-id along with visits:[] field.
One API creates token and stores it in a separate collection(users) and follows step 3.
Another API will take the token and page-id as input, it will first. check the token is available in our collection(user) and if so it will do a mongo update on Page collection with
db.pages.update(
{ _id: page-id },
{ $addToSet: {visits: [ user_token ] } }
)
This query takes care if the token stored is unique no need to worry about anything. We can maintain a separate field called visitCount if needed, which gets updated with the latest count once query gets executed updating at least one record.
Pros
Page refresh will not affect the count.
The token is also maintained at our end so we can validate.
Cons
We will need huge space to store these token and we have to delete them if a user did not visit the site for a very long time.
API calls overhead.

Skip or Disable validation for mongoose model save() call

I'm looking to create a new Document that is saved to the MongoDB regardless of if it is valid. I just want to temporarily skip mongoose validation upon the model save call.
In my case of CSV import, some required fields are not included in the CSV file, especially the reference fields to the other document. Then, the mongoose validation required check is not passed for the following example:
var product = mongoose.model("Product", Schema({
name: {
type: String,
required: true
},
price: {
type: Number,
required: true,
default: 0
},
supplier: {
type: Schema.Types.ObjectId,
ref: "Supplier",
required: true,
default: {}
}
}));
var data = {
name: 'Test',
price: 99
}; // this may be array of documents either
product(data).save(function(err) {
if (err) throw err;
});
Is it possible to let Mongoose know to not execute validation in the save() call?
[Edit]
I alternatively tried Model.create(), but it invokes the validation process too.
This is supported since v4.4.2:
doc.save({ validateBeforeSave: false });
Though there may be a way to disable validation that I am not aware of one of your options is to use methods that do not use middleware (and hence no validation). One of these is insert which accesses the Mongo driver directly.
Product.collection.insert({
item: "ABC1",
details: {
model: "14Q3",
manufacturer: "XYZ Company"
},
}, function(err, doc) {
console.log(err);
console.log(doc);
});
You can have multiple models that use the same collection, so create a second model without the required field constraints for use with CSV import:
var rawProduct = mongoose.model("RawProduct", Schema({
name: String,
price: Number
}), 'products');
The third parameter to model provides an explicit collection name, allowing you to have this model also use the products collection.
I was able to ignore validation and preserve the middleware behavior by replacing the validate method:
schema.method('saveWithoutValidation', function(next) {
var defaultValidate = this.validate;
this.validate = function(next) {next();};
var self = this;
this.save(function(err, doc, numberAffected) {
self.validate = defaultValidate;
next(err, doc, numberAffected);
});
});
I've tested it only with mongoose 3.8.23
schema config validateBeforeSave=false
use validate methed
// define
var GiftSchema = new mongoose.Schema({
name: {type: String, required: true},
image: {type: String}
},{validateBeforeSave:false});
// use
var it new Gift({...});
it.validate(function(err){
if (err) next(err)
else it.save(function (err, model) {
...
});
})

Hide embedded document in mongoose/node REST server

I'm trying to hide certain fields on my GET output for my REST server. I have 2 schema's, both have a field to embed related data from eachother into the GET, so getting /people would return a list of locations they work at and getting a list of locations returns who works there. Doing that, however, will add a person.locations.employees field and will then list out the employees again, which obviously I don't want. So how do I remove that field from the output before displaying it? Thanks all, let me know if you need any more information.
/********************
/ GET :endpoint
********************/
app.get('/:endpoint', function (req, res) {
var endpoint = req.params.endpoint;
// Select model based on endpoint, otherwise throw err
if( endpoint == 'people' ){
model = PeopleModel.find().populate('locations');
} else if( endpoint == 'locations' ){
model = LocationsModel.find().populate('employees');
} else {
return res.send(404, { erorr: "That resource doesn't exist" });
}
// Display the results
return model.exec(function (err, obj) {
if (!err) {
return res.send(obj);
} else {
return res.send(err);
}
});
});
Here is my GET logic. So I've been trying to use the query functions in mongoose after the populate function to try and filter out those references. Here are my two schema's.
peopleSchema.js
return new Schema({
first_name: String,
last_name: String,
address: {},
image: String,
job_title: String,
created_at: { type: Date, default: Date.now },
active_until: { type: Date, default: null },
hourly_wage: Number,
locations: [{ type: Schema.ObjectId, ref: 'Locations' }],
employee_number: Number
}, { collection: 'people' });
locationsSchema.js
return new Schema({
title: String,
address: {},
current_manager: String, // Inherit person details
alternate_contact: String, // Inherit person details
hours: {},
employees: [{ type: Schema.ObjectId, ref: 'People' }], // mixin employees that work at this location
created_at: { type: Date, default: Date.now },
active_until: { type: Date, default: null }
}, { collection: 'locations' });
You should specify the fields you want to fetch by using the select() method. You can do so by doing something like:
if( endpoint == 'people' ){
model = PeopleModel.find().select('locations').populate('locations');
} else if( endpoint == 'locations' ){
model = LocationsModel.find().select('employees').populate('employees');
} // ...
You can select more fields by separating them with spaces, for example:
PeopleModel.find().select('first_name last_name locations') ...
Select is the right answer but it also may help to specify it in your schema so that you maintain consistency in your API and I've found it helps me to not remember to do it everywhere I perform a query on the object.
You can set certain fields in your schema to never return by using the select: true|false attribute on the schema field.
More details can be found here: http://mongoosejs.com/docs/api.html#schematype_SchemaType-select
SOLUTION!
Because this was so hard for me to find i'm going to leave this here for anybody else. In order to "deselect" a populated item, just prefix the field with "-" in your select. Example:
PeopleModel.find().populate({path: 'locations', select: '-employees'});
And now locations.employee's will be hidden.
If you remember from you SQL days, SELECT does a restriction on the table(s) being queried. Restrict is one of the primitive operations from the relational model and continues to be a useful feature as the relational model has evolved. blah blah blah.
In mongoose, the Query.select() method allows you to perform this operation with some extra features. Particularly, not only can you specify what attributes (columns) to return, but you can also specify what attributes you want to exclude.
So here's the example:
function getPeople(req,res, next) {
var query = PeopleModel.find().populate({path: 'locations', select: '-employees'});
query.exec(function(err, people) {
// error handling stuff
// process and return response stuff
});
}
function getLocations(req,res, next) {
var query = LocationModel.find().populate({path: 'employees', select: '-locations'});
query.exec(function(err, people) {
// error handling stuff
// processing and returning response stuff
});
}
app.get('people', getPeople);
app.get('locations', getLocations);
Directly from the Mongoose Docs:
Go to http://mongoosejs.com/docs/populate.html and search for "Query conditions and other options"
Query conditions and other options
What if we wanted to populate our fans array based on their age,
select just their names, and return at most, any 5 of them?
Story
.find(...)
.populate({
path: 'fans',
match: { age: { $gte: 21 }},
select: 'name -_id',
options: { limit: 5 }
})
.exec()
I just wanted to remark, for the simplicity of the endpoint you may be able to get away with this way to define the endpoints. However, in general this kind of dispacher pattern is not necessary and may pose problems later in development when developing with Express.

Resources