Comments:"Bookshelf.js - Javascript ORM with some Backbone"
Bookshelf is a promise based ORM for Node.js, built on Knex query builder. It extends the Model & Collection foundations of Backbone.js, providing transaction support, eager/nested-eager relation loading, and support for one-to-one, one-to-many, and many-to-many relations.
The project is hosted on GitHub, and the annotated source code is available, as well as a full test suite.
Latest Release: 0.1.0
Bookshelf is available for use under the MIT software license.
You can report bugs and discuss features on theGitHub issues page, add pages to the wiki or send tweets to @tgriesser.
Bookshelf's dependencies are listed in the package.json file, but includeBackbone.js (1.0.x),Underscore.js (1.4.x),when.js (2.0.1) for promise flow control, Knex.js. as the query builder foundation, and inflection for crafting intelligent defaults for related key and table names.
Introduction
Bookshelf aims to provide a simple library for common tasks when querying relational databases in javascript, and forming relations between these objects, using ideas from DataMapper and Active Record. At ~900 lines of well documented code, Bookshelf is simple to read, understand, and extend. It doesn't force you to use any specific validation scheme, and provides flexible and efficient relation/nested-relation loading, and first class transaction support.
Bookshelf extends the excellent foundation provided by Backbone.js Models and Collections, using similar patterns, naming conventions, and philosophies to build a lightweight, easy to navigate ORM. If you know how to use Backbone, you probably already know how to use Bookshelf.
Installation
All dependencies are specified in package.json file but include underscore.js, when.js, and knex.js library. You'll then need to install either mysql, pg, or sqlite3 from npm. It is advisable to install your own copy of Knex as well if you wish to build any custom queries outside of the ORM.
$ npm install knex --save # Then add one of the following: $ npm install mysql $ npm install pg $ npm install sqlite3
Bookshelf.Initialize
Required before using Bookshelf, the initialize performs the setup of the database, passing through to Knex's Initialize.
Bookshelf.Initialize({ client: 'mysql', connection: { host : 'localhost', user : 'root', password : '', database : 'your_db', charset : 'utf8' } });
It is also possible to use Bookshelf with multiple database connection instances if you'd like, by creating named instances with Bookshelf.Initialize. To do this, pass the name of the connection as the first parameter into Bookshelf and it will return that instance, which may also be referenced by name under Bookshelf.Instances.
var SqliteDB = Bookshelf.Initialize('sqlitedb', { client: 'sqlite', connection: { database : './testdb' } }); var MySql2 = Bookshelf.Initialize('mysql2', { client: 'mysql', connection: { ... } }); // Bookshelf.Instances['mysql2'] === MySql2; // Bookshelf.Instances['sqlitedb'] === SqliteDB;
Bookshelf.Model
Models are simple objects representing individual database rows, specifying thetableName and any relations to other models. They can be extended with any domain-specific methods, which can handle components such as validations, computed properties, and access control.
Bookshelf.Model.extend(properties, [classProperties])
To create a Model class of your own, you extend Bookshelf.Model
and provide instance properties, as well as optionalclassProperties to be attached directly to the constructor function.
extend correctly sets up the prototype chain, so subclasses created with extend can be further extended and subclassed as far as you like.
var Customer = Bookshelf.Model.extend({ initialize: function() { ... }, account: function() { ... }, login: function() { ... } });
Brief aside on super: JavaScript does not provide a simple way to call super — the function of the same name defined higher on the prototype chain. If you override a core function like set, or save, and you want to invoke the parent object's implementation, you'll have to explicitly call it, along these lines:
var Customer = Bookshelf.Model.extend({ set: function() { ... Bookshelf.Model.prototype.set.apply(this, arguments); ... } });
Bookshelf.Model.forge(/* args */)
A simple helper function to instantiate a new Model without needing new.
var Customer = Bookshelf.Model.extend({ tableName: 'customers' }); Customer.forge({item: 'value'}).save().then(function() { // ... });
new Model([attributes], [options])
When creating an instance of a model, you can pass in the initial values
of the attributes, which will be set on the
model. If you define an initialize function, it will be invoked when
the model is created.
new Book({ title: "One Thousand and One Nights", author: "Scheherazade" });
In rare cases, if you're looking to get fancy, you may want to override constructor, which allows you to replace the actual constructor function for your model.
var Books = Bookshelf.Model.extend({ tableName: 'documents', constructor: function() { Bookshelf.Model.apply(this, arguments); this.on('beforeQuery', function(model) { model.query('where', 'type', '=', 'book'); }); } });
ThetableName and hasTimestamps properties will be directly attached if passed in the options during model creation.
If {parse: true} is passed as an option, the attributes will first be converted by parse before beingset on the model.
model.tableName
A required property for any database usage, The tableName property
refers to the database table name the model will query against.
var Television = Bookshelf.Model.extend({ tableName: 'televisions' });
model.idAttribute
A database row's unique identifier (typically the primary key) is stored under this attribute.
model.id
A special property of models, the id is the unique idenfifer associated
named by the idAttribute. If you set the id
in the attributes hash, it will be copied onto the model as a direct property.
Models can be retrieved by id from collections, and the id is used in fetching models and
building model relations.
model.set(attributes, [options])
Set a hash of attributes (one or many) on the model. If any of the attributes
change the model's state, a "change" event will be triggered.
Change events for specific attributes are also triggered, and you can
bind to those as well, for example: change:title, and
change:content. You may also pass individual keys and values.
customer.set({first_name: "Joe", last_name: "Customer"}); customer.set("telephone", "555-555-1212");
model.get(attribute)
Get the current value of an attribute from the model. For example:
note.get("title")
model.fetch([options]).then(function(model) {...
Fetches a model from the database, using any attributes currently set on the model
to form a select query. Returns a promise, which will resolve with the fetched Model,
or null if the model isn't fetched. If you wish to trigger an error if the fetched model
is not found, pass {require: true} as one of the options to the fetch call. A "fetched"
event will be fired when a record is successfully retrieved.
// select * from `books` where `ISBN-13` = '9780440180296' new Book({'ISBN-13': '9780440180296'}) .fetch() .then(function(model) { // outputs 'Slaughterhouse Five' console.log(model.get('title')); });
The withRelated parameter may be specified to fetch the resource, along with any specified relations named on the model. A single property, or an array of properties can be specified as a value for the withRelated property. The results of these relation queries will be loaded into a relations property on the model, may be retreived with the related method, and will be serialized as properties on a toJSON call unless {shallow: true} is passed.
var Book = Bookshelf.Model.extend({ tableName: 'books', editions: function() { return this.hasMany(Edition); }, genre: function() { return this.belongsTo(Genre); } }) new Book({'ISBN-13': '9780440180296'}).fetch({ withRelated: ['genre', 'editions'] }).then(function(result) { ... });
model.load(relations, [options]).then(function(model) {...
The load method takes an array of relations to eager load attributes onto a Model,
in a similar way that the withRelated property works on fetch. Dot separated attributes
may be used to specify deep eager loading.
new Posts().fetch().then(function(collection) { collection.at(0) .load(['author', 'content', 'comments.tags']) .then(function(model) { JSON.stringify(model); }); }); { title: 'post title', author: {...}, content: {...}, comments: [ {tags: [...]}, {tags: [...]} ] }
Relation Types:
There are four types of relationships that may be defined between Models and Collections: hasOne, hasMany, belongsTo, belongsToMany. The relations are specified by created named methods on the model, which return the appropriate relation type.
var Summary = Bookshelf.Model.extend({tableName: 'summaries'}); var Author = Bookshelf.Model.extend({tableName: 'authors'}); var Owner = Bookshelf.Model.extend({tableName: 'owners'}); var Book = Bookshelf.Model.extend({ summary: function() { return this.hasOne(Summary); }, owner: function() { return this.belongsTo(Author); }, pages: function() { return this.hasMany(Pages); }, author: function() { return this.belongsToMany(Author); } }); new Book({id: 1}).summary().fetch().then(function(summary) { console.log(summary.toJSON()); });
Relations may also be loaded eagerly, by specifying a withRelated option during the fetch call.
new Book({id: 2}).fetch({ withRelated: ['summary', 'owner', 'pages', 'author'] }).then(function(book) { console.log(book.toJSON()); });
You may also want to eagerly load related models on a model or collection after it has already been fetched. For this, you may use the load method to specify which relations should be eagerly loaded on the model or collection.
var accounts = new Accounts(); accounts.fetch() .then(function(collection) { return collection.at(0).load('account_info'); }) .then(function(model) { res.json({ accounts: accounts.toJSON({shallow: true}), current_account: model }); }) .done();
Nested eager loads may be performed, by separating the nested relations with '.'.
new Story({id: 2}).fetch({ withRelated: ['comments.tags', 'comments.author', 'author'] }).then(function(model) { JSON.stringify(model); }); // performs 5 queries in total, outputting: { id: 2, title: '', author: {...}, comments: [ {tags: [{...}, {...}], author: {...}}, {tags: [...], author: {...}}, {tags: [...], author: {...}} ] }
model.hasOne(Model, [foreignKey])
A one-to-one relation is a very basic relation, where the model has exactly
one of another Model, referenced by a foreignKey in the target Model.
By default, the foreign key is assumed to be the singular form of the current model,
followed by _id.
var Record = Bookshelf.Model.extend({ tableName: 'health_records' }); var Patient = Bookshelf.Model.extend({ record: function() { return this.hasOne(Record); } }); // select * from `health_records` where `patient_id` = 1; new Patient({id: 1}).record().fetch().then(function(model) { ... });
If the foreign key is different than the one assumed by the query builder, you may specify it in the second argument of the query.
model.hasMany(Target, [foreignKey])
Typically the most common relationship type, a hasMany is when the model
has a "one-to-many" relationship with the Target model or collection. Themodel is referenced by a foreignKey, which defaults to thetableName of the current model.
model.belongsTo(Model, [otherKey])
The belongsTo defines an inverse one-to-one relation, where the current model is a member
of another Model, referenced by the otherKey in the target Model.
By default, the other key is assumed to be the singular form of the target Model with a
_id suffix.
var Book = Bookshelf.Model.extend({ author: function() { return this.belongsTo(Author); } }); // select * from `author` where new Book({id: 1}).author().fetch().then(function() { });
model.belongsToMany(Target, [table], [fKey], [otherKey])
The belongsToMany method defines a many-to-many relation, where the current Model
is joined to one ore more of a Target model through another table. The default name for
the joining table is the two table names, joined by an underscore, ordered alphabetically. For example, a
users table and an accounts table would have a joining table of accounts_users.
var Account = Bookshelf.Model.extend({ tableName: 'accounts' }); var User = Bookshelf.Model.extend({ tableName: 'users', allAccounts: function () { return this.belongsToMany(User); }, adminAccounts: function() { return this.belongsToMany(User).query('where', 'access', '=', 'admin'); }, viewAccounts: function() { return this.belongsToMany(User).query('where', 'access', '=', 'readonly'); } });
The default key names in the joining table are the singular version of the table name, followed by _id. So in the above case, the columns in the joining table would be account_id, user_id, and access, which is used as an example of how dynamic relations can be formed using different contexts.
relation.attach(ids, [options])
Attaches one or more ids from a foreign table to the current table, in a belongsToMany relationship.
Creates & saves a new model and attaches the model with the related model.
relation.detach(ids, [options])
Detach one or more related object from their pivot tables. If a model or id is passed,
it attempts to remove the pivot table based on that foreign key. If no parameters
are specified, we assume we will detach all related associations.
relation.withPivot(columns)
The withPivot method is used exclusively on belongsToMany
relations, and allows for additional fields to be pulled from the joining table.
var Tag = Bookshelf.Model.extend({ comments: function() { return this.belongsToMany(Comment).withPivot(['created_at', 'order']); } });
By default, the pivot attributes are defined with the pivot_ prefix. If you would like to specify how they are named on the related models, pass an object with the key/value pairs to withPivot.
var Tag = Bookshelf.Model.extend({ comments: function() { return this.belongsToMany(Comment).withPivot({ created_at: 'comment_created_at', order: 'order' }); } }); new Tag({id: 1}).comments().fetch().then(function(model) { JSON.stringify(model); // {id: 1, name: 'tag name', comments: [ // {id: 1, comment_created_at: ..., order: ...}, // {id: 2, comment_created_at: ..., order: ...} // ]; });
model.save([params], [options]).then(function(model) {...
The Model's save method is used to perform an insert or update
query with the model, dependent on whether the model isNew.
If you wish to use update rather than insert or vice versa, the
{method: ...} option may be specified to explicitly state which sync method to use in the save call. If the method is update,
{patch: true} may be specified to only save the items in the params rather than
the full model.
new Post({name: 'New Article'}).save().then(function(model) { // ... });
Several events fired on the model when saving: a "creating", or "updating" event if the model is being inserted or updated, and a "saving" event in either case. To prevent saving the model (with validation, etc.), throwing an error inside one of these event listeners will stop saving the model and reject the promise. A "created", or "updated" event is fired after the model is saved, as well as a "saved" event either way.
model.destroy
The Model's destroy method performs a delete on the model, using the model'sidAttribute to constrain the query.
A "destroying" event is triggered on the model before being destroyed. To prevent
destroying the model (with validation, etc.), throwing an error inside one of these event listeners will stop
destroying the model and reject the promise. A "destroyed" event is fired after the model's removal
is completed.
model.attributes
The attributes property is the internal hash containing the databse row.
Please use set to update the attributes instead of modifying them directly. If you'd like to retrieve and munge a copy of the model's attributes, use toJSON instead.
model.defaults or model.defaults()
The defaults hash (or function) can be used to specify the default
attributes for your model. Unlike with Backbone's model defaults, Bookshelf defaults
are applied when an object is persisted rather than during creation.
var Meal = Bookshelf.Model.extend({ defaults: { appetizer: "caesar salad", entree: "ravioli", dessert: "cheesecake" } }); alert("Dessert will be " + (new Meal).get('dessert'));
Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.
model.format(attributes, [options])
The format method is used to modify the current state of the model before
it is persisted to the database. The attributes passed are a shallow clone
of the model, and are only used for inserting/updating - the current values of the
model are left intact.
model.parse(response, [options])
The parse method is called whenever a model's data is returned in a fetch call. The function is passed
the raw database response object, and should return the attributes hash
to be set on the model. The default implementation
is a no-op, simply passing through the JSON response. Override this if
you need to format the database responses - for example calling JSON.parse
on a text field containing JSON, or explicitly typecasting a boolean
in a sqlite3 database response.
model.toJSON([options])
Return a copy of the model's attributes for JSON stringification.
If the model has any relations defined, this will also call
toJSON on each of the related objects, and include them on the object unless {shallow: true}
is passed as an option. For more information on the use of toJSON, check out the JavaScript API for JSON.stringify.
var artist = new Bookshelf.Model({ firstName: "Wassily", lastName: "Kandinsky" }); artist.set({birthday: "December 16, 1866"}); console.log(JSON.stringify(artist));
Bookshelf proxies to Underscore.js to provide 6 object functions
on Bookshelf.Model. They aren't all documented here, but
you can take a look at the Underscore documentation for the full details…
user.pick('first_name', 'last_name', 'email'); chapters.keys().join(', ');
model.query([method], [*parameters])
The query method is used to tap into the underlying Knex
query builder instance for the current model. If called with no arguments, it will return the query
builder directly. Otherwise, it will call the specified method on the query builder, applying
any additional arguments from the model.query call.
var qb = model.query(); qb.where({id: 1}).select().then(function(resp) {... model .query('where', 'other_id', '=', '5') .fetch() .then(function(model) { ... });
model.resetQuery()
Used to reset the internal state of the current query builder instance. This method is
called internally each time a database action is completed by Bookshelf.Sync.
model.hasTimestamps
A boolean property on the model, determining whether the timestamp method will
be called on a model.save() call — the default value is false.
model.timestamp(options)
Sets the timestamp attributes on the model, if hasTimestamps is set to true.
The default implementation is to check if the model isNew and
set the created_at and updated_at attributes to the current date if it is new,
and just the updated_at attribute if it isn't. You may override this method if you
wish to use different column names or types for the timestamps.
model.clone()
Returns a new instance of the model with identical attributes, including any relations
from the cloned model.
model.isNew()
Checks for the existence of an id to determine whether the model
is considered "new".
var modelA = new Bookshelf.Model(); modelA.isNew(); // true var modelB = new Bookshelf.Model({id: 1}); modelB.isNew(); // false
model.sync(method, model, [options])
Creates and returns a new Bookshelf.Sync instance. Can be overridden for custom behavior.
model.relations
The relations property is the internal hash containing each of the relations
(models or collections) loaded on the model with load or with
the withRelated property during fetch. You can access
the relation using the related method on the model.
model.related()
The relations property is the internal hash containing each of the relations
(models or collections) loaded on the model with load or with
the withRelated property during fetch. You can access
the relation using the related method on the model.
Bookshelf.Collection
Collections are ordered sets of models returned from the database, which may be used with a full suite of Underscore.js methods.
Bookshelf.Collection.extend(properties, [classProperties])
To create a Collection class of your own, extend Bookshelf.Collection,
providing instance properties, as well as optional classProperties to be attached
directly to the collection's constructor function.
Bookshelf.Collection.forge(/* args */)
A simple helper function to instantiate a new Collection without needing new.
var Accounts = Bookshelf.Model.extend({ model: Account }); Accounts.forge([ {name: 'Person1'}, {name: 'Person2'} ]).save().then(function() { // ... });
collection.model
Override this property to specify the model class that the collection
contains. If defined, you can pass raw attributes objects (and arrays) toadd, create,
and reset, and the attributes will be
converted into a model of the proper type. Unlike in Backbone.js, the model
attribute may not be a polymorphic model.
var Trilogy = Bookshelf.Collection.extend({ model: Book });
new Collection([models], [options])
When creating a Collection, you may choose to pass in the initial array
of models. The collection's comparator
may be included as an option. Passing false as the
comparator option will prevent sorting. If you define aninitialize function, it will be invoked when the collection is
created.
var tabs = new TabSet([tab1, tab2, tab3]);
collection.models
Raw access to the JavaScript array of models inside of the collection. Usually you'll
want to use get, at, or the Underscore methods
to access model objects, but occasionally a direct reference to the array
is desired.
model.parse(response, [options])
The parse method is called whenever a collection's data is returned in a fetch call. The function is passed
the raw database response array, and should return an array
to be set on the collection. The default implementation
is a no-op, simply passing through the JSON response.
collection.toJSON([options])
Return an array containing the toJSON for each model in the
collection. For more information about toJSON, check outJavaScript's JSON API.
You may pass {shallow: true} in the options to omit any
relations loaded on the collection's models.
collection.fetch([options])
Fetch the default set of models for this collection from the database,
resetting the collection when they arrive. If you wish to trigger an error if the fetched collection
is empty, pass {require: true} as one of the options to the fetch call. A "fetched"
event will be fired when records are successfully retrieved.
collection.load(relations, options).then(function(collection) {...
The load method can be used to eager load attributes onto a Collection, in a similar way
that the withRelated property works on fetch. Nested eager
loads can be specified by separating the nested relations with '.'.
new Story({id: 2}).fetch({ withRelated: ['comments.tags', 'comments.author', 'author'] }).then(function(model) { JSON.stringify(model); });
collection.create(relations, options).then(function(model) {...
Convenience to create a new instance of a model within a collection. Equivalent to instantiating a
model with a hash of attributes, saving the model to the database, and adding the model to the collection
after being successfully created. Returns a promise, resolving with the new model.
collection.query([method], [*parameters])
The query method is used to tap into the underlying Knex
query builder instance for the current collection. If called with no arguments, it will return the query
builder directly. Otherwise, it will call the specified method on the query builder, applying
any additional arguments from the collection.query call.
var qb = collection.query(); qb.where({id: 1}).select().then(function(resp) {... collection .query('where', 'other_id', '=', '5') .fetch() .then(function(collection) { ... });
collection.resetQuery()
Used to reset the internal state of the current query builder instance. This method is
called internally each time a database action is completed by Bookshelf.Sync.
collection.sync(collection, [options])
Creates and returns a new Bookshelf.Sync instance. Can be overridden for custom behavior.
Bookshelf also proxies to Backbone.js for any of the core Collection methods
that don't need modification for the ORM. The Backbone documentation
applies equally for Bookself, and can be used to read more on these methods.
Just as with Backbone, Bookshelf proxies to Underscore.js to provide 28 iteration functions
on Bookshelf.Collection. They aren't all documented here, but
you can take a look at the Underscore documentation for the full details…
Bookshelf.Sync
The Bookshelf.Sync object handles all database communication for models and collections. Unlike Backbone's sync method, Bookshelf.Sync is a constructor taking two arguments: the current model (or collection) and any options needed for the sync method being performed. It comes with five built in methods: first, select, insert, update, and del (delete).
sync.first([options]).then(function(model) {...
Used by the model, the first method selects the first matching and returns a promise,
resolving with the model or collection originally passed in the the Bookshelf.Sync.
sync.select([options]).then(function(model) {...
Used by models and collections, the select method selects rows from the database based on the tableName and idAttrribute, along with
any custom query constraints.
sync.insert([options]).then(function(model) {...
Used by models, and relations, the insert method inserts a single model, based on thetableName and any relation constraints,
returning a promise which resolves with the inserted model.
sync.update([options]).then(function(model) {...
Used by models, and relations, the update method inserts a single model, based on thetableName, idAtribute and any customquery or relation constraints,
returning a promise which resolves with the inserted model.
sync.del([options]).then(function(model) {...
Used by models, collections, and relations, the del method removes one or more models,
based on the tableName, idAtribute
and any custom query or relation constraints,
returning a promise which resolves with no arguments. If no idAtribute or
custom constraints have been defined, the promise will fail.
Bookshelf.Events
Bookshelf mixes in the Events module from Backbone, and therefore each Model and Collection has access
to each of the following methods, which can be referenced in the Backbone documentation.
Alias to Events.trigger.
Alias to this.off(event, null, null);
Here's the complete list of recognized Bookshelf events, with arguments.
Other events from Backbone might be triggered, but aren't officially supported unless they
are noted in this list. You're also free to trigger your own events on Models and Collections and
The Bookshelf object itself mixes in Events, and can be used to emit any
global events that your application needs.
Model:
- "fetched" (model, resp, options) — after a model is fetched.
- "creating" (model, attrs, options) — before a model is inserted, throwing an Error prevents insertion.
- "updating" (model, attrs, options) — before a model is updating, throwing an Error prevents updating.
- "saving" (model, attrs, options) — called both on creating & updating.
- "destroying" (model, options) — before a model is destroyed, throwing an Error prevents destruction.
- "created" (model, resp, options) — after a model is created.
- "updated" (model, resp, options) — after a model is updated.
- "saved" (model, resp, options) — after a model is created or updated.
- "destroyed" (model, resp, options) — when a model is destroyed.
Collection:
- "fetched" (model, resp, options) — after a collection is fetched.
Utility
A reference to the copy of Backbone used to create the Bookshelf objects can be accessed
from Bookshelf.Backbone. This can be helpful if you want to use Models & Collections
elsewhere on the server, and ensure they are compatible with the Bookshelf objects.
A reference to the Knex.js instance being used by Bookshelf.
An alias to Knex.Transaction, the transaction
object must be passed along in the options of any relevant Bookshelf.Sync calls,
to ensure all queries are on the same connection. The entire transaction block is
a promise that will resolve when the transaction is committed, or fail if the
transaction is rolled back.
var When = require('when'); Bookshelf.Transaction(function(t) { new Library({name: 'Old Books'}) .save({transacting: t}) .then(function(model) { return When.all(_.map([ {title: 'Canterbury Tales'}, {title: 'Moby Dick'}, {title: 'Hamlet'} ], function(info) { // Some validation could take place here. return new Book(info).save('shelf_id', model.id, {transacting: t}); })); }) .then(function(rows) { t.commit([model.get('name'), rows.length]); }, t.rollback); }).then(function(resp) { // ['Old Books', 3] console.log(msg); }, function() { console.log('Error saving the books.'); });
F.A.Q.
If you pass {debug: true} as one of the options in your initialize settings, you can see
all of the query calls being made. Sometimes you need to dive a bit further into
the various calls and see what all is going on behind the scenes. I'd recommend node-inspector, which allows you to debug
code with debugger statements like you would in the browser.
The test suite looks for an environment variable called BOOKSHELF_TEST for the path to the database
configuration. If you run the following command:
$ export BOOKSHELF_TEST='/path/to/your/knex_config.js', replacing with the path to your config file,
and the config file is valid, the test suite should run with npm test. If you're going to
add a test, you may want to follow similar patterns, used in the test suite,
setting $ export BOOKSHELF_DEV=1 to save the outputs data from the tests into the shared/output.js file.
While it primarily targets Node.js, all dependencies are browser compatible, and
it could be adapted to work with other javascript environments supporting a sqlite3
database, by providing a custom Knex adapter.
Change Log
— May 13, 2013
Initial Bookshelf release.