This is Shyp's fork of Waterline. Currently this differs from upstream Waterline in the following ways:
- Postgres is the only supported backend
- count() is removed
- findOrCreate() is removed
- dynamic finders are removed
- many to many is not supported
- hasManyThrough is not supported
'alter'
is no longer an option (write SQL separately from Waterline).
Install from NPM.
npm install waterline@git://github.com/Shyp/waterline.git
Waterline was extracted from the Sails framework and is the default ORM used in Sails. For more information on using Waterline in your Sails App view the Sails Docs.
For examples of how to use with frameworks such as Express look in the Example folder.
var Waterline = require('waterline');
// Define your collection (aka model)
var User = Waterline.Collection.extend({
attributes: {
firstName: {
type: 'string',
required: true
},
lastName: {
type: 'string',
required: true,
}
}
});
The only supported adapter is Postgres, all others can and will probably break.
A Collection is the main object used in Waterline. It defines the layout/schema of your data along with any validations and instance methods you create.
To create a new collection you extend Waterline.Collection
and add in any properties you need.
Available options are:
tableName
Define a custom table name to store the modelsadapter
the name of the adapter you would like to use for this collectionschema
Set schema true/false to only allow fields defined inattributes
to be saved. Only for schemaless adapters.attributes
A hash of attributes to be defined for a modelautoCreatedAt
andautoUpdatedAt
Set false to prevent creatingcreatedAt
andupdatedAt
properties in your modelautoPK
Set false to prevent creatingid
. By defaultid
will be created as index with auto increment- lifecyle callbacks
- any other class method you define!
The following attribute types are currently available:
- string
- text
- integer
- float
- date
- time
- datetime
- boolean
- binary
- array. NOTE this will actually create a TEXT and serialize it with JSON.stringify. Strongly consider deserializing your data.
- json
var User = Waterline.Collection.extend({
// Define a custom table name
tableName: 'user',
// Set schema true/false for adapters that support schemaless
schema: true,
// Define an adapter to use
adapter: 'postgresql',
// Define attributes for this collection
attributes: {
firstName: {
type: 'string',
// also accepts any validations
required: true
},
lastName: {
type: 'string',
required: true,
maxLength: 20
},
email: {
// Special types are allowed, they are used in validations and
// set as a string when passed to an adapter
type: 'email',
required: true
},
age: {
type: 'integer',
min: 18
},
// You can also define instance methods here
fullName: function() {
return this.firstName + ' ' + this.lastName
}
},
/**
* Lifecycle Callbacks
*
* Run before and after various stages:
*
* beforeValidate
* beforeUpdate
* afterUpdate
* beforeCreate
* beforeDestroy
* afterDestroy
*/
beforeCreate: function(values, cb) {
// An example encrypt function defined somewhere
encrypt(values.password, function(err, password) {
if(err) return cb(err);
values.password = password;
cb();
});
},
// Class Method
doSomething: function() {
// Do something here
}
});
Now that a collection is defined we can instantiate it and begin executing queries against it. All Collections take options
and callback
arguments.
Options will be made up of:
tableName
, used if not defined in a Collection definitionadapters
object that specifies each adapter, either custom definitions or from NPM
var postgres = require('sails-postgresql');
new User({ tableName: 'foobar', adapters: { postgresql: postgres }}, function(err, Model) {
// We now have an instantiated collection to execute queries against
Model.find()
.where({ age: 21 })
.limit(10)
.exec(function(err, users) {
// Now we have an array of users
});
});
Each result that gets returned from a Waterline query will be an instance of Model. This will add in any instance methods defined in your collection along with some CRUD helper methods. View the Core Instance Methods to see how they are implemented.
Default CRUD instance methods:
- save
- destroy
- toObject
- toJSON
If you would like to filter records and remove certain attributes you can override the toJSON
method like so:
var user = Waterline.Collection.extend({
attributes: {
name: 'string',
password: 'string',
// Override toJSON instance method
toJSON: function() {
var obj = this.toObject();
delete obj.password;
return obj;
}
}
});
// Then on an instantiated user:
user.find({ id: 1 }).exec(function(err, model) {
return model.toJSON(); // Will return only the name
});
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. For convenience, promises are supported by default.
Callback Method
User.findOne({ id: 1 }, function(err, user) {
// Do stuff here
});
Deferred Object Method
User.find()
.where({ id: { '>': 100 }})
.where({ age: 21 })
.limit(100)
.sort('name')
.exec(function(err, users) {
// Do stuff here
});
Promises
User.findOne()
.where({ id: 2 })
.then(function(user){
var comments = Comment.find({userId: user.id}).then(function(comments){
return comments;
});
return [user.id, user.friendsList, comments];
}).spread(function(userId, friendsList, comments){
// Promises are awesome!
}).catch(function(err){
// An error occurred
})
Promises use the Bluebird library, so anything you do after the first then
call (or spread
, or catch
), will be a complete Bluebird promise object. Remember, you must end the query somehow (by calling then
or one of the other functions) in order to complete the database request.
Each of the following basic methods are available by default on a Collection instance:
- findOne
- find
- create
- update
- destroy
In addition you also have the following helper methods:
- createEach
- findOneLike
- findLike
- startsWith
- endsWith
- contains
In addition to the other find methods, there are a few helper methods to take care of pagination:
- skip
- limit
- paginate
Skip takes an integer and can be used to skip records:
User.find().skip(20);
Limit takes an integer and limits the amount of records returned:
User.find().limit(10);
And put together they create the ability to paginate through records as you would pages. For example, if I wanted 'page 2' of a given record set, and I only want to see 10 records at a time, I know that I need to skip(10)
and limit(10)
like so:
User.find().skip(10).limit(10);
But, while we are thinking in terms of pagination, or pages, it might be easier to use the final helper - paginate:
User.find().paginate({page: 2, limit: 10});
Paginate has several options:
paginate()
defaults options to{page: 0, limit: 10}
paginate({page: 2})
uses{page: 2, limit: 10}
as the optionspaginate({limit: 20})
uses{page: 0, limit: 20}
as the optionspaginate({page: 1, limit: 20})
uses{page: 1, limit: 20}
as the options
It returns a deferred object so that you can continue to chain your helpers.
Sorting can be performed in the deferred object query method sort
or by adding the sort key into the criteria object.
Simply specify an attribute name for natural (ascending) sort, or specify an asc
or desc
flag for ascending or descending orders respectively.
User.find()
.sort('roleId asc')
.sort({ createdAt: 'desc' })
.exec(function(err, users) {
// Do stuff here
});
Do NOT allow users to define the sort
field.
Validations are handled by Anchor which is based off of Node Validate and supports most of the properties in node-validate. For a full list of validations see: Anchor Validations.
Validations are defined directly in you Collection attributes. In addition you may set the attribute type
to any supported Anchor type and Waterline will build a validation and set the schema type as a string for that attribute.
Validation rules may be defined as simple values or functions (both sync and async) that return the value to test against.
var User = Waterline.Collection.extend({
attributes: {
firstName: {
type: 'string',
required: true,
minLength: 5,
maxLength: 15
},
lastName: {
type: 'string',
required: true,
minLength: 5,
maxLength: 100
},
age: {
type: 'integer',
after: '12/12/2001'
},
website: {
type: 'string',
// Validation rule may be defined as a function. Here, an async function is mimicked.
contains: function(cb) {
setTimeout(function() {
cb('http://');
}, 1);
}
}
}
});
var Event = Waterline.Collection.extend({
attributes: {
startDate: {
type: 'date',
// Validation rule functions allow you to validate values against other attributes
before: function() {
return this.endDate;
}
},
endDate: {
type: 'date',
after: function() {
return this.startDate;
}
}
}
}
You can define your own types and their validation with the types
hash. It's possible to access and compare values to other attributes.
var User = Waterline.Collection.extend({
types: {
point: function(latlng){
return latlng.x && latlng.y
},
password: function(password) {
return password === this.passwordConfirmation;
});
},
attributes: {
firstName: {
type: 'string',
required: true,
minLength: 5,
maxLength: 15
},
location: {
// Note, that the base type (json) still has to be defined
type: 'json',
point: true
},
password: {
type: 'string',
password: true
},
passwordConfirmation: {
type: 'string'
}
}
});
You can add an index property to any attribute to create an index if your adapter supports it. This comes in handy when performing repeated queries against a key.
var User = Waterline.Collection.extend({
attributes: {
serviceID: {
type: 'integer',
index: true
}
}
});
Currently Waterline doesn't support multi-column indexes in the attributes definition. If you would like to build any sort of special index you will still
need to build that manually. Also note that when adding a unique
property to an attribute, an index will automatically be created for that attribute.
There is currently an issue with adding indexes to string fields. Because Waterline performs its queries in a case insensitive manner, we are unable to use the index on a string attribute. There are some workarounds being discussed but nothing is implemented so far. This will be updated in the near future to fully support indexes on strings.
Lifecycle callbacks are functions you can define to run at certain times in a query. They are useful for mutating data before creating or generating properties before they are validated.
Callbacks run on Create
- beforeValidate / fn(values, cb)
- afterValidate / fn(values, cb)
- beforeCreate / fn(values, cb)
Callbacks run on Update
- beforeValidate / fn(valuesToUpdate, cb)
- beforeUpdate / fn(valuesToUpdate, cb)
- afterUpdate / fn(updatedRecord, cb)
Callbacks run on Destroy
- beforeDestroy / fn(criteria, cb)
- afterDestroy / fn(cb)
All tests are written with mocha and should be run with npm:
$ npm test