Mongoose: How to validate length of array based on another documents variable - node.js

I am trying to do validation within the schema to validate the length of an array based on another documents length of its array. Suppose:
const Level = mongoose.Schema({
level: {
type: Array,
required: true,
items: {
exercise: {
type: mongoose.Schema.Types.ObjectId,
ref: "Exercise",
required: true,
},
accuracyThreshold: { type: Number, required: true },
timeThreshold: { type: Number, required: true },
},
},
});
const UserLevelProgress = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
level: {
type: mongoose.Schema.Types.ObjectId,
ref: "Level",
required: true,
},
progress: {
type: Array,
required: true,
validate: {
validator: (progress) => {
return progress.length === level.level.length; // level.level.length is not valid
},
message: () => "level progress legnth is invalid!",
},
items: {
accuracyScore: { type: Number, required: true },
timeScore: { type: Number, required: true },
},
},
});
module.exports = mongoose.model("UserLevelProgress", UserLevelProgress);
module.exports = mongoose.model("Level", Level);
I have a validator function in UserLevelProgress, but obviously level is not valid object. How do I get access to this object?

The short answer is: You could implement this validation logic, but is it really a good idea to do this?
Downsights
validation is taking place in the pre('save') hook. This means that once you initially save of update a document, the validation will run.
since Level documents are stored in a different collection, the values (including length of the array) can change without UserLevelProgress documents noticing. This way your validator would not rerun (as said before it's run in pre('save') hook and your documents would technically be not valid anymore.
Better approach
Check if Level schema can be included as a nested document, instead of a referenced one. This way validation would be much easier and data will not be able to become invalid unnoticed.
Implementation if you still want to follow your intended approach
validate: {
validator: async function(progress) {
const level = await Level.findById(this.level);
return progress.length === level.level.length; // level.level.length is not valid
},
message: () => "level progress legnth is invalid!",
},

Related

Mongoose findBiId

I am working on a real estate project. The property's model is:
const propertySchema = new mongoose.Schema({
owner:{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
title: {
type: String,
require: true,
},
address: {
type: String,
require: true,
},
secondAddress: String,
city: {
type: String,
require: true,
},
state: {
type: String,
require: true,
},
zip: {
type: String,
require: true,
},
});
When I try to get the information of a specific property I get the error Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer. If I hardcode the id everything works. The route to get to the property is:
router.get('/property/:id', async(req, res) => {
const property = await Property.findById(req.params.id)
res.render('pages/user/property', { property })
})
How should I approach this?
You need _id in your schema in order to use findById, but you don't have one.
See documentation here,
and also this answer by JohnnyHK

Whats Best way to Model following Mongodb Model

I have 2 models like this
const testRunSchema = new mongoose.Schema({
testTimeSlot: {
type: String,
required: true
},
testDate: {
type: String,
required: true
},
diagnosticData: [Object],
notes: String,
active: {
type: Boolean,
default: true
}
}, {
timestamps: true,
strict: false
})
const testingSchema = new mongoose.Schema({
testId: {
type: mongoose.Schema.ObjectId,
required: true
},
testDetails: {
//dummy data
},
contactDetails: {
//dummy data
},
testRunDetails: [testRunSchema], //is this a best way?
runByAssistant: Boolean
}, {
timestamps: true
});
module.exports = mongoose.model('Testing', testingSchema)
Now i want to access the testTimeSlot (which is first model) using the testId of the second model.
My Solution:
I can access the testimeSlot of first model, using testId because data of first model is available in testRunDetails of seconf model.
Problem With this solution:
Since testRunSchema is defined as a array in second model, its not easy and efficient to access the testTimeSlot of every array element.
What is the best way to solve this issue?
what you thought is correct it's not easy to access them besides the more the array gets filled the slower the query will be. So it would be better if you separate the testRunSchema and save the reference of it's data in to the testingSchema
const testingSchema = new mongoose.Schema({
testId: {
type: mongoose.Schema.ObjectId,
required: true
},
testDetails: {
//dummy data
},
contactDetails: {
//dummy data
},
testRunDetails:{
type:[Schema.Types.ObjectId], ref:'testRunSchema'
},//like this in here the object id of testRunSchema
runByAssistant: Boolean
}, {
timestamps: true
});
when you want to query, you just need to use polulate() in mongoose
you can read it in here

mongoose - findOneAndUpdate with $set flag

Consider this command:
WorkPlan.findOneAndUpdate({ _id: req.params.id }, updateObj, function(err) {
...
})
versus this:
WorkPlan.findOneAndUpdate({ _id: req.params.id }, { '$set': updateObj }, function(err) {
...
})
While developing my project, I was surprised to find out that the result of the first command is the same as the result of the second command: the updateObj is merged into the existing record in the database, even in the first case when it is supposed to replace it. Is this a bug in mongoose/mongodb or am I doing something wrong? how can I replace an object on update instead of merging it? I'm using mongoose 4.0.7.
Thanks.
==========
Update:
This is the actual WorkPlan schema definition:
workPlanSchema = mongoose.Schema({
planId: { type: String, required: true },
projectName: { type: String, required: true },
projectNumber: { type: String, required: false },
projectManagerName: { type: String, required: true },
clientPhoneNumber: { type: String, required: false },
clientEmail: { type: String, required: true },
projectEndShowDate: { type: Date, required: true },
segmentationsToDisplay: { type: [String], required: false },
areas: [
{
fatherArea: { type: mongoose.Schema.ObjectId, ref: 'Area' },
childAreas: [{ childId : { type: mongoose.Schema.ObjectId, ref: 'Area' }, status: { type: String, default: 'none' } }]
}
],
logoPositions: [
{
lat: { type: Number, required: true },
lng: { type: Number, required: true }
}
],
logoPath: { type: String, required: false },
}, { collection: 'workPlans' });
WorkPlan = mongoose.model('WorkPlan', workPlanSchema);
And this is an example of updateObj:
var updateObj = {
projectManagerName: projectManagerName,
clientEmail: clientEmail,
clientPhoneNumber: clientPhoneNumber,
segmentationsToDisplay: segmentationsToDisplay ? segmentationsToDisplay.split(',') : []
}
Therefore, when I'm NOT using the $set flag, I would expect the field projectNumber, for example, not to exist in the new record, yet I see it is still there.
Mongoose update treats all top level keys as $set operations (this is made more clear in the older docs: Mongoose 2.7.x update docs).
In order to get the behavior you want, you need to set the overwrite option to true:
WorkPlan.findOneAndUpdate({ _id: req.params.id }, updateObj, { overwrite: true }, function(err) {
...
})
See Mongoose Update documentation
In addition to the answer above:
[options.overwrite=false] «Boolean» By default, if you don't include
any update operators in doc, Mongoose will wrap doc in $set for you.
This prevents you from accidentally overwriting the document. This
option tells Mongoose to skip adding $set.
Link to docs: https://mongoosejs.com/docs/api.html#model_Model.update
This is works for me $set in Mongoose 5.10.1,
WorkPlan.where({ _id: req.params.id }).updateOne(updateObj);
Note:if you have inner object then give exact path of each key in updateObj
example:
"Document.data.age" = 19
ref: https://mongoosejs.com/docs/api.html#query_Query-set

Validate subdocuments in Mongoose

I am using a Schema as a Subdocument in Mongoose, but I am not able to validate it in its fields.
That's what I have
var SubdocumentSchema = new Schema({
foo: {
type: String,
trim: true,
required: true
},
bar: {
type: String,
trim: true,
required: true
}
});
var MainDocumentSchema = new Schema({
name: {
type: String,
trim: true,
required: true
},
children: {
type : [ SubdocumentSchema.schema ],
validate: arrayFieldsCannotBeBlankValidation
}
});
I want to be sure that every field of the subdocument is not empty.
I found out that this is not possible to validate an Array with standard methods, so I wrote my custom validation function.
By now I have to manually check that all the fields are correct and not empty, but it looks to me like a not really scalable solution, so I was wondering if there was some native method to trigger the Subdocument validation from the MainDocument one.
In the definition of children, it should be [SubdocumentSchema], not [SubdocumentSchema.schema]:
var MainDocumentSchema = new Schema({
name: {
type: String,
trim: true,
required: true
},
children: {
type : [ SubdocumentSchema ],
validate: arrayFieldsCannotBeBlankValidation
}
});
SubdocumentSchema.schema evaluates to undefined so in your current code Mongoose doesn't have the necessary type information to validate the elements of children.

mongoose how to manage count in a reference document

So I've got these schemas:
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Comment Schema
*/
var CommentSchema = new Schema({
post_id: {
type: Schema.Types.ObjectId,
ref: 'Post',
required: true
},
author:{
type: String,
required: true
},
email:{
type: String,
required: true
},
body: {
type: String,
required: true,
trim: true
},
status: {
type: String,
required: true,
default: 'pending'
},
created: {
type: Date,
required: true,
default: Date.now
},
meta: {
votes: Number
}
});
/**
* Validations
*/
CommentSchema.path('author').validate(function(author) {
return author.length;
}, 'Author cannot be empty');
CommentSchema.path('email').validate(function(email) {
return email.length;
}, 'Email cannot be empty');
CommentSchema.path('email').validate(function(email) {
var emailRegex = /^([\w-\.]+#([\w-]+\.)+[\w-]{2,4})?$/;
return emailRegex.test(email);
}, 'The email is not a valid email');
CommentSchema.path('body').validate(function(body) {
return body.length;
}, 'Body cannot be empty');
mongoose.model('Comment', CommentSchema);
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
monguurl = require('monguurl'),
Schema = mongoose.Schema;
/**
* Article Schema
*/
var PostSchema = new Schema({
title: {
type: String,
required: true,
trim: true
},
author:{
type: String,
required: true,
default: 'whisher'
},
slug: {
type: String,
index: { unique: true }
},
body: {
type: String,
required: true,
trim: true
},
status: {
type: String,
required: true,
trim: true
},
created: {
type: Date,
required: true,
default: Date.now
},
published: {
type: Date,
required: true
},
categories: {
type: [String],
index: { unique: true }
},
tags: {
type: [String],
required: true,
index: true
},
comment: {
type: Schema.Types.ObjectId,
ref: 'CommentSchema'
},
meta: {
votes: Number
}
});
/**
* Validations
*/
PostSchema.path('title').validate(function(title) {
return title.length;
}, 'Title cannot be empty');
PostSchema.path('body').validate(function(body) {
return body.length;
}, 'Body cannot be empty');
PostSchema.path('status').validate(function(status) {
return /publish|draft/.test(status);
}, 'Is not a valid status');
PostSchema.plugin(monguurl({
source: 'title',
target: 'slug'
}));
mongoose.model('Post', PostSchema);
by an api I query Post like
exports.all = function(req, res) {
Post.find().sort('-created').exec(function(err, posts) {
if (err) {
res.jsonp(500,{ error: err.message });
} else {
res.jsonp(200,posts);
}
});
};
How to retrieve how many comments has the post ?
I mean I want an extra propriety in post object
like post.ncomments.
The first thing I think of is adding an extra
field to the post schema and update it whenever a user
add a comment
meta: {
votes: Number,
ncomments:Number
}
but it seems quite ugly I think
If you want the likely the most efficient solution, then manually adding a field like number_comments to the Post schema may be the best way to go, especially if you want to do things like act on multiple posts (like sorting based on comments). Even if you used an index to do the count, it's not likely to be as efficient as having the count pre-calculated (and ultimately, there are just more types of queries you can perform when it has been pre-calculated, if you haven't chosen to embed the comments).
var PostSchema = new Schema({
/* others */
number_comments: {
type: Number
}
});
To update the number:
Post.update({ _id : myPostId}, {$inc: {number_comments: 1}}, /* callback */);
Also, you won't need a comment field in the PostSchema unless you're using it as a "most recent" style field (or some other way where there'd only be one). The fact that you have a Post reference in the Comment schema would be sufficient to find all Comments for a given Post:
Comments.find().where("post_id", myPostId).exec(/* callback */);
You'd want to make sure that the field is indexed. As you can use populate with this as you've specified the ref for the field, you might consider renaming the field to "post".
Comments.find().where("post", myPostId).exec(/* callback */);
You'd still only set the post field to the _id of the Post though (and not an actual Post object instance).
You could also choose to embed the comments in the Post. There's some good information on the MongoDB web site about these choices. Note that even if you embedded the comments, you'd need to bring back the entire array just to get the count.
It looks like your Post schema will only allow for a single comment:
// ....
comment: {
type: Schema.Types.ObjectId,
ref: 'CommentSchema'
},
// ....
One consideration is to just store your comments as subdocuments on your posts rather than in their own collection. Will you in general be querying your comments only as they related to their relevant post, or will you frequently be looking at all comments independent of their post?
If you move the comments to subdocuments, then you'll be able to do something like post.comments.length.
However, if you retain comments as a separate collection (relational structure in a NoSQL DB-- there are sometimes reasons to do this), there isn't an automatic way of doing this. Mongo can't do joins, so you'll have to issue a second query. You have a few options in how to do that. One is an instance method on your post instances. You could also just do a manual CommentSchema.count({postId: <>}).
Your proposed solution is perfectly valid too. That strategy is used in relational databases that can do joins, because it would have better performances than counting up all the comments each time.

Resources