Save an array of sub-documents using Mongoose schema - node.js

I need to save an array of sub-documents (Categories) on each Item, however. The problem relates to processing an array passed in a request.
The main issue I have is how to successfully save the array of categories in the item.
I have two models in MongoDB, namely:
Item
Category
The Item schema:
const Item = mongoose.model(
'Items',
new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
minlength: 2,
maxlength: 255
},
category: [categorySchema],
numInStock: {
type: Number,
required: true,
min: 0,
max: 255
},
unitPrice: {
type: Number,
required: true,
min: 0,
max: 255
}
})
);
The Category schema:
const categorySchema = new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 2,
maxlength: 50
},
user_id: {
type: ObjectId,
required: false
}
});
What I have so far for the Express route for POST to Items:
router.post('/', [auth], async (req, res) => {
const { error } = validate(req.body);
if (error) return res.status(400).send({ data: error.details[0].message });
const category = await Category.findById(req.body.categoryId);
if (!category) return res.status(400).send({ data: 'Invalid category.' });
console.log('LOG: categoryIds: ', req.body.categoryId[0]);
const item = new Item({
name: req.body.name,
category: [
{
_id: req.body.categoryId._id,
name: req.body.categoryId.name
}
],
numInStock: req.body.numInStock,
unitPrice: req.body.unitPrice,
publishDate: moment().toJSON()
});
await item.save();
res.send({ data: item });
});
The body of the POST looks like:
{
"name": "AnotherNew item",
"categoryId": [{"name": "Vegetables", "_id": "5de43de8805f4fac35f691c0"},{"name": "Seafood", "_id": "5de43de8805f4fac35f691c4"}],
"numInStock": 0,
"unitPrice": 0
}
The error I receive:
error: Items validation failed: category.0.name: Path `name` is required. message=Items validation failed: category.0.name: Path `name` is required., message=Path `name` is required., name=ValidatorError, validator=function(v) {
const cachedRequired = get(this, '$__.cachedRequired');
// no validation when this path wasn't selected in the query.
if (cachedRequired != null && !this.isSelected(_this.path) && !this.isModified(_this.path)) {
return true;
}
// `$cachedRequired` gets set in `_evaluateRequiredFunctions()` so we
// don't call required functions multiple times in one validate call
// See gh-6801
if (cachedRequired != null && _this.path in cachedRequired) {
const res = cachedRequired[_this.path] ?
_this.checkRequired(v, this) :
true;
delete cachedRequired[_this.path];
return res;
} else if (typeof required === 'function') {
return required.apply(this) ? _this.checkRequired(v, this) : true;
}
return _this.checkRequired(v, this);
}, message=Path `name` is required., type=required, path=name, value=undefined, kind=required, path=name, value=undefined, reason=undefined, _message=Items validation failed, stack=ValidationError: Items validation failed: category.0.name: Path `name` is required

I solved this by passing the array req.body.categoryId from the request to category on the new instance of Item.
I appreciate any feedback on my changes.
My changes to Item POST route are as follows:
router.post('/', [auth], async (req, res) => {
const { error } = validate(req.body);
if (error) return res.status(400).send({ data: error.details[0].message });
const category = await Category.findById(req.body.categoryId);
if (!category) return res.status(400).send({ data: 'Invalid category.' });
console.log('LOG: categoryIds: ', req.body.categoryId);
const item = new Item({
name: req.body.name,
category: req.body.categoryId,
numInStock: req.body.numInStock,
unitPrice: req.body.unitPrice,
publishDate: moment().toJSON()
});
await item.save();
res.send({ data: item });
});

Related

Mongoose update subarray item

I'm trying to update sub-array item in my collection , i'm trying to use set but can't get it work with _id , it only work when I say array[0] ...
Here is my method :
exports.updateSubCategory = (req, res) => {
const category = req.category;
Category.findById(category._id, function (err, doc) {
if (err) {
return res.status(400).json({
error: "Can't Find parent category",
});
} else {
doc.subcategory.set(0, { name: req.body.name }); works
doc.subcategory.set(req.body.id, { name: req.body.name });//doesn't work
doc.subcategory.set({_id:req.body.id}, { name: req.body.name });//doesn't work
doc.save((err, updatedCategory) => {
if (err) {
return res.status(400).json({
error: "Can't update subcategory",
});
}
res.json(updatedCategory);
});
}
});
};
My schema :
const mongoose = require("mongoose");
const categorySchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: true,
maxlength: 32,
unique: true,
},
subcategory: [
{
name: {
type: String,
trim: true,
required: true,
maxlength: 32,
unique: true,
},
},
],
},
{ timestamps: true }
);
module.exports = mongoose.model("Category", categorySchema);
solution :
exports.updateSubCategory = (req, res) => {
const category = req.category;
Category.findById(category._id, function (err, doc) {
if (err) {
return res.status(400).json({
error: "Can't Find parent category",
});
} else {
let subdoc = doc.subcategory.id(req.body.id);
subdoc.name = req.body.name;
doc.save((err, updatedCategory) => {
if (err) {
return res.status(400).json({
error: "Can't update subcategory",
});
}
res.json(updatedCategory);
});
}
});
};

Mongoose: how to only populate, sort and return a nested object?

I have a User schema, with a messages array. The message array is filled by conversations id and referenced to a Conversation schema.
I want to fetch all conversations from a user, sort them by unread and then most recent messages. Finally, I must only return an array of lastMessage object.
For the moment, I have only managed to populate the whole user object.
Here is the Conversation Schema:
const conversationSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
messages: [{ message: { type: String }, authorId: { type: String } }],
lastMessage: {
authorId: { type: String },
snippet: { type: String },
read: { type: Boolean },
},
},
{ timestamps: true }
);
conversationSchema.index({ name: 1 });
module.exports = mongoose.model("Conversation", conversationSchema);
And here is my code:
router.get("/conversations", async (req, res) => {
try {
const { userId } = req.query;
const user = await User.findById({ _id: userId }).populate("messages");
.sort({ updatedAt: 1, "lastMessage.read": 1 });
return res.json({ messages: user.messages });
} catch (err) {
console.log("error", err);
return res.json({ errorType: "unread-messages-list" });
}
});
How to do this?

findOneAndUpdate seems to be doubling my $inc count

I'm trying to post a comment on to my posts for my MERN app but I'm running into an issue where the comment (Posts.findOneAndUpdate) seems to posting the comments twice. I read a few posts on SO that described the issue to be the way mongoose handles queries but I must be missing something.
If anyone could explain what I'm doing wrong I would greatly appreciate it!
Route I'm using:
router.post('/newReply/:id', async function(req, res) {
const body = req.body
if (!body) {
return res.status(400).json({
success: false,
error: 'No text entered!',
})
}
const reply = new Replies(body)
if (!reply) {
return res.status(400).json({ success: false, error: err })
}
await Posts.findOneAndUpdate(
{ _id: req.params.id },
{
"$inc": { "replies": 1 },
"$push": { "comments": reply },
},
{
new: true
},
(err) => {
if (err) {
return res.status(404).json({
success: false,
error: err,
message: 'Post not found!',
})
}
return res.status(200).json({
success: true,
id: reply._id,
message: 'Reply created!',
reply: reply.reply,
points: reply.points,
createdAt: reply.createdAt
})
})
.catch(err => console.log(err))
})
Posts Model
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// Create Schema
const PostsSchema = new Schema({
post: {
type: String,
required: true
},
points: {
type: Number,
default: 0
},
voters: {
type: Array
},
upvotedBy: {
type: Array
},
downvotedBy: {
type: Array
},
createdAt: {
type: Date,
default: Date.now
},
replies: {
type: Number,
default: 0
},
comments: {
type: Array
},
user_id: {
type: 'string'
},
deleted: {
type: Boolean,
default: false
}
});
module.exports = Posts = mongoose.model("posts", PostsSchema);

TypeError: Cannot read property '_id' of undefined when using findOneAndRemove()

When I execute the function findOneAndRemove() and pass in the required parameters, it shows the error 'TypeError: Cannot read property '_id' of undefined'. My mongodb have the attribute '_id'
I tried findById(). It is working but if I defined findOneAndRemove({_id: req.params.id}), the error occurs.
**router**
router.delete('/delete/:id', async (req, res) => {
try {
var id = req.params.id;
if (!ObjectID.isValid(id))
return res.status(404).send();
let team = await Team.findOneAndDelete({ _id: id, createdBy: req.user._id });
console.log('team', team);
if (!team)
return res.status(404).send();
res.status(201).json({
message: 'Team Deleted',
result: { team }
});
} catch (e) {
console.log(e);
res.status(400).send(e);
}
});
**Team Model**
var mongoose = require('mongoose');
const teamSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
trim: true
},
country: {
type: String,
required: true,
trim: true
},
yearFounded: {
type: Date,
required: true
},
ground: {
type: String,
required: true,
trim: true
},
capacity: {
type: Number,
required: true,
},
manager: {
type: String,
required: false,
},
website: {
type: String,
required: false,
},
imagePath: {
type: String,
required: false,
},
description: {
type: String,
required: false
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
}
}, {
timestamps: true
})
teamSchema.index({ name: "text", manager: "text", ground: "text", country: "text" });
teamSchema.virtual('players', {
ref: 'Player',
localField: '_id',
foreignField: 'team'
})
const Team = mongoose.model('Team', teamSchema);
module.exports = Team
findOneAndRemove returns the removed document so if you remove a document that you later decide should not be removed, you can insert it back into the db. Ensuring your logic is sound before removing the document would be preferred to checks afterward IMO.
findOneAndDelete has the sort parameter which can be used to influence which document is updated. It also has a TimeLimit parameter which can control within which operation has to complete
try this
router.delete('/delete/:id', async (req, res) => {
try {
let id = {_id:req.params.id};
if (!ObjectID.isValid(id))
return res.status(404).send();
let team = await Team.findOneAndRemove({ _id: rid, createdBy: req.user._id });
console.log('team', team);
if (!team)
return res.status(404).send();
res.status(201).json({
message: 'Team Deleted',
result: { team }
});
} catch (e) {
console.log(e);
res.status(400).send(e);
}
});
The answer is I forget to add middleware 'authenticate' and hence the createdBy params req.user._id is forever undefined. The solution.
Routes
router.delete('/delete/:id', authenticate, async (req, res) => {
try {
var id = req.params.id;
if (!ObjectID.isValid(id))
return res.status(404).send();
let team = await Team.findOneAndRemove({ _id: id, createdBy: req.user._id });
if (!team)
return res.status(404).send();
removeImage(team.imagePath);
res.status(201).json({
message: 'Team Deleted',
result: { team }
});
} catch (e) {
console.log(e);
res.status(400).send(e);
}
});
Middleware
let authenticate = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '')
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findOne({ _id: decoded._id, 'tokens.token': token })
if (!user) {
throw new Error()
}
req.token = token;
req.user = user;
next()
} catch (e) {
res.status(401).send({ error: 'Please authenticate.' })
}
};

validation of request parameters in nodejs against database fields

I am building REST API using node, express and MongoDB(using mongoose) i want to add validation to post requests how can I do that I have defined schema like this
var CategorySchema = new Schema({
name: {
type: String,
lowercase: true,
default: '',
trim: true,
unique: [true, 'Category name already exists'],
required: [true, 'Category Name cannot be blank'],
minlength: [4, 'Minimum 4 characters required'],
maxlength: [20, 'Category name cannot be That long']
},
parentCategory: {
type: String,
lowercase: true,
default: '',
trim: true
},
description: {
type: String,
lowercase: true,
default: '',
trim: true,
required: [true, 'description cannot be blank'],
minlength: [10, 'Very short description']
},
slug: {
type: String,
lowercase: true,
unique: [true, 'Slug must be unique'],
required: true,
minlength: [4, "Minimum 4 Charater required"],
maxlength: [20, "Slug cannot be that long"]
},
imageUrl: {
type: String,
default: '',
trim: true
},
created: {
type: Date,
default: Date.now
},
updated: {
type: Date
}
});
module.exports = mongoose.model('Category', CategorySchema);
i am insert data using mongoose models like this
exports.createCategory = function (request, response) {
var newCategory = {
"name": request.body.categoryName,
"parentCategory": request.body.parentCategory,
"description": request.body.description,
"slug": request.body.slug,
"imageUrl": request.body.categoryImage,
"updated": new Date()
}
var category = new Category(newCategory);
category.save()
.then(function (category) {
sendResponse(response, 201, "success", category);
})
.catch(function (error) {
sendResponse(response, 400, "error", error);
});
};
but I want to add validation to the post request. I have to make sure that fields that are defined in a database are there in a request and values must be required as well I am really confused how to validate key in a JSON object inside request body. I have already added some validation using mongoose.
You can use Middlewares for this purpose like (If you are using express framework) :
app.use(function (req, res, next) {
var validationErrors = [];
validationErrors = some_function_to_validate(req); // Returns array
if(validationErrors.length > 0) {
// Send Custom Response with Validation Error
}
else {
next();
}
});
Note : This middleware will be executed for all of your requests (If added before all the routes registration).
For more please refer : http://expressjs.com/en/guide/using-middleware.html
Try following code to get the valid fields. It will return false if any field i.e. not required is coming with the req. Hope this will help.
function validateReq(req)
{
if(req)
{
var prop = ['name','parentCategory','description'] //Add more property name here
var found = false;
for(var key in req.body)
{
if (prop[key] && (prop[key] !== null))
{
found = true;
}
else
{
return false;
}
}
}
else
{
return false;
}
}
exports.createCategory = function (request, response) {
var valid = validateReq(request);
alert(valid);
if(valid){
var newCategory = {
"name": request.body.categoryName,
"parentCategory": request.body.parentCategory,
"description": request.body.description,
"slug": request.body.slug,
"imageUrl": request.body.categoryImage,
"updated": new Date()
}
var category = new Category(newCategory);
category.save()
.then(function (category) {
sendResponse(response, 201, "success", category);
})
.catch(function (error) {
sendResponse(response, 400, "error", error);
});
}
else
{
//Error handling code
}
};
My answer seems to be too late, but hopefully it will help others in future. I think you can try express-validator, here is an article explains how to use it in detail.
Its basic idea is to add a middleware, and put all the validations inside, which can be invoked in subsequent route functions. This way can keep the business logic code clean.
below is an example from official docs
// ...rest of the initial code omitted for simplicity.
const { check, validationResult } = require('express-validator');
app.post('/user', [
// username must be an email
check('username').isEmail(),
// password must be at least 5 chars long
check('password').isLength({ min: 5 })
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
User.create({
username: req.body.username,
password: req.body.password
}).then(user => res.json(user));
});

Resources