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
}
}
};
Every attribute definition must have a built-in data type (or typeclass) specified. This is used for logical validation and coercion of results and criteria.
Data Type | Usage | Description |
---|---|---|
type: 'string' |
Any string (tolerates null ). |
|
type: 'number' |
Any number (tolerates null ) |
|
type: 'boolean' |
true or false (also tolerates null ) |
|
type: 'json' |
Any JSON-serializable value, including numbers, booleans, strings, arrays, dictionaries, and null . |
|
type: 'array' |
Any array consisting solelyof JSON-serializable contents. |
Different databases vary slightly in the way they handle edge cases and special values such as Infinity
, null
, strings of varying lengths, etc. 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 typeclass.
Note that auto-migration also relies on the attribute's declared
type
. This is mainly relevant for schemaful databases (like MySQL or PostgreSQL), since the relevant adapter needs to use this information in order to alter/define tables during auto-migration. Remember that in production,migrate: 'safe'
will be enabled and auto-migration will be skipped.
The following validation rules are handled by Anchor, a robust validation library for Node.js.
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 coincidentally, the table below takes a shortcut: If compatible with
Name of Rule | What It Checks For | Notes On Usage | Compatible Attribute Type(s) |
---|---|---|---|
after | A value that, when parsed as a date, refers to moment after the configured JavaScript Date instance. |
after: new Date('Sat Nov 05 1605 00:00:00 GMT-0000') |
|
alpha | A value that contains only uppercase and/or lowercase letters. | alpha: true |
|
alphadashed | A value that contains only letters and dashes. | ||
alphanumeric | A value that contains only letters and numbers. | ||
alphanumericdashed | A value that is a string consisting of only letters, numbers, and/or dashes. | ||
before | A value that, when parsed as a date, refers to a moment before the configured JavaScript Date instance. |
before: new Date('Sat Nov 05 1605 00:00:00 GMT-0000') |
|
contains | A value that contains the specified substring. | contains: 'needle' |
|
creditcard | 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. | |
datetime | A value that can be parsed as a timestamp; i.e. would construct a JavaScript Date with new Date() |
||
decimal | Alias for float . |
||
A value that looks like an email address. | |||
finite | A value that is, or can be coerced to, a finite number. | This is not the same as native isFinite which will return true for booleans and empty strings | |
float | A value that is, or can be coerced to, a floating point (aka decimal) number. | ||
hexadecimal | A value that is a hexadecimal number. | ||
hexColor | A value that is a hexadecimal color. | ||
in | A value that is in the specified array of allowed strings. | ||
int | Alias for integer . |
||
integer | A value that is an integer, or a string that looks like one. | ||
ip | A value that is a valid IP address (v4 or v6) | ||
ipv4 | A value that is a valid IP v4 address. | ||
ipv6 | A value that is a valid IP v6 address. | ||
is | Alias for regex . |
||
lowercase | A value that consists only of lowercase characters. | ||
max | A value that is less than the configured number. | ||
maxLength | A value that has no more than the configured number of characters. | ||
min | A value that is greater than the configured number. | ||
minLength | A value that has at least the configured number of characters. | ||
notRegex | A value that does not match the configured regular expression. | ||
notContains | A value that does not contain the configured substring. | e.g. '-haystack-needle-haystack-' would fail validation against notContains: 'needle' |
|
notIn | A value that is not in the configured array. | ||
notNull | A value that is not equal to null |
||
numeric | A value that is a string which is parseable as a number. | Note that while NaN is considered a number in JavaScript, that is not true for the purposes of this validation. |
|
required | A value that is defined; that is, not undefined . |
||
regex | A value that matches the configured regular expression. | ||
truthy | A value that would be considered truthy if used in a JavaScript if statement. |
||
uppercase | A value that is uppercase. | ||
url | A value that is a URL. | ||
urlish | A value that looks vaguely like a URL of some kind (i.e. /^\s([^\/]+\.)+.+\s*$/g ). |
urlish: true |
|
uuid | A value that is a UUID (v3, v4, or v5) | ||
uuidv3 | A value that is a UUID (v3) | ||
uuidv4 | A value that is a UUID (v4) |
unique
is different from all of the validation rules listed above. In fact, it isn't really a validation at all: it is a database-level constraint. More on that in a second.
If an attribute declares itself unique: true
, then Sails ensures 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. Not only would that be slow, but 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 the unique
validation-- specifically, by adding 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 out of the scope of the documentation here. That said if you've never done this before, don't worry-- it's easier than you might 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 be a huge time-saver, preventing 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 OK 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 out to a custom function in one of your services, or a model class method.
For example, let's say 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. Now let's say your User
model has one attribute called linkedInEmail
and another attribute called manuallyEnteredEmail
. Even though one of those email address attributes is required, which one is required depends on how a user signed up. So in that case, your User
model cannot use the required: true
validation-- instead you'll need to validate that one email or the other was provided and is valid by manually checking these values before the relevant .create()
and .update()
calls in your code, e.g.:
if ( !_.isString( req.param('email') ) ) {
return res.badRequest();
}
To take this one step further, now let's say your application accepts payments. During the sign up flow, if a user signs up with a paid plan, he or she must also provide an email address for billing purposes (billingEmail
). If a user signs up with a free account, he or she skips that step. On the account settings page, users on a paid plan do see a "Billing Email" form field where they can customize their billing address. This is different from users on the free plan, who see a call to action which links to the "Upgrade Plan" page.
Even with these requirements, which seem quite specific, there are unanswered questions:
linkedInEmail
is saved?linkedInEmail
is saved?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 changing whether or not to use a unique
constraint.
Finally, here are a few tips:
.update()
and .create()
. Don't be afraid to forgo built-in validation support and check 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 a good idea 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 they are built with full knowledge of the user interface from the get-go, they often have more elegant APIs.
Warning: Support for custom validation rules as documented here will very likely be ending in Waterline 1.0. To future-proof your app, use a function from one of your services or a model class method for custom validation instead.
You can define your own custom validation rules by specifying a types
dictionary as a top level property of your model, then use them in your attribute definitions just like you could any other validation rule above:
// 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',
isPoint: true // << defined below
},
password: {
type: 'string',
password: true // << defined below
}
},
// Custom types / validation rules
// (available for use in this model's attribute definitions above)
types: {
isPoint: function(value){
// For all creates/updates of `User` records that specify a value for an attribute
// which declares itself `isPoint: true`, that value must:
// • be a dictionary with numeric `x` and `y` properties
// • both `x` and `y` must be neither `Infinity` nor `-Infinity`
return _.isObject(value) &&
_.isNumber(value.x) && _.isNumber(value.y) &&
value.x !== Infinity && value.x !== -Infinity &&
value.y !== Infinity && value.y !== -Infinity;
},
password: function(value) {
// For all creates/updates of `User` records that specify a value for an attribute
// which declares itself `type: 'password'`, that value must:
// • 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 being validated as their first argument, and are expected to return true
if it is valid, false
otherwise. Once set up, these custom validation rules can be used in one or more attributes in the model where they are defined by setting an extra property with the same name in relevant attribute definitions; e.g. someCustomValidationRuleOrType: true
.
Note that custom validation rules are not namespaced from built-in validations and types-- they are all merged together. So be careful not to define a custom validation that collides with any of the base types or validations in Waterline (e.g. don't name your custom validation rule string
or minLength
).
Out of the box, Sails.js does not support custom validation messages. Instead your code should look at validation errors in the callback from your 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.
If you are using Sails v0.11.0+, you may want to take advantage of
sails-hook-validation
, a custom hook by @lykmapipo. Details regarding its installation and usage can be found in thesails-hook-validation
repository on GitHub.