I've been playing around with Sails for maybe one day. I'm trying to wrap my head around what would be the best way to do extensive validation in Sails.js.
Here is the scenario:
Registration Form:
Username: _______________
E-Mail: _______________
Password: _______________
Confirm: _______________
User inputs:
a correct e-mail
a username that already exists
two passwords that don't match
Desired outcome:
Username: _______________ x Already taken
E-Mail: _______________ ✓
Password: _______________ ✓
Confirm: _______________ x Does not match
Requirements, a few key points:
The user receives all error messages (not just the first one) for every aspect of his input. They are not vague ("username already taken" or "username must be at least 4 letters long" is better than "invalid username")
The built-in model validation can obviously not be responsible for checking a matched password confirmation (SRP)
What I think I need to do:
UserController:
create: function(req, res) {
try {
// use a UserManager-Service to keep the controller nice and thin
UserManager.create(req.params.all(), function(user) {
res.send(user.toJSON());
});
}
catch (e) {
res.send(e);
}
}
UserManager:
create: function(input, cb) {
UserValidator.validate(input); // this can throw a ValidationException which will then be handled by the controller
User.create(input, cb); // this line should only be reached if the UserValidator did not throw an exception
}
User: (model)
attributes: {
username: {
type: 'string',
required: true,
minLength: 3,
unique: true
},
email: {
type: 'email',
required: true,
unique: true
},
password: {
type: 'string',
required: true
}
}
UserValidator:
This is the tricky part. I need to combine input-specific validation (does the password confirmation match?) with the Model validation (is the username taken and is the e-mail address valid?).
If there was a way to instantiate a User-model and perform validation without saving to the database in Sails/Waterline I think this would be quite straight-forward, but there doesn't seem to be that option.
How would you go about solving this problem? Thank you very much for your help!
You can do this in your model:
module.exports = {
types: {
mycustomtype: function (password) {
return password === this.confirm;
}
},
attributes: {,
password:{
type: 'STRING',
required: true,
mycustomtype: true
}
}
}
There are going to be some validations that you can perform immediately on the client-side without needing to round-trip to the server. Things like comparing the password with the confirmation password, as well as verifying a string matches an email regex can be done with client-side javascript.
For other things like checking whether a username exists or not, you could use an ajax call to sails to directly ask it 'does this username exist' and provide real-time validation on the client-side based on the result, or you can wait until the user submits the form and parse the form submission to display those validations. Since checking ahead of time for things like this aren't 100% reliable (i.e. someone could create a user with that name after the check but prior to the form being posted back), some people choose to forgo the pre-check and only handle the error after post.
Waterline has its own built-in validation mechanism called Anchor, which is built on validator.js (previously called node-validator). For a full list of validations available, see here. I would recommend that instead of defining a separate validation layer, you define a method that parses the sails validation messages and formats them in a way that is user-friendly and consistent.
If you want to perform your own validations outside of what Waterline would do for you, you could do those validations inside a lifecycle callback, for instance the beforeCreate(values, callback) lifecycle callback. If you detect errors, you could pass them into the callback as the first parameter, and they would be passed back as an error to the caller of the create collection method.
An alternative to using a lifecycle callback, would be to create your own collection method that handles the create. Something like this:
Users.validateAndCreate(req.params.all(), function (err, user) {
...
});
More information about how to create a collection method like this can be found in my answer to this question: How can I write sails function on to use in Controller?
Related
Here is how my application works. A user logs in for the first time using Google Sign in. We get the following data from their Google Account:
Given name
Family name
Email ID
We wish to use this information to call our API (POST request) to create a user profile.
The data we send is
{
firstName: firstName ,
lastName: lastName,
email: email
}
Here is where the issue comes from. The user profile has many fields and one of them is designation. When the user logs in for the first time, we don't know their designation.
We are using MongoDB for our database. So we use Mongoose to set up the connection. In Mongoose model, we have added some validation for our schema. Designation is a required field. It should be at least one character of length and maximum of 40 characters. If we set designation as null, the validation would fail.
Is there any way to allow null in a required field in Mongoose?
Rather than setting required to true or false, you can pass it a function:
const user = new Schema({
designation: {
type: String,
minLength: 1,
maxLength: 40,
required: function() {
// Rather than checking a stored variable, you could check
// static functions on the model, a custom value on the
// instance that isn't persisted, etc.
return this.hasLoggedInAtLeastOnce === true;
// If this function returns true, the field is required.
}
}
hasLoggedInAtLeastOnce: {
type: Boolean,
}
});
Suppose the following User Schema in MongoDB (using Mongoose/Nodejs):
var UserSchema = new Schema({
email: {
type: String,
unique: true,
required: 'User email is required.'
},
password: {
type: String,
required: 'User password is required.'
},
token: {
type: String,
unique: true,
default: hat
},
created_at: {
type: Date,
default: Date.now
},
});
// mongoose-encrypt package
UserSchema.plugin(encrypt, {
secret: 'my secret',
encryptedFields: ['email', 'password', 'token', 'created_at']
});
Now assume I want to return the user object from an API endpoint. In fact, suppose I want to return user objects from multiple API endpoints. Possibly as a standalone object, possibly as a related model.
Obviously, I don't want password to be present in the returned structure - and in many cases I wouldn't want token to be returned either. I could do this manually on every endpoint, but I'd prefer a no-thought solution - being able to simply retrieve the user, end of story, and not worry about unsetting certain values after the fact.
I mainly come from the world of Laravel, where things like API Resources (https://laravel.com/docs/5.6/eloquent-resources) exist. I already tried implementing the mongoose-hidden package (https://www.npmjs.com/package/mongoose-hidden) to hide the password and token, but unfortunately it seems as though that breaks the encryption package I'm using.
I'm new to Nodejs and MongoDB in general - is there a good way to implement this?
How to protect the password field in Mongoose/MongoDB so it won't return in a query when I populate collections?
You can use this: Users.find().select("-password"),
but this is done whenever you send the queried item to the user (res.json()...) so you can do your manipultions with this field included and then remove it from the user before you send it back (this is using the promise approach, the best practice).
And if you want your changes to be used as default you can add "select: false" into the schema object's password field.
Hope this helps :)
I'm looking for a way to send a custom error message to the client when a required input is not set.
A sample Mongoose Schema:
this._schema = new mongoose.Schema({
email: {
type: String,
unique: true,
required: true
}
});
I use a this._schema.pre('validate') function to test if a required input is missing and, if it is missing, pass a custom AppError to my next() function which will handle the rest.
The pre-validation:
this._schema.pre('validate', function(next) {
if(!this.email) return next(new AppError('Email not set'));
next();
});
I don't want to check for each field manually using a list of if statements. Rather I would like to loop over all required fields and throw the error if one of them is missing (loop should be recursive too for nested fields).
Question:
How can I loop over required fields and pass the first missing field to an Error?
Note: I'm not sure if this is the right way, so other interpretations are welcome! Found something here, but I'm not sure that's the correct answer.
I am using Loopback and the push component. When calling Notification.create() I get the error:
Cannot call Notification.create(). The create method has not been setup.
The PersistedModel has not been correctly attached to a DataSource!
I'm just running the basic example server 2.0. In code I am trying to create a Notification. What's the problem?
I too got the same problem when trying to use login function of User model.
Got it fixed after an hour of hit and trial.
Answer: I extended User model to MyUser model (No coding inside this model, just used it as a wrapper) and inside Hotel.js (in my case a business class where i use to authenticate user before accessing hotel details) created a remoteMethod for login
code:
Hotel.auth=function(uname,pwd, cb)
{
Hotel.app.models.MyUser.login({username: uname, password: pwd}, function (err, token) {
if(err)
cb(null,err);
else
cb(null,token);
});
}
Hotel.remoteMethod(
'auth',
{
accepts:
[
{arg: 'uname', type: 'string',required: true},
{arg: 'pwd', type: 'string',required: true}
],
returns: {arg: 'Response Message', type: 'string'}
}
);
This works!
This one is pretty old, but just to put something up here. Without seeing your setup my guess is that the model you are using is not connected to any data source, or one that is not written properly. The default connector is in-memory and does implement this method correctly. Check your server/model-config.json file and find the entry for Notification and check what you have for the data source.
I have a basic Mongoose model with a Meeting and Participants array:
var MeetingSchema = new Schema({
description: {
type: String
},
maxNumberOfParticipants: {
type: Number
},
participants: [ {
type: Schema.ObjectId,
ref: 'User'
} ]
});
Let's say I want to validate that the number of participants added doesn't exceed the maxNumberOfParticipants for that meeting.
I've thought through a few options:
Custom Validator - which I can't do because I have to validate one attribute (participants length) against another (maxNumberOfParticipants).
Middleware - i.e., pre-save. I can't do this either because my addition of participants occurs via a findOneAndUpdate (and these don't get called unless I use save).
Add validation as part of my addParticipants method. This seems reasonable, but I'm not sure how to pass back a validation error from the model.
Note that I don't want to implement the validation in the controller (express, MEAN.js stack) because I'd like to keep all logic and validations on the model.
Here is my addParticipants method:
MeetingSchema.methods.addParticipant = function addParticipant(params, callback) {
var Meeting = mongoose.model('Meeting');
if (this.participants.length == this.maxNumberOfParticipants) {
// since we already have the max length then don't add one more
return ????
}
return Meeting.findOneAndUpdate({ _id: this.id },
{ $addToSet: { participants: params.id } },
{new: true})
.populate('participants', 'displayName')
.exec(callback);
};
Not sure how to return a validation error in this case or even if this pattern is a recommended approach.
I wouldn't think that's it's common practice for this to be done at the mongoose schema level. Typically you will have something in between the function getting called and the database layer (your schema) that performs some kind of validation (such as checking max count). You would want your database layer to be in charge of just doing simple/basic data manipulation that way you don't have to worry about any extra dependencies when/if anything else calls it. This may mean you'd need to go with route 1 that you suggested, yes you would need to perform a database request to find out what your current number of participants but I think it the long run it will help you :)