rethink
v1.4.0
Published
rethinkdb odm
Downloads
33
Readme
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 work
example
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 rethink --save
Usage
// 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
- opensource?
- 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 [email protected]:seedalpha/rethink.git
$ cd rethink
$ npm install
$ npm test
$ npm run coverage
Author
Vladimir Popov [email protected]
License
©2015 SeedAlpha