Note: You are viewing the Sails.js v0.9.x documentation. If you're looking for information on v0.8.x, please visit here.
Like most MVC frameworks, Sails provides an ORM (Object Relational Mapping) called Waterline for normalizing interactions with models, no matter what data source you're using. It also defines an interface for mapping your own custom models from external APIs, not-yet-supported databases, or in-memory state (i.e. Session storage.)
A model is a persistent data type: a representation of data stored in a database. If you're using MySQL, a model might correspond to a table. If you're using MongoDB, it might correspond to a collection. In either case, our goal is to provide a simple, modular way of managing data without relying on any one type of database.
Model definitions contain attributes, validations, instance methods, lifecycle callbacks and class methods.
Attributes are basic pieces of information about a model. For instance, a model called Person
might have attribute called firstName
, lastName
, phoneNumber
, age
, birthDate
and emailAddress
.
The model definition for Person
might look like this:
// Person.js
var Person = {
attributes: {
firstName: 'STRING',
lastName: 'STRING',
age: {
type: 'INTEGER',
max: 150,
required: true
},
birthDate: 'DATE',
phoneNumber: {
type: 'STRING',
defaultsTo: '111-222-3333'
},
emailAddress: {
type: 'email', // Email type will get validated by the ORM
required: true
}
}
};
module.exports = Person;
The following attribute types are currently available:
- string
- text
- integer
- float
- date
- time
- datetime
- boolean
- binary
- array
- json
To learn more about what methods are available to you, check out the waterline documentation.
Validations are defined on you attributes when defining a model. It uses Anchor behind the scenes to run validations whenever you create or update a record.
attributes: {
name: {
type: 'string',
maxLength: 20,
minLength: 5
},
email: {
type: 'email',
required: true
}
}
Available validations are:
- empty
- required
- notEmpty
- undefined
- string
- alpha
- numeric
- alphanumeric
- url
- urlish
- ip
- ipv4
- ipv6
- creditcard
- uuid
- uuidv3
- uuidv4
- int
- integer
- number
- finite
- decimal
- float
- falsey
- truthy
- null
- notNull
- boolean
- array
- date
- hexadecimal
- hexColor
- lowercase
- uppercase
- after
- before
- is
- regex
- not
- notRegex
- equals
- contains
- notContains
- len
- in
- notIn
- max
- min
- minLength
- maxLength
You can attach instance methods to a model which will be available on any record returned from a query. There are also a few helper instance methods that get attached to allow you to perform operations on a model instance without having to build the lookup criteria.
Helper Instance Methods:
- save
- destroy
- toObject
- toJSON
The save instance method will write the current values of the model instance to the datastore. It only takes a callback as it's argument.
// Lookup a user
User.findOne(1).done(function(err, user) {
// we now have a model with instance methods attached
// update an attribute value
user.email = 'foo.bar@gmail.com';
// save the updated value
user.save(function(err) {
// value has been saved
});
});
The destroy instance method allows you to delete a single record from the datastore without having to build up a criteria search.
// Lookup a user
User.findOne(1).done(function(err, user) {
// we now have a model with instance methods attached
// destroy the record
user.destroy(function(err) {
// record has been removed
});
});
The toObject()
method will return the currently set model values only, without any of the instance
methods attached. Useful if you want to change or remove values before sending to the client.
However we provide an even easier way to filter values before returning to the client when using
the blueprints by allowing you to override the toJSON()
method in your model. This function will
automatically be called when using the blueprints so you don't need to write custom controllers in
order to filter values.
Example of filtering a password in your model definition:
module.exports = {
attributes: {
name: 'string',
password: 'string',
// Override toJSON instance method
// to remove password value
toJSON: function() {
var obj = this.toObject();
delete obj.password;
return obj;
}
}
}
// Then later when you query it:
User.findOne(1).done(function(err, user) {
// user.password doesn't exist
});
You may define custom instance methods that are available after querying a model. These are defined as functions in your model attributes.
module.exports = {
attributes: {
firstName: 'string',
lastName: 'string',
// Define a custom instance method
fullName: function() {
return this.firstName + ' ' + this.lastName;
}
}
}
// Then use it in your query results like:
User.findOne(1).done(function(err, user) {
// use the instance method
var name = user.fullName();
});
Lifecycle callbacks are functions you can define to run at certain times in a query. They are hooks that you can tap into in order to change data. An example use case would be automatically encrypting a password before creating or automatically generating a slugified url attribute.
Callbacks run on Create:
- beforeValidation / *fn(values, cb)*
- beforeCreate / *fn(values, cb)*
- afterCreate / *fn(newlyInsertedRecord, cb)*
Callbacks run on Update:
- beforeValidation / *fn(valuesToUpdate, cb)*
- beforeUpdate / *fn(valuesToUpdate, cb)*
- afterUpdate / *fn(updatedRecord, cb)*
Callbacks run on Destroy:
- beforeDestroy / *fn(criteria, cb)*
- afterDestroy / *fn(cb)*
Examples
Encrypt a password before saving to the database.
var bcrypt = require('bcrypt');
module.exports = {
attributes: {
username: {
type: 'string',
required: true
},
password: {
type: 'string',
minLength: 6,
required: true,
columnName: 'encrypted_password'
}
},
// Lifecycle Callbacks
beforeCreate: function(values, next) {
bcrypt.hash(values.password, 10, function(err, hash) {
if(err) return next(err);
values.password = hash;
next();
});
}
};
You can define a custom table name on your adapter by adding a tableName
attribute. If no table
name is supplied it will use the filename as the table name when passing it to an adapter. So if
your model's name is UserModel, the table name will be set to "user" by default.
// api/models/User.js
module.exports = {
tableName: 'sails_user',
attributes: {
// attributes here
}
};
Adapters can be included from npm, or defined locally in the api/adapters
directory of your
project.
You can override the adapter globally for your application, or you can configure different models to point to different adapters. To see how to change your default application adapter config, check out the Configuration section of this documentation at Configuration
To override the adapter of a single model, you specify the adapter module's name and any extra configuration information necessary to make it work.
For example:
// api/models/User.js
module.exports = {
adapter: 'mysql',
config: {
user: 'root',
password: 'thePassword',
database: 'testdb',
host: '127.0.0.1'
},
attributes: {
name: 'string',
email: 'string',
phoneNumber: {
type: 'string',
defaultsTo: '555-555-5555'
}
}
};
Our global is set to disk
, however since we overrode the adapter our User models will now be
stored in MySQL using the sails-mysql adapter.
For adapters that don't require a schema such as Mongo or Redis the default setting is to be schemaless. This means that you don't need to specify attributes on your model in order to persist them. For some cases this may be fine but in other cases you would like to specify that all data sticks to a schema.
You can toggle the schema
flag on models on or off. However note that if you are using a schema database such as MySQL
or PostgreSQL you will not be able to use the model if schema
is set to false.
module.exports = {
adapter: 'mongo',
schema: true,
attributes: {
// some attributes
}
};
You may also set the schema
flag globally in your config/adapters.js
for a datastore. This will enable/disable a schema
on all your models.
module.exports.adapters = {
mongo: {
module: 'sails-mongo',
schema: true
}
};
Associations are not yet available for Sails.JS, however are on the immediate todo list. Please check out issue #124 for more information regarding the proposed changes for associations.
Models are defined in the api/models/ directory in your Sails application.
You can generate a model with the command line tool:
sails generate model Person
Depending on your configuration, the database tables will be recreated automatically.
You'll want to create, access, modify, and destroy models from controllers, views, services, and policies, and so you'll need a way to deal with them.
Queries can be run with either a callback interface or with a deferred object. For building complicated queries the deferred object method is the best choice.
User.findOne({ id: 1 }, function(err, user) {
// Do stuff here
});
User.find()
.where({ id: { '>': 100 }})
.where({ age: 21 })
.limit(100)
.sort('name')
.exec(function(err, users) {
// Do stuff here
});
To create a new record in the database, use create()
.
// For example
User.create({
name: 'Mike',
age: 13,
phoneNumber: '(512)-555-5555'
}).done(function(err, user) {
// Error handling
if (err) {
return console.log(err);
// The User was created successfully!
}else {
console.log("User created:", user);
}
});
To lookup a model by id, use findOne(id)
. You can call also look for a model by passing in an
object composed of the desired matching criteria.
// For example to find by id
User.findOne(123).done(function(err, user) {
// Error handling
if (err) {
return console.log(err);
// The User was found successfully!
} else {
console.log("User found:", user);
}
});
// To find by a criteria
User.findOne({
name: 'Steven',
age: 32,
phone:'(210)-555-1234'
}).done(function(err, user) {
// Error handling
if (err) {
return console.log(err);
// The User was found successfully!
} else {
console.log("User found:", user);
}
});
find()
lets you search for one or more models which meet the criteria you specify. You can also
include a limit
(max number of models to return), skip
(useful for pagination), and sort
sort
. Find all will always return an array even if only one model fits the criteria.
// For example, this query returns the first ten 18 year olds, sorted alphabetically
User.find({
age: 18
}).limit(10).sort('name ASC').done(function(err, users) {
// Error handling
if (err) {
return console.log(err);
// Found multiple users!
} else {
console.log("Users found:", users);
}
});
Below are some more examples. Some of these examples include query modifiers. You can view more about query modifiers here.
// Search-as-you-type input field
User.find({
name: {
startsWith: 'thelas'
}
}, cb);
// Search-as-you-type input field which checks multiple attributes
User.find({
or: [
name: { startsWith: 'thelas' },
email: { startsWith: 'thelas' }
]
}, cb);
// Keyword search
User.find({
description: {
contains: 'roller coaster'
}
}, cb);
// Alphabetical search
User.find({
name: {
'>=': 'a'
}
}, cb);
// you can also do <=, <, >, and ! See query modifiers
// Alphabetical search.. but paginated:
// (here's page 2)
User.find({
where: {
name: {
'>=': 'a'
}
},
limit: 15,
skip: 15,
sort: 'name ASC'
}, cb);
With Sails built in ORM, Waterline, you can use a very helpful tool called dynamic finders. You can query your models with automatically genereated methods that depend on the attributes you define for the model. For example, if you had a book model that looks like this.
var Book = {
title: 'STRING',
author: 'STRING',
publisher: 'STRING',
}
module.exports = Book;
You can query the db using methods such as these
// Query by author
Book.findOneByTitle('50 Shades of Grey').done(function(err, book) {
// Error handling
if (err) {
return console.log(err);
// The Book was found successfully!
} else {
console.log("Book found:", book);
}
});
// Query by Author
Book.findByAuthor('John R. Erickson').done(function(err, books) {
// Error handling
if (err) {
return console.log(err);
// The Books were found successfully!
} else {
console.log("Books found:", books);
}
});
update()
allows you to update an instance of a model from the database. It will always return
an array of records that have been updated.
// For example, to update a user's name,
// .update(query, params to change, callback)
User.update({
name: 'sally'
},{
phone: '555-555-5555'
}, function(err, users) {
// Error handling
if (err) {
return console.log(err);
// Updated users successfully!
} else {
console.log("Users updated:", users);
}
});
destroy()
allows you to delete models from the database. It will work on all matching criteria.
// For example, to delete a user named Johnny,
User.destroy({
name: 'Johnny',
age: 22
}).done(function(err) {
// Error handling
if (err) {
return console.log(err);
// Johnny was deleted!
} else {
console.log("User deleted");
}
});
Modifiers can be used in your database queries. These make it easier to get information from your database without having to write a bunch of code. Currently supported modifiers are contains, or, startsWith, endsWith, greaterThan, lessThan, >=, and <=. Each of these are shown in examples below.
In order to use a contains
modifier, you would do the following.
where: {
name: {
contains: 'James'
}
}
In order to use an or
modifier, you would do the following.
where: {
or: [{name: 'James'}, {name: 'Mike'}]
}
In schemaful databases (like MySQL) schema migrations occur automatically. Models default to
migrate:alter
, which tells Sails to attempt to auto-migrate the schema.
Explicit production data migrations, like in Rails, do not exist at this time-- production data is precious, and manual migrations can be dangerous. However, if this is a feature that you are interested in, please submit an issue, or better yet, a pull-request!
For more information on using Models you can visit the Waterline documentation which goes more in depth on how the internals work.