Mogoose: Check if fields exist in schema before updating a document - node.js

I have a collection model like this:
const Task = mongoose.model('Task', {
description: {
type: String,
trim: true,
required: true
},
completed: {
type: Boolean,
default: false
}
})
Currently, When a user tries to update a document using the API, any field that is not in the schema will be ignored, and the document will be updated. I want to throw an error if the user sends an update request to the API with a field that is not available in the database.
if I have a task with id of 12345 in the database:
{
_id: 12345,
description: "Buy cheese."
completed: false
}
and a user sends an update query to the API for a the task:
id = '12345'
updates = {
description: 'Buy Milk',
due: '1 Week' //<-- Invalid Field
}
And I use this object to update the document:
await Task.findByIdAndUpdate(id, updates)
mongoose completely ignores the invalid due field and updates the document with the new description field.
Is there a clean way to avoid this sort of invalid update requests?

There is a way i found which is:
const params = Object.keys(req.body) // req.body is the updates coming from API
const allowedMethods = ["completed", "description"];
const isValidOperation = params.every((param) => allowedMethods.includes(param));
if (!isValidOperation) {
return res.status(400).send({ error: "invalid update" });
}

While updating add in criteria the key that comes from the front end with $exists if it's not found in database update will not return any data and you can throw an error in that case.
criteria will be {_id:id};
update will be {description: 'Buy Milk'};
if(payload.due){
criteria.due:{$exists:true},
update.due=payload.due
}
let updatedData= await Task.update(criteria,update)
if(updatedData.n== 0) throw error invalid parameters passed
// https://docs.mongodb.com/manual/reference/method/db.collection.update/
and this only use one query

Related

Axios react node post request is giving a 422 (Unprocessable Entity)

I am trying to save the desired book to my MongoDB database when I press the saved button I get a 422 error I pass in the data as an object but for some reason, the data doesn't appear in the response back from the server The data is being passed to the Axios call but for some reason, the data property always returns an empty object,
The save handler
const handleSave = (event) => {
event.preventDefault();
let save = books.filter((book) => book.id === event.target.id);
// console.log(save);
// const da/ta = ;
// console.log(data);
API.saveBook({
title: save[0].title,
authors: save[0].author,
description: save[0].description,
image: save[0].image,
})
.then((res) => {
alert("book saved");
console.log(res);
// console.log(data);
})
.catch((err) => {
// console.log(data);
console.log("book not saved");
console.log(err.response);
});
};
This is the book model and the heroku link where you can see what is being logged out
const bookSchema = new Schema({
title: { type: String, required: true },
authors: [{ type: String, required: true }],
description: { type: String, required: true },
image: { type: String },
date: { type: Date, default: Date.now },
});
Heroku Link
github
I have console.logs in my inspect so you can check those out to see the response im getting back
I have cloned this repository and tested on both your Heroku link and locally, and cannot recreate the error locally. I suspect something to do with the MongoDB server rather than a code issue. I recommend you test creating a record in the live/Heroku-attached MongoDB server using an alternative method.
Thanks,
Will Walsh
Looks like volumeInfo.description is undefined for some books. The API returns a 422 error since description is required but is not present in the request payload. You could pass a default description if the book doesn't have one.
result = {
// ...
title: result.volumeInfo.title,
description: result.volumeInfo.description || "This book doesn't have a description",
// ...
}
Or you could remove the required validation for the description field if it's not an issue.
I would recommend you rename author to authors in the result object for clarity.
result = {
// ...
authors: result.volumeInfo.authors,
// ...
}

avoid duplicates on MongoDB using Mongoose

Hi I'm new to MongoDB and Moongoose I'm trying to avoid my api's users to store on the Mongo database duplicated contact's name but seems like it's not working at all.
This is how I'm trying to do it right now the name and the phone number are mandatory and also the name must be unique otherwise it should throw an error.
const contactSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true
},
number: {
type: Number,
required: true
}
});
app.post('/api/persons', (request, response) => {
const body = request.body;
const person = new Contact({
name: body.name,
number: +body.number
});
person.save()
.then(saved => {
response.json(saved);
})
.catch(error => {
return response.status(400).json({
error: 'content missing'
});
});
})
If I send a post request with missing name or number it already throws an error but seems like it's not gettin the unique value validation.
Finally found a package that allows me to avoid duplicted entries on Mongo. I used this package following the documentation instructions:
https://github.com/blakehaswell/mongoose-unique-validator#readme
This is the code I had to write:
const uniqueValidator = require('mongoose-unique-validator');
const contactSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true
},
number: {
type: Number,
required: true
}
});
contactSchema.plugin(uniqueValidator);
the error of unique validation is weird so you can use unique-validator plugin, after that when you send a post request with missing name or number, the error is about required: true
Refer to validation
Validators are not run on undefined values. The only exception is the required validator.
Since both the fields(name and number) in your DB are required.
Instead of directly passing the request body to the query, you can do something like this.
const name = body.name;
const number = body.number;
if(!name || !number) {
// Return response saying either of the fields is empty.
// It's not a good practice to hit the DB with undefined values.
}
let personDetails = {
"name": name,
"contact": contact
};
const person = new Contact(personDetails);
Regarding the unique validation either you can use the unique-validator plugin as suggested by Mohammad Yaser Ahmadi or you can make a DB call to check if the name and number are unique and then hit the save method if that's is feasible for your database.
If you want both the fields name and number to be combined unique you can create Compound Index as follows:
contactSchema.index({ name: 1, number: 1 }, { unique: true });
You can read more on Compound Indexes here: https://docs.mongodb.com/manual/core/index-compound/

how to save new record even if same record exist with status false in unique schema using mongoose node js

Need to a save new record even if one record already exists with status false in unique schema using mongoose node js. I have one record along with {status:false} by updating status true to false while deleting that record. Now i try to add a new record. but new record is not crated. because i have apply unique: true in mongoose schema. I need to apply unique : true, but same time need to create new record if it's status false for exist one. So can anyone give some suggestion
Mongoose, nodejs with mongoose-beautiful-unique-validator
//schema model
var userSchema = new mongoose.Schema({
userName: {
type: String,
required: true,
unique: true
})
//save user function inside module export function
const user= mongoose.model('userSchema');
user.userName = fields.username;
user.save((err, doc) => {
if (!err){
res.send(true);
} else {
console.log(err);
return;
}
})
Expected result:
[
{_id: 12345, userName: 'Uday', status:false},
{_id: 67894, userName: 'Uday', status:true}
]
Actual result:
I got duplicate error When i try to save name with Uday with status: true
[
{_id: 12345, userName: 'Uday', status:false},
]
If you want duplicate entries for the username then you need to remove unique: true from schema design, because you can't save the same username twice in same collections.
And if your requirement is just to update status so you can use update query instead of save/insert.
Your id is alway unique in mongodb. Without same id you only can insert data but same id its not possible.
Drop the required attribute from your Schema, then you could pass a callback to pre('save', callback) that checks 2 things:
Does the username already exists (just check the return from Model.find({username: usernameToCheck}, callback));
In your callback, check for the status attribute, if its false then throw an exception, else save your document.

Custom Validation on a field that checks if field already exists and is active

I have a mongodb Collection "Users" having "Name", "Email", "Active" fields.
I want to add a validation that for every document email should be unique. However if a document is invalid i.e Active is false then the email can be accepted.
Here is the model
email: { type: String, validate: {
validator: function(v) {
return new Promise((resolve, reject)=> {
console.log("validating email")
const UserModel = mongoose.model('User');
UserModel.find({email : v, active: true}, function (err, docs)
{
if (!docs.length){
resolve();
}else{
console.log('user exists: ',v);
reject(new Error("User exists!"));
}
});
})
},
message: '{VALUE} already exists!'
}
},
name: {
type: String,
required: true
},
active: {
type: Boolean,
default: true
}
Problem is whenever i do any updation on this model then this validation is called.
So if i update the name then also this validation is called and it gives the error that email already exists.
How do I add a validation on email field so if someone adds a new entry to database or updates email it checks in database if existing user has same email id and is active?
I would first call Mongoose findOne function if the User is already registered the Mongo DB, for example;
let foundUser = await User.findOne({email});
if (!foundUser) {
// update user, create user etc.
...
}
I think it is better to not use logic inside the Mongoose document object. Maybe there is a way to achieve it but I prefer to do these validations in the code, not in the document, it is just my preference.
Also you can try making email unique as follows:
email: {
type: String,
unique: true
}
I'd use unique compound index, instead of having one more additional query to your db. Your code would look like this:
const schema = = new Schema(...);
schema.index({email: 1, active: 1}, {unique: true});
Mongo itself will reject your documents and you can catch it in your code like this:
const {MongoError} = require('mongodb'); // native driver
try {
await model.updateOne(...).exec(); // or .save()
} catch (err) {
//11000 is error code for unique constraints
if (err instanceof MongoError && err.code === 11000)
console.error('Duplicate email/active pair');
}

TypeError: expected string but received array postman

I was trying to send form data that has multiple fields with the same name, I'm getting back "TypeError: expected string but received array".
I think the problem is with postman, I want to have multiple participant fields, and those would be added to the should be added to the array.
final results of array
// this is from models/Battle
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create Schema
const BattleSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
date: {
type: Date,
default: Date.now
},
category: {
type: Number,
required: true // this will come from the selected category
},
winner: {
type: Number,
default: 0
},
status: {
type: Number,
default: 0 // 0 means the battle is closed, 1 means the battle is open for votes, the status will stay 0 until all participants dropped
},
participants: [
{
participant: {
type: Schema.Types.ObjectId,
required: true
}
}
]
});
module.exports = Battle = mongoose.model('battles', BattleSchema);
//this is from routes/api/battles
// #route POST api/battles
// #desc Create battle
// #access Private
router.post(
'/create-battle',
passport.authenticate('jwt', { session: false }),
(req, res) => {
const { errors, isValid } = validateBattleInput(req.body);
// Check Validation
if (!isValid) {
// If any errors, send 400 with errors object
return res.status(400).json(errors);
console.log(errors);
}
const newBattle = new Battle({
user: req.user.id,
category: req.body.category,
participant: req.body.participant
});
//save
newBattle.save().then(battle => {
// const participant = req.body.participant;
const participant = req.body.participant;
// add participants to array
battle.participants.push( participant );
console.log(typeof req.body.participant);
// get the inserted id
const battleId = battle._id;
res.json(battle);
});
}
);
// this is battle validation
const Validator = require('validator');
const isEmpty = require('./is-empty');
var bodyParser = require('body-parser');
module.exports = function validateBattleInput(data) {
let errors = {};
data.category = !isEmpty(data.category) ? data.category : '';
data.participant = !isEmpty(data.participant) ? data.participant : '';
if (Validator.isEmpty(data.category)) {
errors.category = 'Category field is required';
}
// if (Validator.isEmpty(data.challenger)) {
// errors.challenger = 'Challenger field is required';
// }
if (Validator.isEmpty(data.participant)) {
errors.participant = 'Participant field is required';
}
return {
errors,
isValid: isEmpty(errors)
};
};
TypeError: Expected string but received Array. ---throws an error in postman as well as in a terminal window. I suspect it could be the user schema definition mismatch
Please check your user model user schema eg
name: {
type: String,
required: true
}
it's receiving something else than expected.
try in your "body" tab, selecting "raw", and then to the right, select "JSON (application/json)" instead of "text".
I'm assuming your API endpoint uses JSON instead of a url-encoded form data, just because you are running an API using express and mongoose. but you should clarify that on the question if it isn't the case.
Write a proper JSON body, I mean, use double quotes for keys as in:
{"model": { "property": "value", "property2": 1}}
and try with the wrapping object {"model": <YOUR BODY HERE>} or without to see what works for you, as it's typical to wrap the object, but sometimes people don't use them. (seeing this in your code: req.body.participant makes me think you probably don't).
(PS: not related with the question, but personally prefer ARC or Insomnia for rest clients, as the interface for them is cleaner)
If you want data to be sent in participants array all the fields should be participants and not participant
try sending data through raw data and then selecting application/data for better formatting
When testing in postman - Just figured out Key value must match your validation function defined variables. It's better to be consistent across your development.

Resources