rethink

rethinkdb odm

Usage no npm install needed!

<script type="module">
  import rethink from 'https://cdn.skypack.dev/rethink';
</script>

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 git@github.com:seedalpha/rethink.git
$ cd rethink
$ npm install
$ npm test
$ npm run coverage

Author

Vladimir Popov vlad@seedalpha.net

License

©2015 SeedAlpha