How to print the full stacktrace for mongoose validation errors - node.js

Is there a way to have the stacktrace show the line in code which called save?
I was testing my validation logic and noticed that Mongoose doesn't print a stacktrace to where I call save(). While the validation does say what went wrong, it is not saying where this is located.
const mySchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
accessToken: {
type: String,
required: true,
},
})
mySchema.statics.createOrUpdate = async function(name, accessToken) {
const animal = await this.findOne({ name })
if (!animal) {
animal = new Animal({ name }) // accessToken is missing and required
}
await animal.save() // expected stacktrace error here
}
ValidationError: Animal validation failed: accessToken: Path `accessToken` is required.
at model.Document.invalidate (/Users/michael/repos/MyApp/node_modules/mongoose/lib/document.js:2622:32)
at /Users/michael/repos/MyApp/node_modules/mongoose/lib/document.js:2442:17
at /Users/michael/repos/MyApp/node_modules/mongoose/lib/schematype.js:1225:9
at processTicksAndRejections (internal/process/task_queues.js:79:11)
If I rethrow the error, I can get a more descriptive stacktrace. But I rather not need to do this:
...
await animal.save().catch((e) => { throw Error(e) })
Error: ValidationError: accessToken: Path `accessToken` is required.
at /Users/michael/repos/MyApp/models/Animal.js:19:42
at processTicksAndRejections (internal/process/task_queues.js:97:5)

Related

NodeJS async API Error, when querying embedded array of models

Thanks for taking the time to read this.
I am currently getting this error in my API endpoint for node express when calling this async endpoint.
(node:32308) UnhandledPromiseRejectionWarning: ValidationError: Conversation validation failed: participants.0: Path `participants.0` is required., participants.1: Path `participants.1` is required.
at model.Document.invalidate (D:\cliquenext\flutter\stashify\backend\node_modules\mongoose\lib\document.js:2976:32)
at D:\cliquenext\flutter\stashify\backend\node_modules\mongoose\lib\document.js:2765:17
at D:\cliquenext\flutter\stashify\backend\node_modules\mongoose\lib\schematype.js:1334:9
at processTicksAndRejections (internal/process/task_queues.js:79:11)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:32308) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:32308) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Basically I am trying to get a conversation, with its messages and its participants and return it.
The conversation object model is like so:
const conversationSchema = new Schema(
{
isGroup: {
type: Boolean,
required: true,
},
creator: {
type: Schema.Types.ObjectId,
ref: 'User',
},
passwordProtected: {
type: Boolean,
default: false,
},
password: {
type: String,
},
participants: [
{
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
],
deleted: [
{
type: Schema.Types.ObjectId,
ref: 'User',
},
],
seen: [
{
type: Schema.Types.ObjectId,
ref: 'User',
},
],
messages: [
{
type: Schema.Types.ObjectId,
ref: 'Message',
},
],
},
{ timestamps: true },
);
and this is my endpoint:
exports.getMessagesFromUser = async (req, res, next) => {
try {
// variables
const senderId = req.userId;
const receiverId = req.body.receiverId;
// get users
const sender = await User.findOne({ _id: senderId });
const receiver = await User.findOne({ _id: receiverId });
const ids = [senderId, receiverId];
// look for an existing conversation between two users
// where the participants include both users
// where the conversation is a single conversation
// that the sender hasn't deleted
// if sender chooses to send a message, any undeleted conversations will be resurrected
let conversation = await Conversation.findOne({
isGroup: false,
participants: {
$all: ids,
},
})
.select('participants deleted seen messages')
.populate({ path: 'messages', select: 'sender message deleted seen createdAt' })
.populate({ path: 'participants', select: '_id' })
.populate({ path: 'participants', select: 'nickname' });
if (conversation) {
// conversation exists
let senderBlocked = false;
let receiverBlocked = false;
let conversationDeleted = false;
let filter;
// check if sender has blocked the receiver
filter = sender.blocked.filter((u) => u._id == receiverId);
if (filter.length > 0) {
senderBlocked = true;
}
filter = null;
// check if receiver has blocked the sender
filter = receiver.blocked.filter((u) => u._id == senderId);
if (filter.length > 0) {
receiverBlocked = true;
}
filter = null;
// check if sender has deleted the conversation
filter = conversation.deleted.filter((u) => u._id == senderId);
if (filter.length > 0) {
conversationDeleted = true;
}
// get, map and return the messageSchema
res.status(200).json({
message: 'Conversations fetched successfully!',
conversation: conversation,
senderBlocked: senderBlocked,
receiverBlocked: receiverBlocked,
conversationDeleted: conversationDeleted,
});
} else {
// if conversation does not exist, return empty array
// conversations will be created upon initial message between users
res.status(200).json({
message: 'No Conversations Yet.',
conversation: [],
senderBlocked: false,
receiverBlocked: false,
conversationDeleted: false,
});
}
} catch (e) {
e.statusCode = 500;
next(e);
}
};
I was able to determine the error was throwing inside this endpoint but couldn't for the life of me figure out why. I have added a try, catch and next block. Made sure I had the appropriate awaits in order, etc. My only guess here is the mongoose query of populate and select is wrong somehow? What do you think I am doing wrong here? Please and thank you.
As jimmy pointed out, the problem wasn't there. it was in another socket route where I was trying to save new conversations if they didnt exist.
So I had something like:
socket.on('join-inbox-rooms', async (data) => {
// decode json string
const decodedData = await JSON.parse(data);
// get conversation
let conversation = await Conversation.findOne({
isGroup: false,
participants: {
$all: [decodedData.senderId, decodedData.receiverId],
},
});
if (!conversation) {
conversation = new Conversation({
isGroup: false,
creator: decodedData.senderId,
passwordProtected: false,
paassword: null,
participants: [
decodedData.senderId,
decodedData.receiverId,
],
deleted: [],
seen: [],
messages: [],
});
await conversation.save();
}
Now I was getting the ids from the front end as strings. They should be mongo objects.
I ended up fixing it like so:
const { ObjectId } = require('mongodb');
if (!conversation) {
conversation = new Conversation({
isGroup: false,
creator: decodedData.senderId,
passwordProtected: false,
paassword: null,
participants: [
ObjectId(decodedData.senderId),
ObjectId(decodedData.receiverId),
],
deleted: [],
seen: [],
messages: [],
});
await conversation.save();
}

Blank document and unhandled promise rejection when making an add subdocument request

I am working on my first request that would add a subdocument to a document in MongoDB and I'm struggling with it. My database is a collection of users, and each user has an array of words they working on learning to translate in the application I am building. I am currently having two issues
when I make a request in postman to add a new word to my user's array of words, I add a new object that has ID, but none of the other property value pairs that I have in the word sub-model, and in the request(greek, english, success, timestamp).
my command prompt gives me the following errors
(node:8320) UnhandledPromiseRejectionWarning: Unhandled promise
rejection. This error originated either by throwing inside of an async
function without a catch block, or by rejecting a promise which was
not handled with .catch(). (rejection id: 3) (node:8320)
UnhandledPromiseRejectionWarning: ValidationError: user validation
failed: words.0.greek: Path greek is required., words.0.english:
Path english is required., words.0.success: Path success is
required., words.0.timeStamp: Path timeStamp is required.
The second error is confusing because in my mind it should be word.greek and words.english to get the value from the each word object. However, it adds a 0 between the object and its property/value pair.
My mongoose model for the subdocument is as follows
const wordSchema = new Schema({
greek: {
type: String,
required: true,
trim: true,
minlength: 1,
index: { unique: true, sparse: true },
},
english: {
type: String,
required: true,
minlength: 1
},
success: {
type: Number,
required: true
},
timeStamp: {
type: Date,
required: true
},
});
This is my request to add the word to the User's array of words.
router.post("/update/:id",(req, res) =>{
console.log(req.body.greek)
var greek = req.body.greek;
var english = req.body.english;
var success = req.body.success;
var timeStamp = req.body.timeStamp
var newWord = {
greek: greek,
english: english,
success: success,
timeStamp: timeStamp
}
User.findById(req.params.id)
.then((user) => {
user.words.push(newWord);
user.save()
res.status(200).json(user)
.catch((err) => {res.status(400).json(err)})
})
.catch((err) => {res.status(400).json("Error: "+err)})
});
Any help would be greatly appreciated! I've done some googling on adding subdocuments to a document, but I still haven't found the solution.
Instead of fetching and then updating the document you can directly update the document in one DB call.
router.post("/update/:id",(req, res) =>{
console.log(req.body.greek)
var greek = req.body.greek;
var english = req.body.english;
var success = req.body.success;
var timeStamp = req.body.timeStamp
var newWord = {
greek: greek,
english: english,
success: success,
timeStamp: timeStamp
}
User.findOneAndUpdate({ _id: req.params.id }, {
$push: {
words: newWord
}
})
.then((user) => {
if(!user){
return res.status(404).json({
message: 'Not User matches given ID'
});
}
res.status(200).json(user);
})
.catch((err) => {res.status(400).json("Error: "+err)})
});
one thing I see is user.save() will return a promise, which you do not handle, hence the document will not be save. maybe consider:
User.findById(req.params.id)
.then(async (user) => {
user.words.push(newWord);
await user.save()
res.status(200).json(user)
.catch((err) => {res.status(400).json(err)})
})
.catch((err) => {res.status(400).json("Error: "+err)})

Mongoose saves invalid data without throwing validation errors if model.validate() is called first

MongoDB 4.2.2 and Mongoose 5.8.3 (latest) and NodeJS 13.3.0 (Windows x64)
If I create a schema and model, then create an instance of the model and add some data, then run validate(), then save(): even if validate() fails, the data is saved into the collection, without throwing an additional validation error.
Is this a bug, or am I doing something wrong?
Here's the test code:
var mongoose = require('mongoose')
mongoose.connect("mongodb://user:pass#localhost/mydb")
db = mongoose.connection
var Schema = mongoose.Schema
var PartSchema = new Schema({
name: {
type: String,
required: true,
validate: {
validator: (v) => v !== 'asdf' // Don't allow name to be 'asdf'
}
},
number: {
type: String,
required: true,
validate: {
validator: (v) => !v.includes(' ') // Don't allow spaces in part number.
}
}
})
var ProductSchema = new Schema({
name: String,
parts: [PartSchema]
})
var Part = mongoose.model('Part', PartSchema)
var Product = mongoose.model('Product', ProductSchema)
var p1 = new Product({name:"Baseball Bat", parts:[ new Part({name:"First part", number: "003344"}), new Part({name: "Second part", number: "554422"}) ]})
p1.parts.push(new Part({name: "No number, so invalid"})) // this one is invalid because no part number is specified (required)
p1.parts.push(new Part({name: 'asdf', number: 'zzzzzaaaa'}))
p1.parts.push(new Part({name: 'bbbb', number: 'with a space'})) // This one is invalid because number has spaces.
p1.validate()
.then(() => {console.log('Validation successful')})
.catch((err) => { console.log("Validation failed.")})
p1.save()
.then(()=>{ console.log("Saved successfully")})
.catch((err)=>{console.log("Save ERROR", err)})
Running this code yields the following:
Validation failed.
Saved successfully
And the new document appears in the database:
However, if I remove the p1.validate() before calling save(), the save function's catch() block triggers and the item is not saved:
Save ERROR Error [ValidationError]: Product validation failed: parts.2.number: Path `number` is required., parts.3.name: Validator failed for path `name` with value `asdf`, parts.4.number: Validator failed for path `number` with value `with a space`
at ValidationError.inspect
... snipped
May be you need to use p1.save() inside the promise chain.
p1.validate()
.then(res => {
console.log("Validation successful");
})
.then(() => {
return p1.save();
})
.then(res => {
console.log("saved success ", res);
})
.catch(err => {
console.log("Some error.", err);
});

Mongoose ignores string attributes in post requests

I am currently developing a RESTful API with Express using TypeScript and MongoDB. My ORM of choice is mongoose. When test my Application with the following POST request:
curl -i -X POST -H "Content-Type: application/json" -d '{ "first_name":"hansi"}' localhost:3000
I get the following error message, stating that the attribute "first_name" is required:
{ ValidationError: Person validation failed: first_name: Enter a first name
at ValidationError.inspect (/Users/schnabl/s6/node_modules/mongoose/lib/error/validation.js:59:24)
at formatValue (internal/util/inspect.js:453:31)
at inspect (internal/util/inspect.js:193:10)
at Object.formatWithOptions (util.js:85:12)
at Console.(anonymous function) (console.js:188:15)
at Console.log (console.js:199:31)
at /Users/schnabl/s6/src/routes/person.js:23:29
at /Users/schnabl/s6/node_modules/mongoose/lib/model.js:4533:16
at parallel (/Users/schnabl/s6/node_modules/mongoose/lib/model.js:2667:16)
at /Users/schnabl/s6/node_modules/async/internal/parallel.js:39:9
at /Users/schnabl/s6/node_modules/async/internal/once.js:12:16
at iteratorCallback (/Users/schnabl/s6/node_modules/async/eachOf.js:60:13)
at /Users/schnabl/s6/node_modules/async/internal/onlyOnce.js:12:16
at /Users/schnabl/s6/node_modules/async/internal/parallel.js:36:13
at callbackWrapper (/Users/schnabl/s6/node_modules/mongoose/lib/model.js:2640:20)
at /Users/schnabl/s6/node_modules/mongoose/lib/model.js:4533:16
errors:
{ first_name:
{ ValidatorError: Enter a first name
at new ValidatorError (/Users/schnabl/s6/node_modules/mongoose/lib/error/validator.js:29:11)
at validate (/Users/schnabl/s6/node_modules/mongoose/lib/schematype.js:871:13)
at /Users/schnabl/s6/node_modules/mongoose/lib/schematype.js:924:11
at Array.forEach (<anonymous>)
at SchemaString.SchemaType.doValidate (/Users/schnabl/s6/node_modules/mongoose/lib/schematype.js:880:19)
at /Users/schnabl/s6/node_modules/mongoose/lib/document.js:1913:9
at process._tickCallback (internal/process/next_tick.js:61:11)
message: 'Enter a first name',
name: 'ValidatorError',
properties: [Object],
kind: 'required',
path: 'first_name',
value: undefined,
reason: undefined,
[Symbol(mongoose:validatorError)]: true } },
_message: 'Person validation failed',
name: 'ValidationError' }
However, when I comment the '"required: "enter a string"' out, no server error is thrown. But still the first_name and all the other string attributes I use in my schema aren't added to the collection. There are just the Mongo generated "ids" and default values for the birth_date (current_timestamp) in the collection.
My schema (personSchema.ts):
//imports
const Schema = mongoose.Schema;
export const PersonSchema = new Schema({
first_name: {
type: String
//required: "Enter a first name"
},
...
birth_date: {
type: Date,
default: Date.now
}
}, { collection: 'person' });
The router (personRouter.ts):
//imports
export class PersonRoute {
public personController: PersonController = new PersonController();
public routes(app): void {
...
//getrequests
...
app.route("/").post((req: Request, res: Response) => {
const PersonModel = mongoose.model('Person', PersonSchema);
PersonModel.create(req.body, function (err, post) {
if (err) console.log(err);
res.json(post);
});
});
}
}
Is it possible that there is something wrong with my test request?
EDIT:
I tested the post request in Postman and still getting the same result:

Trouble manually setting the model property using mongoose

I have a simple model, which is:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var citySchema = new Schema({
name: { type: String, required: true },
state: { type: Schema.Types.ObjectId, ref: 'State' }
});
module.exports = mongoose.model('City', citySchema);
Only i access a route, asking to insert a city, giving as post params
POST: {
name: 'My City',
state: 'SE' // Stands for Some State
}
I know the type of the state property is not correct, but in my logic i do:
var newCity = new City(req.body);
if (typeof req.body.state !== 'undefined' && req.body.state.length == 2) {
State.findOne({uf: req.body.state.toUpperCase()}, function(err, foundState) {
if (err) { res.send({status: 500, message: 'Could not find the required state'}); return; }
newCity.state = foundState._id;
newCity.set('state', foundState._id);
return;
});
}
But, once i do a res.send(newCity), to check out the newCity variable properties, it prints:
{
"name": "Balneário Camború",
"_id": "570ff2944c6bd6df4e8e76e8"
}
And if i try to save it, i get the following error:
ValidationError: CastError: Cast to ObjectID failed for value \"SE\" at path \"state\""
So, i'm quite confused, because when the Model is created using the req.body properties, it does not list the state property, even if i set it later in the code, but when i try to save the City, it throw an error of mistype.
What could be causing this, and how should i procede?

Resources