seed-rethink
v1.4.2
Published
rethinkdb odm
Downloads
3
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 seed-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
- 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
MIT