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 =
2. Initialize services
var LecturesService = Rethinker; var CoursesService = Rethinker; var StudentsService = Rethinker; var VideosService = Rethinker var lecturesService = coursesService = studentsService = 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)
//Examplevar coursesService = CoursesService; // returns singleton instance of coursesServicecoursesService coursesService
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
//Examplevar lecturesService = LecturesService // returns singleton instance of lecturesService coursesService = CoursesService; lecturesService // find lecture by idlecturesService // retrieve a single lecture by secondary index 'userId'lecturesService // find lecture's title by titlelecturesService // find all lectures that has the videoId attribute, ordered by titlecoursesService
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
//Examplevar videosService = VideosService; // returns singleton instance of videosServicevideosService videosService // update all user's videos
Delete
CoursesService.prototype.deleteCourse([queryCriteria, options]) -> Promise
queryCriteria
and options
are the same ones described in Retrieve section
//ExamplecoursesService coursesService // 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
OrdersServiceprototypesomeOtherBusinessLogics ... OrdersServiceprototype { // override the default findAll method to support extra filter options !opts && opts = {}; !filters && filters = {}; var orderQuery = filtersq || "" query = this; if orderQuerylength > 0 query = query; return thisdb;};
Rethinker also exposes a DB
instance, basically it wraps around the run method using pooling and returns a promise
var r = DB = DB 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;
Save relational data
Currently it only supports saving has-one relationships
var BookingService = Rethinker; OrdersService = Rethinker
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.