Reputation: 9477
In my application, I have the user model as follows. When I validate the confirm passwords, some business logic part was there in the mongoose model. How can I put this in a nicer way? Should I seperate the validation part from model? Or should I keep the validation in the mongoose model?
import Joi from 'joi';
import { Schema, model } from 'mongoose';
const userSchema = new Schema(
{
firstName: {
type: String,
required: true,
minlength: 2,
maxlength: 30,
},
lastName: {
type: String,
required: true,
minlength: 2,
maxlength: 30,
},
email: {
type: String,
required: true,
minlength: 5,
maxlength: 100,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 8,
maxlength: 1024,
},
avatar: {
type: String
},
},
);
export const User = model('User', userSchema);
/** Validate user */
export function validateUser(user) {
const schema = {
firstName: Joi.string().min(2).max(30).required(),
lastName: Joi.string().min(2).max(30).required(),
email: Joi.string().min(5).max(100).required().email(),
password: Joi.string().min(8).max(1024).required(),
confirmPassword: Joi.any().valid(Joi.ref('password')).required().options({ language: { any: { allowOnly: 'Passwords do not match' } } }),
avatar: Joi.string(),
};
return Joi.validate(user, schema, { abortEarly: false });
}
/** Validate login */
export function validateLogin(login) {
const schema = {
email: Joi.string().min(5).max(255).required().email(),
password: Joi.string().min(5).max(255).required()
};
return Joi.validate(login, schema);
}
Upvotes: 1
Views: 737
Reputation: 3321
Maybe you should try asking on CodeReview, since it's more about design than issues.
Anyway, here's my opinion (because, since it's design, it's mostly a question of opinion).
I wouldn't use Mongoose to validate object-related constraints.
In your example, Mongoose would only declare the mandatory type
, and unique
because it asks the DB to index this property (by extension, index
or sparse
as well).
Simply because it's Joi's purpose is to validate objects, making it clearly better and easier to handle than Mongoose's features.
In your example, there's no need for Mongoose to (double-)check minlength
and maxlength
because Joi did it already - provided you properly controlled the entry points to your DB. lowercase
can also be handled by Joi. These have no effect on the way Mongo would store the data, it's pure business just like the password
/confirmPassword
equality check.
(As a side note, you can also .strip()
this confirmPassword
property to avoid doing it in your request handler)
I wouldn't expose a function that validates an entity, instead I would expose a POST and a PUT schemas. In most cases, you can automatically derive the PUT from the POST (just adding an ID after all), yet users with password confirmations are a typical example of the need to differentiate.
This way your can build the classical REST API almost generically here: get the name of the route using files/modules, try their exposed validation depending on HTTP method used, then use their exposed model to do the persistence job. And then, you can think about making authorization generic as well, which makes a fully generically-built API on entities if your datamodel articulates on the user in session, but that's another story.
Keeping both Mongoose and Joi schemas in the same file also allows developers (and you, in 6 months, when you forgot how you coded it) to quickly understand how a specific entity must be used. Which means that when someone develops a CLI script inserting data into the DB, they will have the validation schema to use close by, and you can blame them if they "forget" to use it: again, control entry points to the DB. ;)
I like my entity files to keep being entity files.
You expose a way to validate login, however you know one cannot insert something that doesn't comply with Joi and Mongoose schemas. Therefore, if someone wants to log in using a 1500-character-long email, let them: it'll end up being a normally-rejected login as nothing would match in DB. Doesn't mean you can't implement a UI validation though, but it also means adapt the UI code whenever the schema changes.
Yet in a more general way, logging in indeed is not a normal, potentially generic endpoint. These are the only cases (AFAIK) where you might want a special validation step. Since these routes are totally business-related, you need to handcraft them, and that's where I would put these special - also business-related - validation steps. Anyway, not in the "closest related" entity.
Here's about what I'd have in my user's entity file:
import Joi from 'joi';
import { Schema, model } from 'mongoose';
// purely DB stuff
export default model('User', new Schema(
{
firstName: { type: String },
lastName: { type: String },
email: { type: String, unique: true },
password: { type: String },
avatar: { type: String }
}
));
// purely business stuff
// common schema first
const schema = {
firstName: Joi.string().min(2).max(30).required(),
lastName: Joi.string().min(2).max(30).required(),
email: Joi.string().min(5).max(100).required().lowercase().email(),
password: Joi.string().min(8).max(1024),
avatar: Joi.string(),
};
// POST-specific case
const post = {
...schema,
// note that for this particular case, a UI check is enough
confirmPassword: Joi.string().valid(Joi.ref('password')).required()
.options({ language: { any: { allowOnly: 'Passwords do not match' } } })
.strip()
};
// password is only required on registration
post.password = post.password.required();
// PUT-specific case
const put = {
...schema,
id: Joi.string().required()
};
// you may also want to use a derived Joi schema on GET output to strip some data like password
export const joi = { post, put };
Upvotes: 5