Sails bundles support for automatic validations of your models' attributes. Any time a record is updated, or a new record is created, the data for each attribute will be checked against all of your predefined validation rules. This provides a convenient failsafe to ensure that invalid entries don't make their way into your app's database(s).
Except for unique
(which is implemented as a database-level constraint; see "Unique"), all validations below are implemented in JavaScript and run in the same Node.js server process as Sails. Also keep in mind that, no matter what validations are used, an attribute must always specify one of the built-in data types (string
, number
, json
, etc).
// User
module.exports = {
attributes: {
emailAddress: {
type: 'string',
unique: true,
required: true
}
}
};
In Sails/Waterline, model attributes always have some kind of data type guarantee. This is above and beyond any physical-layer constraints which might exist in your underlying database—it's more about providing a way for developers to maintain reasonable assumptions about the data that goes in or comes out of a particular model.
This data type guarantee is used for logical validation and coercion of results and criteria. Here is a list of the data types supported by Sails and Waterline:
Data Type | Usage | Description |
---|---|---|
type: 'string' |
Any string. | |
type: 'number' |
Any number. | |
type: 'boolean' |
true or false . |
|
type: 'json' |
Any JSON-serializable value, including numbers, booleans, strings, arrays, dictionaries (plain JavaScript objects), and null . |
|
type: 'ref' |
Any JavaScript value except undefined . (Should only be used when taking advantage of adapter-specific behavior.) |
Sails' ORM (Waterline) and its adapters perform loose validation to ensure that the values provided in criteria dictionaries and as values to .create()
or .update()
match the expected data type.
NOTE: In adapters that don't support the JSON.stringify()
called on it and then is stored in a column with a type set to text
. Each time the record is returned, the data has JSON.parse()
called on it. This is something to be aware of when considering performance and compatibility with other applications or existing data in the database. The official PostgreSQL and mongoDB adapters can read and write
The string
, number
and boolean
data types do not accept null
as a value when creating or updating records. In order to allow a null
value to be set, toggle the allowNull
flag on the attribute. The allowNull
flag is only valid on the above data types; it is not valid on attributes with types json
or ref
, any associations, or any primary key attributes.
Since empty string ("") is a string, it is normally supported by type: 'string'
attributes; but there are a couple of exceptions: primary keys (because primary keys never support empty string) and any attribute which has required: true
.
If an attribute is required: true
, then a value must always be specified for it when calling .create()
. This also prevents a value from being set to null
or empty string ("") when created or updated.
None of the following validation rules impose any additional restrictions against null
. That is, if null
would be allowed normally, then enabling the isEmail
validation rule will not cause null
to be rejected as invalid.
Similarly, most of the following validation rules don't impose any additional restrictions against empty string (""). There are a few exceptions (isNotEmptyString
and non-string-related rules like isBoolean
, isNumber
, max
, and min
), but otherwise, for any attribute where empty string ("") would normally be allowed, adding a validation rule will not cause it to be rejected.
In the table below, the "Compatible Attribute Type(s)" column shows what data type(s) (i.e. for the attribute definition's type
property) are appropriate for each validation rule. In many cases, a validation rule can be used with more than one type. Note that the table below takes a shortcut: if compatible with
Name of Rule | What It Checks For | Notes On Usage | Compatible Attribute Type(s) |
---|---|---|---|
custom | A value such that when it is provided as the first argument to the custom function, the function returns true . |
Example | Any |
isAfter | A value that, when parsed as a date, refers to a moment after the configured JavaScript Date instance. |
isAfter: new Date('Sat Nov 05 1605 00:00:00 GMT-0000') |
|
isBefore | A value that, when parsed as a date, refers to a moment before the configured JavaScript Date instance. |
isBefore: new Date('Sat Nov 05 1605 00:00:00 GMT-0000') |
|
isBoolean | A value that is true or false |
isBoolean: true | |
isCreditCard | A value that is a credit card number. | Do not store credit card numbers in your database unless your app is PCI compliant! If you want to allow users to store credit card information, a safe alternative is to use a payment API like Stripe. | |
isEmail | A value that looks like an email address. | isEmail: true |
|
isHexColor | A string that is a hexadecimal color. | isHexColor: true |
|
isIn | A value that is in the specified array of allowed strings. | isIn: ['paid', 'delinquent'] |
|
isInteger | A number that is an integer (a whole number) | isInteger: true |
|
isIP | A value that is a valid IP address (v4 or v6) | isIP: true |
|
isNotEmptyString | A value that is not an empty string | isNotEmptyString: true |
|
isNotIn | A value that is not in the configured array. | isNotIn: ['profanity1', 'profanity2'] |
|
isNumber | A value that is a Javascript number | isNumber: true |
|
isString | A value that is a string (i.e. typeof(value) === 'string' ) |
isString: true |
|
isURL | A value that looks like a URL. | isURL: true |
|
isUUID | A value that looks like a UUID (v3, v4 or v5) | isUUID: true |
|
max | A number that is less than or equal to the configured number. | max: 10000 |
|
min | A number that is greater than or equal to the configured number. | min: 0 |
|
maxLength | A string that has no more than the configured number of characters. | maxLength: 144 |
|
minLength | A string that has at least the configured number of characters. | minLength: 8 |
|
regex | A string that matches the configured regular expression. | regex: /^[a-z0-9]$/i |
Imagine that you have an attribute defined as follows:
workEmail: {
type: 'string',
isEmail: true,
}
When you call .create()
_or_ .update()
, this value can be set to any valid email address (like "[email protected]") OR to an empty string (""). You would not be able to set it to null
, though, because that would violate the type safety restriction imposed by type: 'string'
.
To make this attribute accept
null
(e.g. if you are working with a pre-existing database), change it totype: 'json'
. You'd normally also want to addisString: true
, but since we already enforceisEmail: true
in this example, there's no need to do so.A more advanced feature to keep in mind is that, depending on your database, you can choose to take advantage of
columnType
to inform Sails / Waterline which column type to define during auto-migrations (if relevant).
If we want to indicate that an attribute supports certain numbers, like a star rating, we might do something like this:
starRating: {
type: 'number',
min: 1,
max: 5,
required: true,
}
If we want to make our star rating optional, it's easiest to just remove the required: true
flag. If omitted, the starRating will default to zero.
null
)But what if the star rating can't always be a number? Imagine we need to integrate with a legacy database in which star ratings could be either a number or the special null literal. In this scenario, we would like to define the starRating
attribute to support both certain numbers and null
.
To accomplish this, just use allowNull
:
starRating: {
type: 'number',
allowNull: true,
min: 1,
max: 5,
}
Sails and Waterline attributes support
allowNull
for convenience, but another viable solution is to changestarRating
fromtype: 'number'
totype: 'json'
. Remember, though, that thejson
type allows other data, like booleans, arrays, etc. If we want to explicitly protect against those data types being supported bystarRating
, we could add theisNumber: true
validation rule:starRating: { type: 'json', isNumber: true, min: 1, max: 5, }
unique
is different from all of the validation rules listed above. In fact, it isn't really a validation rule at all: it is a database-level constraint. More on that in a second.
If an attribute declares itself unique: true
, then Sails ensures that no two records will be allowed with the same value. The canonical example is an emailAddress
attribute on a User
model:
// api/models/User.js
module.exports = {
attributes: {
emailAddress: {
type: 'string',
unique: true,
required: true
}
}
};
unique
different from other validations?Imagine you have 1,000,000 user records in your database. If unique
was implemented like other validations, every time a new user signed up for your app, Sails would need to search through one million existing records to ensure that no one else was already using the email address provided by the new user. That would be so slow that by the time we finished searching through all those records, someone else could have signed up!
Fortunately, this type of uniqueness check is perhaps the most universal feature of any database. To take advantage of that, Sails relies on the database adapter to implement support for unique
—specifically, it adds a uniqueness constraint to the relevant field/column/attribute in the database itself during auto-migration. That is, while your app is set to migrate:'alter'
, Sails will automatically generate tables/collections in the underlying database with uniqueness constraints built right in. Once you switch to migrate:'safe'
, updating your database constraints is up to you.
When you start using your production database, it is always a good idea to set up indexes to boost your database's performance. The exact process and best practices for setting up indexes varies between databases and is beyond the scope of this documentation. That said, if you've never done this before, don't worry: it's easier than you think.
Just like everything else related to your production schema, once you set your app to use migrate: 'safe'
, Sails leaves database indexes entirely up to you.
Note that this means you should be sure to update your indexes alongside your uniqueness constraints when performing manual migrations.
Validations can save you from writing many hundreds of lines of repetitive code, but keep in mind that model validations are run for every create or update in your application. Before using a validation rule in one of your attribute definitions, make sure you are okay with it being applied every time your application calls .create()
or .update()
to specify a new value for that attribute. If that is not the case, write code that validates the incoming values inline in your controller, or call a custom function in one of your services or a model class method.
Suppose that your Sails app allows users to sign up for an account by either (A) entering an email address and password and then confirming that email address or (B) signing up with LinkedIn. Your User
model might have one attribute called manuallyEnteredEmail
and another called linkedInEmail
. While one of those email address attributes is required, which one that is depends on how a user signs up. In this case, your User
model cannot use the required: true
validation. In order to confirm that one of the two emails has been provided—and that the provided email is valid—you'll instead have to manually check these values before the relevant .create()
and .update()
calls in your code:
if ( !_.isString( req.param('email') ) ) {
return res.badRequest();
}
Taking this one step further, let's say your application accepts payments. During the sign-up flow, if the user signs up with a paid plan, they must provide an email address for billing purposes (billingEmail
), while if the user signs up with a free account, they skip that step. On the account settings page, users on the paid plan see a "Billing Email" form field where they can customize their billing email. Users with the free plan, on the other hand, see a call to action which links to the "Upgrade Plan" page.
While these requirements seem specific, there are still unanswered questions:
linkedInEmail
is saved?linkedInEmail
?Depending on the answers to questions like these, we might end up keeping the required
validation on billingEmail
, adding new attributes (like hasBillingEmailBeenChangedManually
), or even rethinking whether we use a unique
constraint.
Finally, here are a few tips:
.update()
and .create()
. Don't be afraid to forgo built-in validation support in favor of checking values by hand in your controllers or in a helper function. Oftentimes this is the cleanest and most maintainable approach.unique
. During development, when your app is configured to use migrate: 'alter'
, you can add or remove unique
validations at will. However, if you are using migrate: safe
(e.g. with your production database), you will want to update constraints/indices in your database, as well as migrate your data by hand.As much as possible, it is best to obtain or flesh out your own wireframes of your app's user interface before you spend any serious amount of time implementing any backend code. Of course, this isn't always possible, and that's what the blueprint API is for. Applications built with a UI-centric, or "front-end first", philosophy are easier to maintain, tend to have fewer bugs, and—since mindfulness of the user experience is at their core—often have more elegant APIs.
You can define your own custom validation rules by specifying a custom
function in your attributes.
// api/models/User.js
module.exports = {
// Values passed for creates or updates of the User model must obey the following rules:
attributes: {
firstName: {
// Note that a base type (in this case "string") still has to be defined, even though validation rules are in use.
type: 'string',
required: true,
minLength: 5,
maxLength: 15
},
location: {
type: 'json',
custom: function(value) {
return _.isObject(value) &&
_.isNumber(value.x) && _.isNumber(value.y) &&
value.x !== Infinity && value.x !== -Infinity &&
value.y !== Infinity && value.y !== -Infinity;
}
},
password: {
type: 'string',
custom: function(value) {
// • be a string
// • be at least 6 characters long
// • contain at least one number
// • contain at least one letter
return _.isString(value) && value.length >= 6 && value.match(/[a-z]/i) && value.match(/[0-9]/);
}
}
}
}
Custom validation functions receive the incoming value to be validated as their first argument. They are expected to return true
if valid and false
otherwise.
Out of the box, Sails.js does not support custom validation messages. Instead, your code should look at (or "negotiate") validation errors thrown by .create()
or .update()
calls and take the appropriate action, whether that's sending a particular error code in your JSON response or rendering the appropriate message in an HTML error page.