rethinker v0.3.2
Rethinker
Rethinker offers a minimalist ActiveRecord-like API service layer for RethinkDB, the main focus is to simplify the relational queries for has-one, has-many, many-many relationships, with filters, and nested relational query support.
#Install
npm install rethinker
#Running Tests
Ensure that RethinkDB is installed correctly, and it's listening on port 28015. Then run the tests with
npm test
#Getting started
Let's assume we have the following entries and their relationships in our model:
A onlne course can be composed by many video lectures, which can be either be public or private, and a course is enrolled by many students.
And we would like to query the following:
- All courses along with their private lectures, with video related data if it's available
- All students with email ending in '@institution.org', along with their enrolled courses
##1. Initialize rethinker with database connection string
var Rethinker = require('rethinker').init({
host: 'localhost',
port: 28015,
db: 'test',
pool: { // optional settings for pooling (further reference: https://github.com/coopernurse/node-pool)
max: 100,
min: 0,
log: false,
idleTimeoutMillis: 30000,
reapIntervalMillis: 15000
}
})
##2. Initialize services
var LecturesService = Rethinker.extend({
modelName: 'Lecture',
tableName: 'lectures', // optional, by default it takes the modelName, lowercase it, and make it plural
relations: {
hasOne: {
course: { //for simplicity, 'has one course' is the same as 'belongsTo a course'
on: 'courseId', // attribute defined on the 'lectures' table
from: 'Course'
},
video: {
on: 'videoId',
from: 'Video'
}
}
}
});
var CoursesService = Rethinker.extend({
modelName: 'Course',
relations: {
hasMany: {
videoLectures: {
on: 'courseId',
from: 'Lecture',
filter : function(lecture){ // this will be used for 'filter' method in the rethinkdb API
return lecture.ne(null);
}
},
students: {
on: ['courseId', 'studentId'],
through: 'courses_students', //table 'courses_students' has to be created manually for now
from: 'Student'
}
},
hasOne: {
privateLecture: {
on: 'courseId',
from: 'Lecture',
filter: {
private: true
}
}
}
}
});
var StudentsService = Rethinker.extend({
modelName: 'Student',
relations: {
hasMany: {
'enrolledCourses': {
on: ['studentId', 'courseId'],
through: {
tableName: 'courses_students',
filter: {
enrolled: true
}
},
from: 'Course'
}
}
}
});
var VideosService = Rethinker.extend({
modelName: 'Video',
})
var lecturesService = new LecturesService(),
coursesService = new CoursesService(),
studentsService = new StudentsService(),
videosService = new VideosService();
##3. Querying data
#####All courses along with their private lectures ordered by createTime, with video related data if it's available
coursesService.findAllCourse(null, {
with : {
related : 'privateLecture',
orderBy : 'createTime desc',
with : 'video'
}
})
//Sample result
[{
id : '0f5a54ea-dba3-4eda-b44f-faf17ab1c9e4',
title : "Course I",
privateLecture : {
courseId : '0f5a54ea-dba3-4eda-b44f-faf17ab1c9e4',
private : true,
createTime : 1394630809686,
videoId : '400693be-de3d-4f41-80d3-86f58eb26cc6'
video : {
id : '400693be-de3d-4f41-80d3-86f58eb26cc6',
url : 'path/video1.mp4'
}
}
},
...
]
#####All students with email ending in '@institution.org', along with their enrolled courses
studentsService.findAllStudent(function(studentRow){
return studentRow('email').match('@institution.org')
}, {with : 'enrolledCourses'})
//Sample results
[{
name : "Khanh Luc",
email : "khanh@institution.org",
enrolledCourses : [{
id : "0f5a54ea-dba3-4eda-b44f-faf17ab1c9e4"
title : "Course I",
},
...
]
}
....
]
#CRUD operations
By initializing the service layer as:
var CoursesService = Rethinker.extend({modelName : 'Course'})
Rethinker adds the following methods to CoursesService.prototype
###Create
CoursesService.prototype.createCourse([jsonData, options]) -> Promise
The options
argument is optional. It can be an object with the fields:
validate
: whether to call validation method on saving the data (default = true)returnVals
: whether or not to return the saved value, it also supports multiple insert (default = true)
//Example
var coursesService = CoursesService.getService(); // returns singleton instance of coursesService
coursesService.createCourse({ // insert a single course data
title : "Physics I"
}).then(function(course){
//course : {id: ... , title : 'Physics I'}
})
coursesService.createCourse([ // insert multiple courses data
{ title : "Physics II"},
{ title : "Physics III"}
]).then(function(courses){
//course : [{id: ... , title : 'Physics II'}, {id: ... , title : 'Physics III'}]
})
###Retrieve
CoursesService.prototype.findCourse([queryCriteria, options]) -> Promise
CoursesService.prototype.findAllCourse([queryCriteria, options]) -> Promise
The queryCriteria
can be set as either object, function or string:
object/function
: the filter method is invoked to query the datastring
: when options.index is not set, the value is treated as primary key, otherwise getAll method is invoked to query the data
In order to query all the data in the table, the queryCriteria
argument can be set to null
in findAllCourses
method
The options
argument is optional. It can be an object with the fields:
index
: same index value to be passed to the APIorderBy
: same as orderBy, with a minor syntax difference:orderBy: r.desc('createTime')
can be written asorderBy: 'createTime desc'
fields
: same as pluck, it also can be provided with an array of field names:fields : ['id', 'title', 'createTitle']
with
: can be set as either string, array, or objectstring
: name of the relationship previously definedarray
: an array of relational query options,object
: used when need to apply some filtering or query nested relational datarelated
: name of the relationship relative to the resulting queried datafilter
: filter the results using filterorderBy
: order the resulting relational datafields
: pluck fields from the resulting relational datawith
: in case further nested relational data need to be fetched, same options above are also applied
//Example
var lecturesService = LecturesService.getService(), // returns singleton instance of lecturesService
coursesService = CoursesService.getService();
lecturesService.findLecture('143ef66b-58fd-41d0-b019-30818841699f') // find lecture by id
lecturesService.findLecture(user.id, {index : 'userId'}) // retrieve a single lecture by secondary index 'userId'
lecturesService.findLecture({title : "Lecture I"}, {fields : 'title'}) // find lecture's title by title
lecturesService.findAllLecture(function(lecture){
return lecture.hasFields('videoId')
}, {orderBy : 'title desc'}) // find all lectures that has the videoId attribute, ordered by title
coursesService.findAllCourse(null, { // find all the courses with enrolled students, and private video lectures ordered by title
with : ['students', {
related : 'lectures',
filter : {
private : true
},
orderBy : 'title',
with : 'video'
}
]
}).then(function(courses){
/*
courses : [
{
id : '..',
title : 'Physics I',
lectures : [{ id: ..., title : 'Lecture I', private : true, videoId : ..., video : {...} }...],
students : [{ ... }]
},
...
]
*/
})
###Update
CoursesService.prototype.updateCourse([jsonData, queryCriteria, options]) -> Promise
CoursesService.prototype.updateAllCourse([jsonData, queryCriteria, options]) -> Promise
The jsonData
is the data to be updated, queryCriteria
and options
are the same ones described in Retrieve section, with additional options: validate
, returnVals
described in the Create section
//Example
var videosService = VideosService.getService(); // returns singleton instance of videosService
videosService.updateVideo({url : "path/newName.mp4"}, '3e3a00a1-7d5c-4ed3-9a10-7494d81919eb').then(function(){ // update video by it's primary key
}).then(function(video){
// video : video json data with updated values
})
videosService.updateAllVideo({url : "path/newName.mp4"}, req.user.id, {index : 'userId'}) // update all user's videos
.then(function(videos){
//returns an array of updated video values
})
###Delete
CoursesService.prototype.deleteCourse([queryCriteria, options]) -> Promise
queryCriteria
and options
are the same ones described in Retrieve section
//Example
coursesService.findCourse({title : 'Physics I'})
.then(function(course){
return lecturesService.deleteLecture(course.id, {index : 'courseId'}) // delete all lectures in 'Physics I'
})
coursesService.deleteCourse() // delete all courses
#Additional methods
Also the following additional methods are available, all of them return promise
CoursesService.prototype.validateCourse([jsonData]) -> Promise // return false to cancel the persistence task
CoursesService.prototype.beforeCreateCourse([jsonData]) -> Promise // return false to cancel the insert task
CoursesService.prototype.beforeUpdateCourse([jsonData]) -> Promise // return false to cancel the update task
CoursesService.prototype.beforeSaveCourse([jsonData]) -> Promise // return false to cancel the persistence task
CoursesService.prototype.afterCreateCourse([jsonData]) -> Promise
CoursesService.prototype.afterUpdateCourse([jsonData]) -> Promise
CoursesService.prototype.existCourse([jsonData]) -> Promise
#Extend default methods
Each instance of Rethinker exposes the following attributes/methods that allow to build a complex queries more easily:
r
: exposes the rethinkdb APItable
: exposes the table instance, takes the this.tableName to initialize ther.table(this.tableName)
db
: expose the DB instance with the run methodbuildQuery
:function buildQuery(queryCriteria, opts, tableName) -> Promise
OrdersService.prototype.someOtherBusinessLogics ...
OrdersService.prototype.findAllOrder = function (queryData, opts, filters) { // override the default findAll method to support extra filter options
!opts && (opts = {});
!filters && (filters = {});
var orderQuery = filters.q || "",
query = this.buildQuery(queryData, _.merge({orderBy: [filters.sort, filters.order].join(' ')}, opts));
if (orderQuery.length > 0) {
query = query.filter(function (order) {
return order('orderId').match(orderQuery)
.or(order('code').eq(orderQuery))
.or(order('user')('name').match(orderQuery))
.or(order('user')('email').match(orderQuery))
.or(order('user')('address').match(orderQuery));
});
}
return this.db.run(query);
};
Rethinker also exposes a DB
instance, basically it wraps around the run method using pooling and returns a promise
var r = require('rethinkdb'),
DB = require('rethinker').DB,
db = new DB({
host: 'localhost',
port: 28015,
db: 'test',
pool: { // optional settings for pooling (further reference: https://github.com/coopernurse/node-pool)
max: 100,
min: 0,
idleTimeoutMillis: 30000,
reapIntervalMillis: 15000
}
});
db.run(r.tableCreate('courses_students')).then(function(result){
});
#Save relational data
Currently it only supports saving has-one relationships
var BookingService = Rethinker.extend({
modelName : 'Booking',
relations : {
hasOne : {
activeOrder : {
on : 'bookingId',
from : 'Order',
filter : {
active : true
}
sync : true
},
completedOrder : {
on : 'bookingId',
from : 'Order',
filter : {
active : false,
completed : true
}
sync : 'readOnly'
}
}
}
});
OrdersService = Rethinker.extend({
modelName : 'Order'
}
})
The sync
property in each relation
declaration is used to specify whether or not to save those related data.
When inserting the following data to the database:
var bookingService = new BookingService();
bookingService.createBooking({
date : new Date(),
userId : req.user.id,
activeOrder : {
active : true,
completed : false
},
completedOrder : {
active : false,
completed : true
}
});
It will generate the following data in 'booking' table and the 'orders' table:
//booking table
{
id : 'b0de0baa-5028-4da4-ae08-456b1c0d7239'
date : ...
userId : ..
}
//orders table
{
id : ...
active : true,
completed : false,
bookingId : 'b0de0baa-5028-4da4-ae08-456b1c0d7239'
}
Notice that in order to avoid data duplicity, the activeOrder
, and completedOrder
attributes are not saved in the booking table. Also in the orders table, only the activeOrder
is saved since it has the property sync : true
Please refer the test file for further usage example of this option.
#FAQ
##Is this an ORM? Not quite so, the main intend is to offer a wrapper around the official API, placing the main emphasis on querying relational (nested relational) data with less code, it's basically a mixin that decorates methods in a class prototype chain. If you are looking for fully featured ORM solution, there are couple of alternatives: Thinky, Reheat
##Does this offer validation layer?
Personally i use the validate
hook along with express-validator library to validate the incoming data manually, might consider to add a validation layer in the future releases.
##Can the API be simplified?
Like instead of coursesService.findAllCourse
, can't it just be courses.findAll
?
Sure thing, it's just my personal preference, when i'm refactoring, finding 'findAllCourse' usage is a lot more easier, and less error prone than just 'findAll', will consider to add an extra option for this.
##What version of RethinkDB supports? As RethinkDB hasn't reach the LTS release yet, use of latest version of RethinkDB would be recommended.