seed-rethink v1.4.2
rethink
rethinkdb ODM. It's conventions and api highly resemble but are not limited to mongoose ODM for mongodb.
Architecture
Rethink
Main module Rethink is responsible for initalisation of schema, models and database connection. To simplify the API rethink will queue all db operations until connection is established.
var r = new Rethink();
var schema = r.schema({
name: r.types.string()
});
var User = r.model('User', schema);
User.create({ name: 'Ben' }, cb);
r.connect('localhost:28015');Schema
Module Schema has a few thin, but capacitive api layers:
validation
validation is powered by node-veee, check https://github.com/node-veee/veee for details
before hooks
validate will be fired before object is validated. It will be invoked asynchronously:
schema.before('validate', function(object, done) {});create will be fired after validation. It will be invoked asynchronously:
schema.before('create', function(object, done) {});update will be fired after validation. It will be invoked asynchronously:
schema.before('update', function(object, done) {});save will be fired after create or update, but before object is saved to db. It will be invoked asynchronously:
schema.before('save', function(object, done) {});remove hook is fired before object removal. It will be invoked asynchronously:
schema.before('remove', function(id, done) {});query hook is fired before results are retrieved from database. It will be invoked asynchronously:
schema.before('query', function(params, done) {
// params.query
// params.options
// params.schema
});after hooks
validate will be fired after object is validated. It will be invoked asynchronously:
schema.after('validate', function(object, done) {});save will be fired before create or update, but after object is saved to db. It will be invoked synchronously:
schema.after('save', function(object) {});create will be invoked synchronously:
schema.after('create', function(object) {});update will be invoked synchronously:
schema.after('update', function(object) {});remove hook is fired after object removal and will be invoked synchronously:
schema.after('remove', function(id) {});query hook is fired after results have been retrieved from database and it will be invoked asynchronously:
schema.after('query', function(params, done) {
// params.query
// params.options
// params.results
// params.schema
});hook error handling
All asynchronous callbacks, eg. done, will accept an optional error argument that will stop execution chain.
schema.before('save', function(article, done) {
if (article.user !== req.session.user) {
return done(new Error('Unauthorized'));
}
done();
});plugins
schema plugins use node-style .use pattern
schema
.use(plugin)
.use(anotherPlugin);model prototype extension
its possible to extend default model api with static methods from schema.
schema.statics.greet = function() {
console.log('hello');
}
var Person = r.model('Person', schema);
Person.hello(); // will log 'hello'By design schema should not be altered after it's built, eg.:
var Post = r.model('Post', schema);
schema.statics.publish = function() {...}
Post.publish(); // won't workexample
var schema = r.schema({
username: { type: r.types.string().alphanum(), index: true },
password: r.types.string().min(8).max(32)
});
schema.before('save', function(record, done) {
record.user = req.session.user;
done();
});
schema.after('save', function(record) {
log('Record saved %j', record);
});
schema.statics.findById = function(id, cb) {
this.find({ id: id }, cb);
}
schema.use(timestamps);Table
Module Table is used (internally) to automatically create a backing table, manage table indexes and wait (lock) until rethinkdb builds them. Table name is lower-cased pluralised version of a model name, eg. model: 'Person', table: 'people'.
Model
Model provides a node callback-style api to manipulate records in a single rethinkdb table. Model api could be extended, using schema.statics object.
var Person = r.model('Person', schema);
Person.create(...);
Person.update(...);
Person.remove(...);
Person.find(...);
Person.findOne(...);API
Rethink
constructor({Object} options):Rethink
Initialize a new instance of Rethink (with options hash)
var r = new Rethink();schema({Object} definition, {Object} options):Schema
Create a new schema, using definition and optionally pass options to it
var schema = r.schema({
name: { type: r.types.string().required(), index: true, default: 'anonymous' }
});model({String} name, {Schema} schema):Model
Create a new model using singular case-sensitive name and a schema
var User = r.model('User', schema);connect({String} dbUrl, {Object} options)
Connect to rethinkdb instance using database url (e.g. 'host:port') and options
r.connect('localhost:28015');before({String} hook, {Function} fn({Rethink} rethink)):Rethink
Create a before hook for one of the actions. Only used to attach to buildSchema hook.
rethink.before('buildSchema', function(schema) {
schema.use(somePlugin);
});use({Function} plugin({Rethink} rethink, {Object} options), {Object} options)):Rethink
Attach a plugin to rethink. Mainly used for attaching a plugin to an internal before:buildSchema hook.
r.use(plugin, options);
function plugin(rethink, options) {
rethink.Schema.prototype.hello = function() {
console.log('World');
}
// will fire everytime before schema build
// equivalent of attaching a plugin to every schema
rethink.before('buildSchema', function(schema) {
schema.use(timestamps, options);
});
}
function timestamps(schema, options) {
schema.before('create', function(record, done) {
var now = (new Date).toISOString();
record.createdAt = now;
record.updatedAt = now;
done();
});
schema.before('update', function(record, done) {
record.updatedAt = (new Date).toISOString();
done();
});
};types
Access to node-veee validation types
rethink.types.string().required();Schema
Access to schema class
rethink.Schema.prototype.hello = function() {
return 'world';
}Static Properties
Types — access to node-veee validation types
Rethink.Types.string().required();Schema — access to schema class
var schema = new Rethink.Schema({
name: Rethink.Types.string().min(3)
});Plugins — access to built-in plugins
var rethink = new Rethink();
rethink
.use(Rethink.Plugins.Timestamps)
.use(Rethink.Plugins.Populate); Schema
constructor({Object} params):Schema
{Object} type: node-veee validator for a given type
var schema = new Rethink.Schema({
startsAt: { type: Rethink.Types.date() }
});
// shorthand
var schema = new Rethink.Schema({
startsAt: Rethink.Types.date()
});{Boolean|Object} index: used by Table to make sure rethink indexes a given field
var postSchema = new Rethink.Schema({
authorId: { index: true }
});
var markerSchema = new Rethink.Schema({
position: { index: { geo: true } }
});{*} default: set default value to field if fnot specified. Default also has support for sync functions with length 1 and async with length 2.
// primitives
var postSchema = new Rethink.Schema({
title: { default: 'Untitled Post' }
});
// sync
var postSchema = new Rethink.Schema({
createdAt: {
default: function(object) {
return (new Date).toISOString();
}
}
});
// async
var ticker = new Rethink.Schema({
price: {
default: function(object, cb) {
request(YAHOO_FINANCE_TICKER_PRICE + object.name, function(err, body, response) {
if (err) return cb(err);
cb(null, body.price);
});
}
}
});before({String} action, {Function} hook({Object|Array} data, {Function} done({Error} err))):Schema
Create a before hook for one of the actions
schema.before('save', function(record, done) {
record.updatedAt = Date.now();
done();
});after({String} action, {Function} hook({Object|Array} data, {Function} done({Error} err))):Schema
Create an after hook for one of the actions
schema.after('query', function(records, done) {
records.forEach(function(record) {
record.updatedAt = new Date(record.updatedAt); // deserialize date
});
done();
});statics
Define a static model method
schema.static.findByUsername = function(username, cb) {
this.findOne({ username: username }, cb);
}
var User = r.model('User', schema);
User.findByUsername('peter', function(err, user) {});use({Function} plugin({Schema} schema, {Object} options), {Object} options):Schema
Attach a plugin to schema
var timestamps = function(schema) {
schema.before('create', function(record, done) {
record.createdAt = Date.now();
done();
});
schema.before('update', function(record, done) {
record.updatedAt = Date.now();
done();
});
}
schema.use(timestamps);Instance Properties
types — Access to node-veee validation types
schema.types.string().required();Model
create({Object} object, {Function} callback(err, object))
Create an object
User.create({ name: 'Peter' }, function(err, user) {});update({Object} object, {Function} callback(err, object))
Update an object
User.update({ id: '123434-1233-1231-123124', name: 'Peter' }, function(err, user) {});remove({String} id, {Function} callback(err, id))
Remove an object using an id
User.remove('123434-1233-1231-123124', function(err, userId) {});find({Object} query, {Object} options, {Function} callback(err, records))
Find model records using a query and options
User.find({ name: 'Peter' }, { limit: 2, skip: 0, order: 'name' }, function(err, users) {});findOne({Object} query, {Object} options, {Function} callback(err, record))
Find one model record using a query and options
User.find({ name: 'Peter' }, function(err, user) {});Plugins
Architecture
Rethink is made to be pluggable, in fact, most of rethink functionality could be (and is) implemented as plugins.
Plugin is just a function that takes rethink and options as arguments and is able to plug into Schema, Model and Rethink prototypes and hooks.
// plugins/safe-delete.js
function updateSchema(schema, options) {
schema._fields.deletedAt = {
type: schema.types.string().isodate().optional(),
default: ''
}
schema.before('query', function(params, done) {
if (!params.options.includeDeleted) {
extend(params.query, { deletedAt: '' });
}
done();
});
schema.statics.remove = function(id, cb) {
this.update({
id: id,
deletedAt: (new Date).toISOString()
}, function(err, record) {
if (err) return cb(err);
if (!record) return cb(new Error('record not found'));
cb(null, 1);
});
}
}
// expose rethink plugin
exports = module.exports = function(rethink, options) {
rethink.before('buildSchema', function(schema) {
updateSchema(schema, options);
});
}
// expose schema plugin
exports.safeDelete = function(schema, options) {
updateSchema(schema, options);
}
// app.js
// use safe delete plugin on all schema
var safeDelete = require('./plugins/safe-delete');
rethink.use(safeDelete);
// use safe delete plugin only on one schema
var safeDelete = require('./plugins/safe-delete').safeDelete;
schema.use(safeDelete); Populate
Populate plugin introduces application-side deep table joins to rethink.
// inject schema instance methods
rethink.use(Rethink.Plugins.Populate);
var User = rethink.model('User', rethink.schema({
name: rethink.types.string(),
}).hasOne('company'));
var Company = rethink.model('Company', rethink.schema({
name: rethink.types.string()
}).hasMany('users', { refField: 'company' }));
User.find({}, { populate: 'company' }, function(err, users) {
// each user object will contain corresponded company
});
User.find({}, { populate: { field: 'company', populate: 'users' }, function(err, users) {
// each user object will contain corresponded company
// corresponded company will contain all users of that company
});Advanced
var schema = rethink.schema({
user: {
type: schema.types.string(),
// population options
field: 'user', // source of join
ref: 'User', // foregin model name
refField: 'id', // foreign field to match source of join
destination: 'user' // destination of model query
single: true // will treat relationship as a single object
index: true // all relational fields must be indexed
},
comments: {
// population options
field: 'id', // source of join
ref: 'Comment', // foreign model name
refField: 'reference', // foreign field to match source of joins
destination: 'comments' // destination of model query
single: false // will treat realtionship as array
virtual: true // exclude from object validation
}
});
// shorthands, do same above
// !!!note: refField is required to be specified for `hasMany` relationships
schema.hasOne('user');
schema.hasMany('comments', { refField: 'reference' });
var Reference = rethink.model('Reference', schema);
Reference.find({}, { populate: ['user', 'comments'] }, function(err, references) {
// each reference object will contain user object
// each reference object will contain comments array
// eg. references:
// [{
// id: "123",
// user: {
// id: "345",
// name: "John Doe"
// },
// comments: [{
// id: "238",
// user: "555",
// message: "hello"
// }]
// }]
});Query time population
to make use of advanced population you could define population options in query
// get all references of this user
Reference.find({ user: user.id }, {
populate: {
field: 'id',
ref: 'Bookmark',
refField: 'reference',
destination: 'bookmark',
query: { user: user.id } // only get reference bookmark of this user,
options: {} // in case of limit, offset, order, etc.
single: true
}
}, function(err, references) {
// references:
// [{
// id: "123",
// user: "555",
// bookmark: {
// id: "932",
// reference: "123",
// user: "555"
// }
// }]
});Timestamps
Timestamps plugin adds createdAt and updatedAt ISO 8601 dates to schema and helps to manage them accordingly
rethink.use(Rethink.Plugins.Timestamps);
var User = rethink.model(rethink.schema({
name: rethink.schema.string()
}));
User.create({ name: 'Vlad' }, function(err, user) {
// user:
// {
// id: "123",
// name: "Vlad",
// createdAt: "2015-09-23T17:35:22.124Z",
// updatedAt: "2015-09-23T17:35:22.124Z"
// }
});
...
User.update({ id: '123', name: 'Vladimir' }, function(err, user) {
// user:
// {
// id: "123",
// name: "Vladimir",
// createdAt: "2015-09-23T17:35:22.124Z",
// updatedAt: "2015-09-23T17:36:23.416Z"
// }
});Installation
$ npm install seed-rethink --saveUsage
// require
var Rethink = require('rethink');
// initialize
var r = new Rethink();
// connect to database
r.connect(url, connectionOptions);
// define schema
var companySchema = r.schema({
name: r.types.string().required(),
address: r.types.object().keys({
country: r.types.string(),
city: r.types.string(),
street: r.types.string(),
block: r.types.string()
}).optional()
}, schemaOptions);
var userSchema = r.schema({
username: { type: Rethink.Types.string(), index: true },
password: r.types.string().min(6).max(32).alphanum(),
company: { type: r.types.uuid(), ref: 'Company' },
gender: { type: r.types.string(), default: 'not-specified' }
}, schemaOptions);
// use hooks
userSchema.before('save', function(record, done) {
// do stuff with record
done();
});
userSchema.after('query', function(records, done) {
// do stuff with records
done();
});
// use plugins
userSchema.use(plugin, options);
// define static (helper) methods
userSchema.statics.byEmail = function(email, options, cb) {
return this.find({ email: email }, options, cb);
}
// create model (table)
var User = r.model('User', userSchema);
var Company = r.model('Company', companySchema);
// create records
Company.create({ name: 'seedalpha' }, function(err, company) {
User.create({
username: 'john',
password: '123456',
company: company.id
}, function(err, user) {
// ...
});
});
// search records
User.find({ username: 'john' }, { limit: 1 }, function(err, user) {
// ...
});Roadmap
- live query support
- make use of rethinkdb indexes
- rethink options
- spec tests
- 100% coverage
- move all rethinkdb calls out into an engine
- implement es-index as a plugin
- parallel index creation, table creation (speedup startup time)
- rethink eventmietter api
- model eventemitter api
Development
$ git clone git@github.com:seedalpha/rethink.git
$ cd rethink
$ npm install
$ npm test
$ npm run coverageAuthor
Vladimir Popov vlad@seedalpha.net
License
MIT