Mongoose query with await returns undefined - node.js

Following code is written in a loop in a express route handler;
const projects = await Projects.find({});//working fine
for (let i = 0; i < projects.length; i++) {
const project = projects[i];
const build = await Build.find({ id: project.latest_build});
//logger.debug(build[0].status); //error
const features = await Feature.find({ build_id: project.latest_build});
logger.debug(features[0].status);
}
The above code gives error at liine 4.
(node:672375) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'status' of undefined
However, It is correctly printed in the logs. Seems like the value of the variable is being filled by a promise lazily.
But Line number 6 always works fine.
Note: I'm not performing any other read/write operations on above collections.
Update
id is unique key for Build collection.
build_id is normal field for Feature collection.
Schemas and Documents are created like this
Schemas.Projects = new mongoose.Schema({
id: {
type: String,
unique: true
},
title: String,
latest_build: String
});
Schemas.Build = new mongoose.Schema({
id: {
type: String,
unique: true
},
run_date: Date,
status: String,
});
Schemas.Feature = new mongoose.Schema({
id : String,
build_id: String,
summary : String,
status: String,
flows: Number,
});
const Projects = mongoose.model('Projects', Schemas.Projects);
const Build = mongoose.model(`Build_${projId}`, Schemas.Build);
const Feature = mongoose.model(`Feature_${projId}`, Schemas.Feature);

The problem with the code in question is that it was overly optimistic in its expectations. This line...
const build = await Build.find({ id: project.latest_build });
logger.debug(build[0].status)
... assumed the query always finds at least one Build document. But, as the code was running as a part of the loop, the results were actually mixed:
at the first iteration, the query did find the Build object by the first project's data, and correctly logged its status
at the second iteration it gave back an empty array instead. Then the code threw an Error on attempting to access status property of build[0] (which was undefined).
The appropriate solution for this issue depends on how you should treat those cases. If each Project must have a corresponding Build (it's a strictly 1-1 relation), then having an Error thrown at you is actually fine, as you just have to fix it.
Still, it might be worth treating it as a 'known error', like this:
const buildId = project.latest_build;
const builds = await Build.find({ id: buildId });
if (!builds.length) {
logger.error(`No build found for ${buildId}`);
continue;
}
In this case (assuming logger is set up correctly) you won't have your code blowing up, yet the error will be logged. But if it's actually an ok situation to have no builds yet, just drop the logging - and treat this case as a known edge case.

Related

Does empty string match mongo db schema of required?

I have a mongodb schema like below.
const DemoSchema: Schema = new Schema({
demoProperty1: { type: String, required: true }
});
I create a document with syntax in node js server like below.
const savedDemo = await this.demoModel.create({demoProperty1:""});
Why it is returning error message
UnhandledPromiseRejectionWarning: ValidationError:
How to fix this issue?
From the documentation:
// Use this to allow empty strings to pass the `required` validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
Yes I have fix this issue by trial and error.
Empty string is not eligible for mongodb required schema,
Took me 5 hours to figure it out. I can just passed in any string but not an empty string to make it valid and passed without any error.

Preventing NoSQL injection: Isn't mongoose supposed to convert inputs based on given schema?

Looking to prevent NoSQL injection attacks for a node.js app using mongodb.
var mongoose = require('mongoose'); // "^5.5.9"
var Schema = mongoose.Schema;
var historySchema = new Schema({
userId: {
type: String,
index: true,
},
message: {},
date: {
type: Date,
default: Date.now,
}
});
var history = mongoose.model('history', historySchema);
// the following is to illustrate the logic, not actual code
function getHistory(user){
history.find({userId: user}, function(err, docs) {
console.log(docs)
}
}
Based on this answer to a similar question, my understanding is that using mongoose and defining the field as string should prevent query injection. However, by changing the user input to a query object, it is possible to return all users. For example:
getHistory({$ne: 1}) // returns the history for all users
I am aware of other ways to prevent this type of attack before it gets to the mongoose query, like using mongo-sanitize. But I'd like to know if there's something wrong with the way I defined the schema or if one can't expect mongoose to convert inputs according to the schema.
Thanks in advance!
this part is good enough, you do not need anything else there. There is method that receives string and uses the string.
The best approach is to validate the input that can be modified (usually HTTP request) on top level before processing anything (I can recommend https://github.com/hapijs/joi its easy to use and you can check if there all required fields and if all fields are in correct format).
So put the validation into middleware just before it hits your controller. Or at the beginning of your controller.
From that point you are in full control of all the code and you believe what you got through your validation, so it cannot happen that someone pass object instead of string and get through.
Following the "skinny controllers, fat model" paradigm, it would be best to expose a custom validation schema from your model to be used in your controller for POST and PUT requests. This means that any data that attempts to enter your database will first be sanitized against a validation schema. Every Mongoose model should own its own validation schema.
My personal favorite for this is Joi. It's relatively simple and effective. Here is a link to the documentation: https://www.npmjs.com/package/#hapi/joi
A Joi schema permits type checking (i.e., Boolean vs. String vs. Number, etc), mandatory inputs if your document has the field required, and other type-specific enforcement such as "max" for numbers, enumerable values, etc.
Here is an example you'd include in your model:
const Joi = require('joi');
...
function validateHistory(history) {
const historySchema = {
userId: Joi.string(),
message: Joi.object(),
date: Joi.date()
}
return Joi.validate(history, historySchema);
}
...
module.exports.validate = validateHistory;
And then in your controller you can do:
const {
validate
} = require('../models/history');
...
router.post('/history', async (req, res) => {
const {
error
} = validate(req.body.data);
if (error) return res.status(400).send(error.details[0].message);
let history = new History({
userID: req.body.user,
message: req.body.message,
date: req.body.date
})
history = await history.save();
res.send(history);
});
*Note that in a real app this route would also have an authentication callback before handling the request.

Automatically manipulating argument for Mongoose Document constructor

Let's say I have have this model:
const employeeSchema = new Schema({
name: String,
age: Number,
employeeData: {
department: String,
position: String,
lastTraining: Date
}
});
const Employee = mongoose.model('employee', employeeSchema);
In the database, the only thing that is going to be saved is something that looks like this:
{
_id: ...
name: 'John Smith',
age: 40,
employeeCode: '.... '
}
What's going on is that by some business rules, the employeeData info, which is coming from the reqeust body, is going through some function that compiles out of it the employeeCode, and when saving to the database I just use to the employeeCode.
Right now, the way I am implementing this is using statics. So, I have in the model the follwing:
employeeSchema.statics.compileEmployeeCode = (doc) => {
if (!doc.employeeData) {
doc.employeeCode= compileCode(doc.employeeData);
delete doc.employeeData;
}
return doc;
}
And then, I need to remember, for each call that receives info from the client, to call this function before creating the document (an instance of the model):
const compiledDoc = Employee.compileEmployeeCode(req.body);
const employee = new Employee(comiledDoc);
My question is: is there a way to automatically invoke some function that compiles the code out of the data any time I create a document like that, so I won't need to remember to always call on the static method beforehand?
Middlaware is what you are looking for. You need to create a function that will set a pre-save hook on the schema (which will be triggered every time before saving a new document) and to plug this function into the schema.
function compileEmployeeCode (schema) {
schema.pre('save', next => {
if (this.employeeData) {
this.employeeCode= compileCode(this.employeeData);
delete this.employeeData;
next();
}
});
}
employeeSchema.plugin(compileEmployeeCode);
OK. It was really hard but I finally managed to find the solution. The trick is to use a setter on a specific path. Each field in the schema is of type SchemaType which can have a setter apply on it:
https://mongoosejs.com/docs/api.html#schematype_SchemaType-set
Anyway, if I want to make it possible for the request to enter an object that will be converted to some other format, say a string, I would need to define the schema like this:
const employeeSchema = new Schema({
name: String,
age: Number,
employeeCode: {
type: String,
set: setCodeFromObj,
alias: 'employeeData'
}
});
The setter function I'm using here looks something like this (I'm omitting here all the error handling and the like to keep this short:
function setCodeFromObj(v) {
const obj = {};
obj.department = v.department;
obj.position = v.position;
obj.lastTraining = v.lastTraing
// breaking the object to properties just to show that v actually includes them
return compileEmployeeCode(obj);
}
I used an alias to make the name visible to the user different from what is actually saved in the database. I could have also done that using virtuals or just design the system a bit differently to use up the same name.

Invalid Object Passes Schema Validation

In a meteor app I've been getting my feet wet with SimpleSchema lately. I've built the below mentioned pretty simple schema :-)
However, when I try to validate an invalidate entry (say one where entry.link.url ist not a valid URL or one where entry.link.project is undefined) against that schema via entrySchema.validate() the validation does not work properly, i.e. the invalid entry passes the validation whereas I would expect it to fail.
let entrySchema = new SimpleSchema({
userId: {
type: String,
optional: true
},
link: {
type: Object
},
'link.project': {
type: String //this validation does not work!
},
'link.url': {
type: SimpleSchema.RegEx.Url //this validation does not work!
}
});
Can anyone please tell me what I am doing wrong or what I am missing here?

Adding fields to model which derived from Mongoose schema

I have a Mongoose schema that looks like this:
ManifestSchema = new Schema({
entries: [{
order_id: String,
line_item: {}, // <-- resolved at run time
address: {},// <-- resolved at run time
added_at: Number,
stop: Number,
}]
}, {collection: 'manifests', strict: true });
and somewhere in the code I have this:
Q.ninvoke(Manifests.findById(req.params.id), 'exec')
.then(function(manifest)
{
// ... so many things, like resolving the address and the item information
entry.line_item = item;
entry.address = order.delivery.address;
})
The issue that I faced is that without defining address and line_item in the schema, when I resolved them at run time, they wouldn't returned to the user because they weren't in the schema...so I added them...which cause me another unwanted behavior: When I saved the object back, both address and line_item were saved with the manifest object, something that I would like to avoid.
Is there anyway to enable adding fields to the schema at run time, but yet, not saving them on the way back?
I was trying to use 'virtuals' in mongoose, but they really provide what I need because I don't create the model from a schema, but it rather returned from the database.
Call toObject() on your manifest Mongoose instance to create a plain JavaScript copy that you can add extra fields to for the user response without affecting the doc you need to save:
Q.ninvoke(Manifests.findById(req.params.id), 'exec')
.then(function(manifest)
{
var manifestResponse = manifest.toObject();
// ... so many things, like resolving the address and the item information
entry.line_item = item;
entry.address = order.delivery.address;
})

Resources